From fa0f4d5b4ce9ad3a891b719ca099f4bbc12f1096 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Mon, 18 Mar 2024 10:28:43 -0400
Subject: comments

---
 src/client/views/DocComponent.tsx | 61 ++++++++++++++++++++++++++++++++-------
 1 file changed, 50 insertions(+), 11 deletions(-)

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

diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx
index a25a8f77a..18e7ab12a 100644
--- a/src/client/views/DocComponent.tsx
+++ b/src/client/views/DocComponent.tsx
@@ -75,15 +75,25 @@ export function DocComponent<P extends DocComponentProps>() {
             makeObservable(this);
         }
 
-        //TODO This might be pretty inefficient if doc isn't observed, because computed doesn't cache then
+        /**
+         * This is the document being rendered.  In the case of a compound template, it
+         * may not be the actual document rendered and it also may not be the 'real' root document.
+         * Rather, it specifies the shared properties of all layouts of the document (eg, x,y,)
+         */
         get Document() {
             return this._props.Document;
         }
-        // This is the rendering data of a document -- it may be "The Document", or it may be some template document that holds the rendering info
+
+        /**
+         * This is the document being rendered.  It may be a template so it may or may no inherit from the data doc.
+         */
         @computed get layoutDoc() {
             return this._props.LayoutTemplateString ? this.Document : Doc.Layout(this.Document, this._props.LayoutTemplate?.());
         }
-        // This is the data part of a document -- ie, the data that is constant across all views of the document
+
+        /**
+         * This is the unique data repository for a dcoument that stores the intrinsic document data
+         */
         @computed get dataDoc() {
             return this.Document[DocData];
         }
@@ -109,19 +119,32 @@ export function ViewBoxBaseComponent<P extends FieldViewProps>() {
         get DocumentView() {
             return this._props.DocumentView;
         }
-        //TODO This might be pretty inefficient if doc isn't observed, because computed doesn't cache then
+
+        /**
+         * This is the document being rendered.  In the case of a compound template, it
+         * may not be the actual document rendered and it also may not be the 'real' root document.
+         * Rather, it specifies the shared properties of all layouts of the document (eg, x,y,)
+         */
         get Document() {
             return this._props.Document;
         }
-        // This is the rendering data of a document -- it may be "The Document", or it may be some template document that holds the rendering info
+        /**
+         * This is the document being rendered.  It may be a template so it may or may no inherit from the data doc.
+         */
         @computed get layoutDoc() {
             return Doc.Layout(this.Document);
         }
-        // This is the data part of a document -- ie, the data that is constant across all views of the document
+
+        /**
+         * This is the unique data repository for a dcoument that stores the intrinsic document data
+         */
         @computed get dataDoc() {
             return this.Document.isTemplateForField || this.Document.isTemplateDoc ? this._props.TemplateDataDocument ?? this.Document[DocData] : this.Document[DocData];
         }
-        // key where data is stored
+
+        /**
+         * this is the field key where the primary rendering data is stored for the layout doc (e.g., it's often the 'data' field for a collection, or the 'text' field for rich text)
+         */
         get fieldKey() {
             return this._props.fieldKey;
         }
@@ -154,23 +177,39 @@ export function ViewBoxAnnotatableComponent<P extends FieldViewProps>() {
         get DocumentView() {
             return this._props.DocumentView;
         }
-        //TODO This might be pretty inefficient if doc isn't observed, because computed doesn't cache then
+
+        /**
+         * This is the document being rendered.  In the case of a compound template, it
+         * may not be the actual document rendered and it also may not be the 'real' root document.
+         * Rather, it specifies the shared properties of all layouts of the document (eg, x,y,)
+         */
         @computed get Document() {
             return this._props.Document;
         }
-        // This is the rendering data of a document -- it may be "The Document", or it may be some template document that holds the rendering info
+        /**
+         * This is the document being rendered.  It may be a template so it may or may no inherit from the data doc.
+         */
         @computed get layoutDoc() {
             return Doc.Layout(this.Document);
         }
-        // This is the data part of a document -- ie, the data that is constant across all views of the document
+
+        /**
+         * This is the unique data repository for a dcoument that stores the intrinsic document data
+         */
         @computed get dataDoc() {
             return this.Document.isTemplateForField || this.Document.isTemplateDoc ? this._props.TemplateDataDocument ?? this.Document[DocData] : this.Document[DocData];
         }
 
-        // key where data is stored
+        /**
+         * this is the field key where the primary rendering data is stored for the layout doc (e.g., it's often the 'data' field for a collection, or the 'text' field for rich text)
+         */
         @computed get fieldKey() {
             return this._props.fieldKey;
         }
+
+        /**
+         * this is field key where the list of annotations is stored
+         */
         @computed public get annotationKey() {
             return this.fieldKey + (this._annotationKeySuffix() ? '_' + this._annotationKeySuffix() : '');
         }
-- 
cgit v1.2.3-70-g09d2


From b90f19828ed7785ead8454b3ec92431edf2ce031 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Sat, 30 Mar 2024 09:49:39 -0400
Subject: trying to cleanup import cycles

---
 src/client/views/DocComponent.tsx                       |  6 +++---
 src/client/views/LightboxView.tsx                       | 14 +++++++-------
 src/client/views/StyleProvider.tsx                      |  2 +-
 src/client/views/collections/CollectionPileView.tsx     |  2 +-
 src/client/views/collections/CollectionStackingView.tsx |  4 ++--
 src/client/views/collections/CollectionSubView.tsx      |  2 +-
 src/client/views/collections/CollectionTreeView.tsx     |  2 +-
 src/client/views/collections/TreeView.tsx               |  1 -
 src/client/views/nodes/VideoBox.tsx                     |  6 +++---
 9 files changed, 19 insertions(+), 20 deletions(-)

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

diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx
index 18e7ab12a..de4df1830 100644
--- a/src/client/views/DocComponent.tsx
+++ b/src/client/views/DocComponent.tsx
@@ -5,17 +5,17 @@ import { DateField } from '../../fields/DateField';
 import { Doc, DocListCast, Field, Opt } from '../../fields/Doc';
 import { AclAdmin, AclAugment, AclEdit, AclPrivate, AclReadonly, DocData } from '../../fields/DocSymbols';
 import { List } from '../../fields/List';
+import { RefField } from '../../fields/RefField';
 import { GetEffectiveAcl, inheritParentAcls } from '../../fields/util';
 import { DocumentType } from '../documents/DocumentTypes';
 import { DocUtils } from '../documents/Documents';
 import { DocumentManager } from '../util/DocumentManager';
+import { DragManager } from '../util/DragManager';
 import { ObservableReactComponent } from './ObservableReactComponent';
 import { CollectionFreeFormView } from './collections/collectionFreeForm';
-import { FieldViewProps, FocusViewOptions } from './nodes/FieldView';
 import { DocumentView, OpenWhere } from './nodes/DocumentView';
+import { FieldViewProps, FocusViewOptions } from './nodes/FieldView';
 import { PinProps } from './nodes/trails';
-import { RefField } from '../../fields/RefField';
-import { DragManager } from '../util/DragManager';
 
 /**
  * Shared interface among all viewBox'es (ie, react classes that render the contents of a Doc)
diff --git a/src/client/views/LightboxView.tsx b/src/client/views/LightboxView.tsx
index 79700d8ab..3d77ee901 100644
--- a/src/client/views/LightboxView.tsx
+++ b/src/client/views/LightboxView.tsx
@@ -4,26 +4,26 @@ import { Toggle, ToggleType, Type } from 'browndash-components';
 import { action, computed, makeObservable, observable, runInAction } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
+import { Utils, emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnTrue } from '../../Utils';
 import { Doc, DocListCast, FieldResult, Opt } from '../../fields/Doc';
+import { Id } from '../../fields/FieldSymbols';
 import { InkTool } from '../../fields/InkField';
 import { Cast, NumCast } from '../../fields/Types';
-import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue, Utils } from '../../Utils';
 import { DocUtils } from '../documents/Documents';
 import { DocumentManager } from '../util/DocumentManager';
 import { LinkManager } from '../util/LinkManager';
 import { SelectionManager } from '../util/SelectionManager';
 import { SettingsManager } from '../util/SettingsManager';
+import { SnappingManager } from '../util/SnappingManager';
 import { Transform } from '../util/Transform';
+import { GestureOverlay } from './GestureOverlay';
+import './LightboxView.scss';
+import { ObservableReactComponent } from './ObservableReactComponent';
 import { CollectionDockingView } from './collections/CollectionDockingView';
 import { CollectionStackedTimeline } from './collections/CollectionStackedTimeline';
+import { DefaultStyleProvider, wavyBorderPath } from './StyleProvider';
 import { TabDocView } from './collections/TabDocView';
-import { GestureOverlay } from './GestureOverlay';
-import './LightboxView.scss';
 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;
diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx
index f33f032b7..749198fe6 100644
--- a/src/client/views/StyleProvider.tsx
+++ b/src/client/views/StyleProvider.tsx
@@ -18,6 +18,7 @@ import { LinkManager } from '../util/LinkManager';
 import { SettingsManager } from '../util/SettingsManager';
 import { SnappingManager } from '../util/SnappingManager';
 import { undoBatch, UndoManager } from '../util/UndoManager';
+import { CollectionSchemaView } from './collections/collectionSchema/CollectionSchemaView';
 import { TreeSort } from './collections/TreeSort';
 import { Colors } from './global/globalEnums';
 import { DocumentViewProps } from './nodes/DocumentView';
@@ -25,7 +26,6 @@ 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',
diff --git a/src/client/views/collections/CollectionPileView.tsx b/src/client/views/collections/CollectionPileView.tsx
index 7d7f0bb61..9d68c621b 100644
--- a/src/client/views/collections/CollectionPileView.tsx
+++ b/src/client/views/collections/CollectionPileView.tsx
@@ -6,6 +6,7 @@ import { ScriptField } from '../../../fields/ScriptField';
 import { NumCast, StrCast } from '../../../fields/Types';
 import { emptyFunction, returnFalse, setupMoveUpEvents } from '../../../Utils';
 import { DocUtils } from '../../documents/Documents';
+import { dropActionType } from '../../util/DragManager';
 import { SelectionManager } from '../../util/SelectionManager';
 import { undoBatch, UndoManager } from '../../util/UndoManager';
 import { OpenWhere } from '../nodes/DocumentView';
@@ -13,7 +14,6 @@ import { computePassLayout, computeStarburstLayout } from './collectionFreeForm'
 import { CollectionFreeFormView } from './collectionFreeForm/CollectionFreeFormView';
 import './CollectionPileView.scss';
 import { CollectionSubView } from './CollectionSubView';
-import { dropActionType } from '../../util/DragManager';
 
 @observer
 export class CollectionPileView extends CollectionSubView() {
diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx
index 2b23935eb..3a62a53d7 100644
--- a/src/client/views/collections/CollectionStackingView.tsx
+++ b/src/client/views/collections/CollectionStackingView.tsx
@@ -11,7 +11,7 @@ import { listSpec } from '../../../fields/Schema';
 import { SchemaHeaderField } from '../../../fields/SchemaHeaderField';
 import { BoolCast, Cast, DocCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
 import { TraceMobx } from '../../../fields/util';
-import { DivHeight, emptyFunction, returnEmptyDoclist, returnFalse, returnNone, returnZero, setupMoveUpEvents, smoothScroll, Utils } from '../../../Utils';
+import { DivHeight, emptyFunction, returnEmptyDoclist, returnNone, returnZero, setupMoveUpEvents, smoothScroll, Utils } from '../../../Utils';
 import { Docs, DocUtils } from '../../documents/Documents';
 import { CollectionViewType } from '../../documents/DocumentTypes';
 import { DragManager, dropActionType } from '../../util/DragManager';
@@ -24,7 +24,7 @@ import { EditableView } from '../EditableView';
 import { LightboxView } from '../LightboxView';
 import { CollectionFreeFormDocumentView } from '../nodes/CollectionFreeFormDocumentView';
 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 { CollectionMasonryViewFieldRow } from './CollectionMasonryViewFieldRow';
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index 59a695a3d..ecc2aea31 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -15,13 +15,13 @@ import { GestureUtils } from '../../../pen-gestures/GestureUtils';
 import { DocServer } from '../../DocServer';
 import { Networking } from '../../Network';
 import { CollectionViewType, DocumentType } from '../../documents/DocumentTypes';
+import { ViewBoxBaseComponent } from '../DocComponent';
 import { DocUtils, Docs, DocumentOptions } from '../../documents/Documents';
 import { DragManager, dropActionType } from '../../util/DragManager';
 import { ImageUtils } from '../../util/Import & Export/ImageUtils';
 import { SelectionManager } from '../../util/SelectionManager';
 import { SnappingManager } from '../../util/SnappingManager';
 import { UndoManager, undoBatch } from '../../util/UndoManager';
-import { ViewBoxBaseComponent } from '../DocComponent';
 import { LoadingBox } from '../nodes/LoadingBox';
 import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox';
 import { CollectionView, CollectionViewProps } from './CollectionView';
diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx
index 3b37bdcfa..293c79119 100644
--- a/src/client/views/collections/CollectionTreeView.tsx
+++ b/src/client/views/collections/CollectionTreeView.tsx
@@ -12,6 +12,7 @@ import { DivHeight, emptyFunction, returnAll, returnEmptyDoclist, returnEmptyFil
 import { Docs, DocUtils } from '../../documents/Documents';
 import { DocumentManager } from '../../util/DocumentManager';
 import { DragManager, dropActionType } from '../../util/DragManager';
+import { ScriptingGlobals } from '../../util/ScriptingGlobals';
 import { SelectionManager } from '../../util/SelectionManager';
 import { SnappingManager } from '../../util/SnappingManager';
 import { Transform } from '../../util/Transform';
@@ -27,7 +28,6 @@ import { CollectionFreeFormView } from './collectionFreeForm';
 import { CollectionSubView } from './CollectionSubView';
 import './CollectionTreeView.scss';
 import { TreeView } from './TreeView';
-import { ScriptingGlobals } from '../../util/ScriptingGlobals';
 const _global = (window /* browser */ || global) /* node */ as any;
 
 export type collectionTreeViewProps = {
diff --git a/src/client/views/collections/TreeView.tsx b/src/client/views/collections/TreeView.tsx
index 1b4349f44..5eac6cb09 100644
--- a/src/client/views/collections/TreeView.tsx
+++ b/src/client/views/collections/TreeView.tsx
@@ -19,7 +19,6 @@ import { DocUtils, Docs } from '../../documents/Documents';
 import { DocumentManager } from '../../util/DocumentManager';
 import { DragManager, dropActionType } from '../../util/DragManager';
 import { LinkManager } from '../../util/LinkManager';
-import { ScriptingGlobals } from '../../util/ScriptingGlobals';
 import { SettingsManager } from '../../util/SettingsManager';
 import { SnappingManager } from '../../util/SnappingManager';
 import { Transform } from '../../util/Transform';
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index 0af0751bf..d6881b973 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -8,12 +8,13 @@ import { DocData } from '../../../fields/DocSymbols';
 import { InkTool } from '../../../fields/InkField';
 import { List } from '../../../fields/List';
 import { ObjectField } from '../../../fields/ObjectField';
-import { Cast, DocCast, NumCast, StrCast } from '../../../fields/Types';
+import { Cast, NumCast, StrCast } from '../../../fields/Types';
 import { AudioField, ImageField, VideoField } from '../../../fields/URLField';
 import { emptyFunction, formatTime, returnEmptyString, returnFalse, returnOne, returnZero, setupMoveUpEvents, Utils } from '../../../Utils';
 import { Docs, DocUtils } from '../../documents/Documents';
 import { DocumentType } from '../../documents/DocumentTypes';
 import { DocumentManager } from '../../util/DocumentManager';
+import { dropActionType } from '../../util/DragManager';
 import { FollowLinkScript } from '../../util/LinkFollower';
 import { LinkManager } from '../../util/LinkManager';
 import { ReplayMovements } from '../../util/ReplayMovements';
@@ -27,11 +28,10 @@ import { MarqueeAnnotator } from '../MarqueeAnnotator';
 import { AnchorMenu } from '../pdf/AnchorMenu';
 import { StyleProp } from '../StyleProvider';
 import { DocumentView } from './DocumentView';
-import { FocusViewOptions, FieldView, FieldViewProps } from './FieldView';
+import { FieldView, FieldViewProps, FocusViewOptions } from './FieldView';
 import { RecordingBox } from './RecordingBox';
 import { PinProps, PresBox } from './trails';
 import './VideoBox.scss';
-import { dropActionType } from '../../util/DragManager';
 
 /**
  * VideoBox
-- 
cgit v1.2.3-70-g09d2


From 2a313f28fcb8675223708b0657de7517a3281095 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Wed, 17 Apr 2024 12:27:21 -0400
Subject: restoring eslint - updates not complete yet

---
 .eslintrc.json                                     |   69 +-
 package-lock.json                                  |  838 +++++++++++++--
 package.json                                       |   17 +-
 src/ServerUtils.ts                                 |   27 +
 src/Utils.ts                                       |  865 ++--------------
 src/client/DocServer.ts                            |  228 ++---
 src/client/Network.ts                              |   11 +-
 .../apis/google_docs/GooglePhotosClientUtils.ts    |   10 +-
 src/client/apis/gpt/GPT.ts                         |    5 +
 src/client/apis/youtube/YoutubeBox.scss            |  126 ---
 src/client/apis/youtube/YoutubeBox.tsx             |  369 -------
 src/client/cognitive_services/CognitiveServices.ts |   24 +-
 src/client/documents/DocumentTypes.ts              |    6 +-
 src/client/documents/Documents.ts                  |  317 ++++--
 src/client/util/CaptureManager.tsx                 |    2 +-
 src/client/util/CurrentUserUtils.ts                |  250 ++---
 src/client/util/DictationManager.ts                |    6 +-
 src/client/util/DocumentManager.ts                 |   80 +-
 src/client/util/DragManager.ts                     |  175 ++--
 src/client/util/DropActionTypes.ts                 |    9 +
 src/client/util/GroupManager.tsx                   |   20 +-
 src/client/util/History.ts                         |    4 +-
 src/client/util/HypothesisUtils.ts                 |    4 +-
 src/client/util/Import & Export/ImageUtils.ts      |   10 +-
 src/client/util/InteractionUtils.tsx               |  317 ++----
 src/client/util/LinkFollower.ts                    |    4 +-
 src/client/util/LinkManager.ts                     |   49 +-
 src/client/util/Scripting.ts                       |   52 +-
 src/client/util/ScriptingGlobals.ts                |   47 +-
 src/client/util/SearchUtil.ts                      |    6 +-
 src/client/util/SerializationHelper.ts             |   29 +-
 src/client/util/SettingsManager.tsx                |   86 +-
 src/client/util/SharingManager.tsx                 |   27 +-
 src/client/util/SnappingManager.ts                 |   31 +-
 src/client/util/UndoManager.ts                     |   13 +-
 src/client/util/bezierFit.ts                       |  395 ++++----
 src/client/util/reportManager/ReportManager.tsx    |   31 +-
 src/client/views/ContextMenu.tsx                   |   30 +-
 src/client/views/ContextMenuItem.tsx               |   27 +-
 src/client/views/DashboardView.tsx                 |   62 +-
 src/client/views/DocComponent.tsx                  |   51 +-
 src/client/views/DocumentButtonBar.tsx             |   18 +-
 src/client/views/DocumentDecorations.tsx           |   98 +-
 src/client/views/FilterPanel.tsx                   |  162 +--
 src/client/views/GestureOverlay.tsx                |  214 ++--
 src/client/views/GlobalKeyHandler.ts               |   73 +-
 src/client/views/InkControlPtHandles.tsx           |    2 +-
 src/client/views/InkTangentHandles.tsx             |    3 +-
 src/client/views/InkingStroke.tsx                  |   15 +-
 src/client/views/LightboxView.tsx                  |    9 +-
 src/client/views/MainView.tsx                      |  132 ++-
 src/client/views/MainViewModal.tsx                 |    4 +-
 src/client/views/MarqueeAnnotator.tsx              |    2 +-
 src/client/views/MetadataEntryMenu.tsx             |    9 +-
 src/client/views/ObservableReactComponent.tsx      |    7 +-
 src/client/views/OverlayView.tsx                   |   37 +-
 src/client/views/PreviewCursor.tsx                 |    2 +-
 src/client/views/PropertiesButtons.tsx             |    8 +-
 src/client/views/PropertiesView.tsx                | 1062 +++++++++++---------
 src/client/views/ScriptBox.tsx                     |    2 +-
 src/client/views/SidebarAnnos.tsx                  |   13 +-
 src/client/views/StyleProvider.tsx                 |   23 +-
 src/client/views/TemplateMenu.tsx                  |   70 +-
 src/client/views/Touchable.tsx                     |  213 ----
 src/client/views/animationtimeline/Timeline.tsx    |    3 +-
 .../views/animationtimeline/TimelineMenu.tsx       |   25 +-
 .../views/collections/CollectionCalendarView.tsx   |    3 +-
 .../views/collections/CollectionCarousel3DView.tsx |   97 +-
 .../views/collections/CollectionCarouselView.tsx   |    5 +-
 .../views/collections/CollectionDockingView.tsx    |  156 +--
 .../collections/CollectionMasonryViewFieldRow.tsx  |    3 +-
 src/client/views/collections/CollectionMenu.tsx    |   38 +-
 .../views/collections/CollectionNoteTakingView.tsx |   15 +-
 .../collections/CollectionNoteTakingViewColumn.tsx |    2 +-
 .../CollectionNoteTakingViewDivider.tsx            |    3 +-
 .../views/collections/CollectionPileView.tsx       |    5 +-
 .../collections/CollectionStackedTimeline.tsx      |    3 +-
 .../views/collections/CollectionStackingView.tsx   |   79 +-
 .../CollectionStackingViewFieldColumn.tsx          |   80 +-
 src/client/views/collections/CollectionSubView.tsx |  173 ++--
 .../views/collections/CollectionTimeView.tsx       |    5 +-
 .../views/collections/CollectionTreeView.tsx       |    6 +-
 src/client/views/collections/CollectionView.tsx    |   75 +-
 src/client/views/collections/TabDocView.tsx        |   69 +-
 src/client/views/collections/TreeView.tsx          |  307 +++---
 .../CollectionFreeFormInfoUI.tsx                   |   31 +-
 .../CollectionFreeFormLayoutEngines.tsx            |   18 +-
 .../CollectionFreeFormPannableContents.tsx         |   18 +-
 .../CollectionFreeFormRemoteCursors.tsx            |    6 +-
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  122 ++-
 .../collections/collectionFreeForm/MarqueeView.tsx |   50 +-
 .../collectionGrid/CollectionGridView.tsx          |    3 +-
 .../collectionLinear/CollectionLinearView.tsx      |   66 +-
 .../collectionSchema/CollectionSchemaView.tsx      |   24 +-
 .../collectionSchema/SchemaColumnHeader.tsx        |    3 +-
 .../collections/collectionSchema/SchemaRowBox.tsx  |    3 +-
 .../collectionSchema/SchemaTableCell.tsx           |   12 +-
 src/client/views/global/globalScripts.ts           |  137 +--
 src/client/views/linking/LinkMenuItem.tsx          |    6 +-
 src/client/views/linking/LinkPopup.tsx             |    3 +-
 src/client/views/newlightbox/NewLightboxView.tsx   |    3 +-
 src/client/views/nodes/AudioBox.tsx                |  106 +-
 .../views/nodes/CollectionFreeFormDocumentView.tsx |    9 +-
 src/client/views/nodes/ComparisonBox.tsx           |   54 +-
 src/client/views/nodes/DataVizBox/DataVizBox.tsx   |   10 +-
 .../views/nodes/DataVizBox/SchemaCSVPopUp.tsx      |    5 +-
 .../nodes/DataVizBox/components/Histogram.tsx      |   26 +-
 .../nodes/DataVizBox/components/LineChart.tsx      |   80 +-
 .../views/nodes/DataVizBox/components/PieChart.tsx |   24 +-
 .../views/nodes/DataVizBox/components/TableBox.tsx |    5 +-
 src/client/views/nodes/DocumentContentsView.tsx    |    5 +-
 src/client/views/nodes/DocumentIcon.tsx            |   63 +-
 src/client/views/nodes/DocumentLinksButton.tsx     |    9 +-
 src/client/views/nodes/DocumentView.tsx            |  282 +++---
 src/client/views/nodes/EquationBox.tsx             |    2 +-
 src/client/views/nodes/FieldView.tsx               |   10 +-
 src/client/views/nodes/FontIconBox/FontIconBox.tsx |    5 +-
 src/client/views/nodes/FunctionPlotBox.tsx         |   10 +-
 src/client/views/nodes/ImageBox.tsx                |  117 ++-
 src/client/views/nodes/KeyValueBox.tsx             |   10 +-
 src/client/views/nodes/KeyValuePair.tsx            |    3 +-
 src/client/views/nodes/LabelBox.tsx                |   10 +-
 src/client/views/nodes/LinkAnchorBox.tsx           |   15 +-
 src/client/views/nodes/LinkBox.tsx                 |    3 +-
 src/client/views/nodes/LinkDocPreview.tsx          |    3 +-
 .../views/nodes/MapBox/DirectionsAnchorMenu.tsx    |    3 +-
 src/client/views/nodes/MapBox/MapAnchorMenu.tsx    |    3 +-
 src/client/views/nodes/MapBox/MapBox.tsx           |   11 +-
 .../views/nodes/MapboxMapBox/MapboxContainer.tsx   |   76 +-
 src/client/views/nodes/PDFBox.tsx                  |   31 +-
 .../views/nodes/RecordingBox/RecordingBox.tsx      |   15 +-
 .../views/nodes/RecordingBox/RecordingView.tsx     |    2 +-
 src/client/views/nodes/ScreenshotBox.tsx           |   17 +-
 src/client/views/nodes/ScriptingBox.tsx            |    2 +-
 src/client/views/nodes/VideoBox.tsx                |   21 +-
 src/client/views/nodes/WebBox.tsx                  |   38 +-
 src/client/views/nodes/calendarBox/CalendarBox.tsx |    2 +-
 .../views/nodes/formattedText/DashDocView.tsx      |    3 +-
 .../views/nodes/formattedText/DashFieldView.tsx    |    8 +-
 .../views/nodes/formattedText/FormattedTextBox.tsx |  137 ++-
 .../formattedText/FormattedTextBoxComment.tsx      |    7 +-
 .../formattedText/ProsemirrorExampleTransfer.ts    |   60 +-
 .../views/nodes/formattedText/RichTextMenu.tsx     |    2 +-
 .../views/nodes/formattedText/RichTextRules.ts     |  121 ++-
 src/client/views/nodes/formattedText/marks_rts.ts  |    7 +-
 src/client/views/nodes/formattedText/nodes_rts.ts  |    4 +-
 .../views/nodes/generativeFill/GenerativeFill.tsx  |   91 +-
 .../views/nodes/importBox/ImportElementBox.tsx     |    2 +-
 src/client/views/nodes/trails/PresBox.tsx          |  176 ++--
 src/client/views/nodes/trails/PresElementBox.tsx   |    3 +-
 src/client/views/pdf/AnchorMenu.tsx                |   22 +-
 src/client/views/pdf/GPTPopup/GPTPopup.tsx         |  100 +-
 src/client/views/pdf/PDFViewer.tsx                 |   21 +-
 src/client/views/search/SearchBox.tsx              |    4 +-
 src/client/views/topbar/TopBar.tsx                 |    5 +-
 src/debug/Viewer.tsx                               |    6 +-
 src/fields/DateField.ts                            |    3 +-
 src/fields/Doc.ts                                  |  182 +---
 src/fields/InkField.ts                             |   11 +-
 src/fields/List.ts                                 |   61 +-
 src/fields/ObjectField.ts                          |   25 +-
 src/fields/RefField.ts                             |    4 +-
 src/fields/RichTextUtils.ts                        |   17 +-
 src/fields/Schema.ts                               |    6 +-
 src/fields/SchemaHeaderField.ts                    |   25 +-
 src/fields/ScriptField.ts                          |   96 +-
 src/fields/Types.ts                                |   73 +-
 src/fields/URLField.ts                             |   32 +-
 src/fields/util.ts                                 |  204 ++--
 src/mobile/ImageUpload.tsx                         |   32 +-
 src/mobile/MobileInkOverlay.tsx                    |    6 +-
 src/mobile/MobileInterface.tsx                     |    2 +-
 src/pen-gestures/GestureTypes.ts                   |   16 +
 src/pen-gestures/GestureUtils.ts                   |   26 +-
 src/pen-gestures/ndollar.ts                        |   10 +-
 src/server/ActionUtilities.ts                      |   71 +-
 src/server/ApiManagers/DataVizManager.ts           |   15 +-
 src/server/ApiManagers/DeleteManager.ts            |    2 +-
 src/server/ApiManagers/DownloadManager.ts          |  385 ++++---
 src/server/ApiManagers/GeneralGoogleManager.ts     |   39 +-
 src/server/ApiManagers/MongoStore.js               |  414 ++++++++
 src/server/ApiManagers/SearchManager.ts            |    8 +-
 src/server/ApiManagers/SessionManager.ts           |   61 +-
 src/server/ApiManagers/UploadManager.ts            |  346 +++----
 src/server/ApiManagers/UserManager.ts              |   60 +-
 src/server/Client.ts                               |    8 +-
 src/server/DashSession/DashSessionAgent.ts         |   28 +-
 src/server/DashStats.ts                            |  241 +++--
 src/server/DashUploadUtils.ts                      |  748 +++++++-------
 src/server/Message.ts                              |   91 +-
 src/server/RouteManager.ts                         |   92 +-
 src/server/SocketData.ts                           |   35 +
 src/server/apis/google/GoogleApiServerUtils.ts     |    8 +-
 src/server/authentication/AuthenticationManager.ts |    2 +-
 src/server/authentication/DashUserModel.ts         |   10 +-
 src/server/database.ts                             |  166 +--
 src/server/index.ts                                |   16 +-
 src/server/remapUrl.ts                             |   69 +-
 src/server/server_Initialization.ts                |  251 ++---
 src/server/websocket.ts                            |  380 +++----
 tsconfig.json                                      |    2 +-
 webpack.config.js                                  |    7 +-
 202 files changed, 7525 insertions(+), 7291 deletions(-)
 create mode 100644 src/ServerUtils.ts
 delete mode 100644 src/client/apis/youtube/YoutubeBox.scss
 delete mode 100644 src/client/apis/youtube/YoutubeBox.tsx
 create mode 100644 src/client/util/DropActionTypes.ts
 delete mode 100644 src/client/views/Touchable.tsx
 create mode 100644 src/pen-gestures/GestureTypes.ts
 create mode 100644 src/server/ApiManagers/MongoStore.js
 create mode 100644 src/server/SocketData.ts

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

diff --git a/.eslintrc.json b/.eslintrc.json
index 43bb53566..0c4e375a9 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -1,18 +1,77 @@
 {
-    "extends": ["airbnb", "prettier", "plugin:node/recommended"],
-    "plugins": ["prettier"],
+    "extends": ["airbnb", "prettier", "plugin:node/recommended", "plugin:import/react"],
+    "plugins": ["prettier", "import", "@typescript-eslint"],
+    "settings": {
+        "import/parsers": {
+            "@typescript-eslint/parser": [".ts", ".tsx"]
+        },
+        "import/resolver": {
+            "typescript": {
+                "alwaysTryTypes": true // always try to resolve types under `<root>@types` directory even it doesn't contain any source code, like `@types/unist`
+            },
+            "node": {
+                "extensions": [".js", ".ts", ".tsx"]
+            }
+        },
+        "import/extensions": [".js", ".jsx", ".ts", ".tsx"]
+    },
     "rules": {
+        "import/extensions": [
+            "warn",
+            "ignorePackages",
+            {
+                "": "never",
+                "ts": "never",
+                "tsx": "never"
+            }
+        ],
+        "node/no-missing-import": 0,
         "prettier/prettier": "error",
-        "no-unused-vars": "warn",
         "no-console": "off",
         "func-names": "off",
         "no-process-exit": "off",
         "object-shorthand": "off",
         "class-methods-use-this": "off",
-        "single-quote": "off"
+        "single-quote": "off",
+        "max-classes-per-file": 0,
+        "node/no-unsupported-features/es-syntax": ["error", { "ignores": ["modules"] }],
+        "react/jsx-filename-extension": [2, { "extensions": [".js", ".jsx", ".ts", ".tsx"] }],
+        "import/prefer-default-export": "off",
+        "no-unused-expressions": "off",
+        "prefer-template": "off",
+        "no-inner-declarations": "off",
+        "no-plusplus": "off",
+        "no-multi-assign": "off",
+        "no-underscore-dangle": "off",
+        "no-nested-ternary": "off",
+        "lines-between-class-members": "off",
+        // Note: you must disable the base rule as it can report incorrect errors
+        "no-shadow": "off",
+        "@typescript-eslint/no-shadow": "warn",
+        "no-unused-vars": "off",
+        "@typescript-eslint/no-unused-vars": "error",
+        "react/destructuring-assignment": 0,
+        "no-restricted-globals": ["error", "event"],
+        "no-param-reassign": ["error", { "props": false }],
+        "no-alert": 0,
+        "radix": "off"
     },
+    "overrides": [
+        {
+            "files": ["*.ts", "*.tsx"],
+            "rules": {
+                "no-undef": "off"
+            }
+        }
+    ],
+    "parser": "@typescript-eslint/parser",
     "parserOptions": {
         "ecmaVersion": 11,
-        "sourceType": "module"
+        "sourceType": "module",
+        "ecmaFeatures": {
+            "jsx": true,
+            "modules": true
+        },
+        "useJSXTextNode": false
     }
 }
diff --git a/package-lock.json b/package-lock.json
index 6d0df30e8..d366bb717 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -48,6 +48,8 @@
                 "@types/reveal": "^4.2.0",
                 "@types/supercluster": "^7.1.3",
                 "@types/web": "^0.0.143",
+                "@types/webpack-hot-middleware": "^2.25.9",
+                "@typescript-eslint/parser": "^7.6.0",
                 "@webscopeio/react-textarea-autocomplete": "^4.9.2",
                 "adm-zip": "^0.5.10",
                 "archiver": "^7.0.1",
@@ -84,6 +86,7 @@
                 "D": "^1.0.0",
                 "d3": "^7.8.5",
                 "depcheck": "^1.4.7",
+                "eslint-webpack-plugin": "^4.1.0",
                 "exif": "^0.6.0",
                 "exifr": "^7.1.3",
                 "express": "^4.18.2",
@@ -203,7 +206,9 @@
                 "tough-cookie": "^4.1.3",
                 "tslint": "^6.1.3",
                 "tslint-loader": "^3.5.4",
+                "typescript": "^5.3.3",
                 "typescript-collections": "^1.3.3",
+                "typescript-eslint": "^7.6.0",
                 "typescript-language-server": "^4.1.3",
                 "uninstall": "^0.0.0",
                 "url": "^0.11.3",
@@ -215,6 +220,7 @@
                 "webpack": "^5.89.0",
                 "webpack-cli": "^5.1.4",
                 "webpack-dev-middleware": "^7.0.0",
+                "webpack-hot-middleware": "^2.25.4",
                 "wikijs": "^6.4.1",
                 "words-to-numbers": "^1.5.1",
                 "xoauth2": "^1.2.0",
@@ -263,7 +269,6 @@
                 "@types/uuid": "^9.0.7",
                 "@types/valid-url": "^1.0.7",
                 "@types/webpack": "^5.28.5",
-                "@types/webpack-hot-middleware": "^2.25.9",
                 "@types/youtube": "0.0.50",
                 "chai": "^5.0.0",
                 "cross-env": "^7.0.3",
@@ -272,7 +277,8 @@
                 "eslint-config-airbnb": "^19.0.4",
                 "eslint-config-node": "^4.1.0",
                 "eslint-config-prettier": "^9.1.0",
-                "eslint-plugin-import": "^2.29.0",
+                "eslint-import-resolver-typescript": "^3.6.1",
+                "eslint-plugin-import": "^2.29.1",
                 "eslint-plugin-jsx-a11y": "^6.8.0",
                 "eslint-plugin-node": "^11.1.0",
                 "eslint-plugin-prettier": "^5.0.1",
@@ -287,16 +293,16 @@
                 "ts-loader": "^9.5.1",
                 "ts-node": "^10.9.1",
                 "ts-node-dev": "^2.0.0",
-                "typescript": "^5.3.3",
-                "webpack-dev-server": "^5.0.4",
-                "webpack-hot-middleware": "^2.25.4"
+                "webpack-dev-server": "^5.0.4"
+            },
+            "engines": {
+                "node": ">=10.0.0"
             }
         },
         "node_modules/@aashutoshrathi/word-wrap": {
             "version": "1.2.6",
             "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz",
             "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==",
-            "dev": true,
             "engines": {
                 "node": ">=0.10.0"
             }
@@ -2415,7 +2421,6 @@
             "version": "4.4.0",
             "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz",
             "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==",
-            "dev": true,
             "dependencies": {
                 "eslint-visitor-keys": "^3.3.0"
             },
@@ -2430,7 +2435,6 @@
             "version": "4.10.0",
             "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz",
             "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==",
-            "dev": true,
             "engines": {
                 "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
             }
@@ -2439,7 +2443,6 @@
             "version": "2.1.4",
             "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz",
             "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==",
-            "dev": true,
             "dependencies": {
                 "ajv": "^6.12.4",
                 "debug": "^4.3.2",
@@ -2461,14 +2464,12 @@
         "node_modules/@eslint/eslintrc/node_modules/argparse": {
             "version": "2.0.1",
             "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
-            "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
-            "dev": true
+            "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
         },
         "node_modules/@eslint/eslintrc/node_modules/brace-expansion": {
             "version": "1.1.11",
             "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
             "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
-            "dev": true,
             "dependencies": {
                 "balanced-match": "^1.0.0",
                 "concat-map": "0.0.1"
@@ -2478,7 +2479,6 @@
             "version": "13.24.0",
             "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz",
             "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==",
-            "dev": true,
             "dependencies": {
                 "type-fest": "^0.20.2"
             },
@@ -2493,7 +2493,6 @@
             "version": "4.1.0",
             "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
             "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
-            "dev": true,
             "dependencies": {
                 "argparse": "^2.0.1"
             },
@@ -2505,7 +2504,6 @@
             "version": "3.1.2",
             "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
             "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
-            "dev": true,
             "dependencies": {
                 "brace-expansion": "^1.1.7"
             },
@@ -2517,7 +2515,6 @@
             "version": "8.57.0",
             "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz",
             "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==",
-            "dev": true,
             "engines": {
                 "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
             }
@@ -2758,7 +2755,6 @@
             "version": "0.11.14",
             "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz",
             "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==",
-            "dev": true,
             "dependencies": {
                 "@humanwhocodes/object-schema": "^2.0.2",
                 "debug": "^4.3.1",
@@ -2772,7 +2768,6 @@
             "version": "1.1.11",
             "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
             "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
-            "dev": true,
             "dependencies": {
                 "balanced-match": "^1.0.0",
                 "concat-map": "0.0.1"
@@ -2782,7 +2777,6 @@
             "version": "3.1.2",
             "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
             "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
-            "dev": true,
             "dependencies": {
                 "brace-expansion": "^1.1.7"
             },
@@ -2794,7 +2788,6 @@
             "version": "1.0.1",
             "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
             "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
-            "dev": true,
             "engines": {
                 "node": ">=12.22"
             },
@@ -2806,8 +2799,7 @@
         "node_modules/@humanwhocodes/object-schema": {
             "version": "2.0.3",
             "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz",
-            "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==",
-            "dev": true
+            "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA=="
         },
         "node_modules/@icons/material": {
             "version": "0.2.4",
@@ -2891,6 +2883,97 @@
                 "url": "https://github.com/chalk/strip-ansi?sponsor=1"
             }
         },
+        "node_modules/@jest/schemas": {
+            "version": "29.6.3",
+            "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz",
+            "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==",
+            "dependencies": {
+                "@sinclair/typebox": "^0.27.8"
+            },
+            "engines": {
+                "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+            }
+        },
+        "node_modules/@jest/types": {
+            "version": "29.6.3",
+            "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz",
+            "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==",
+            "dependencies": {
+                "@jest/schemas": "^29.6.3",
+                "@types/istanbul-lib-coverage": "^2.0.0",
+                "@types/istanbul-reports": "^3.0.0",
+                "@types/node": "*",
+                "@types/yargs": "^17.0.8",
+                "chalk": "^4.0.0"
+            },
+            "engines": {
+                "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+            }
+        },
+        "node_modules/@jest/types/node_modules/ansi-styles": {
+            "version": "4.3.0",
+            "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+            "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+            "dependencies": {
+                "color-convert": "^2.0.1"
+            },
+            "engines": {
+                "node": ">=8"
+            },
+            "funding": {
+                "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+            }
+        },
+        "node_modules/@jest/types/node_modules/chalk": {
+            "version": "4.1.2",
+            "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+            "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+            "dependencies": {
+                "ansi-styles": "^4.1.0",
+                "supports-color": "^7.1.0"
+            },
+            "engines": {
+                "node": ">=10"
+            },
+            "funding": {
+                "url": "https://github.com/chalk/chalk?sponsor=1"
+            }
+        },
+        "node_modules/@jest/types/node_modules/color-convert": {
+            "version": "2.0.1",
+            "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+            "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+            "dependencies": {
+                "color-name": "~1.1.4"
+            },
+            "engines": {
+                "node": ">=7.0.0"
+            }
+        },
+        "node_modules/@jest/types/node_modules/color-name": {
+            "version": "1.1.4",
+            "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+            "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
+        },
+        "node_modules/@jest/types/node_modules/has-flag": {
+            "version": "4.0.0",
+            "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+            "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+            "engines": {
+                "node": ">=8"
+            }
+        },
+        "node_modules/@jest/types/node_modules/supports-color": {
+            "version": "7.2.0",
+            "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+            "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+            "dependencies": {
+                "has-flag": "^4.0.0"
+            },
+            "engines": {
+                "node": ">=8"
+            }
+        },
         "node_modules/@jimp/bmp": {
             "version": "0.22.12",
             "resolved": "https://registry.npmjs.org/@jimp/bmp/-/bmp-0.22.12.tgz",
@@ -3817,7 +3900,6 @@
             "version": "2.1.5",
             "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
             "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
-            "dev": true,
             "dependencies": {
                 "@nodelib/fs.stat": "2.0.5",
                 "run-parallel": "^1.1.9"
@@ -3830,7 +3912,6 @@
             "version": "2.0.5",
             "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
             "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
-            "dev": true,
             "engines": {
                 "node": ">= 8"
             }
@@ -3839,7 +3920,6 @@
             "version": "1.2.8",
             "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
             "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
-            "dev": true,
             "dependencies": {
                 "@nodelib/fs.scandir": "2.1.5",
                 "fastq": "^1.6.0"
@@ -6733,6 +6813,11 @@
                 "url": "https://ko-fi.com/killymxi"
             }
         },
+        "node_modules/@sinclair/typebox": {
+            "version": "0.27.8",
+            "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz",
+            "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA=="
+        },
         "node_modules/@sindresorhus/is": {
             "version": "6.2.0",
             "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-6.2.0.tgz",
@@ -8503,7 +8588,6 @@
             "version": "3.4.38",
             "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz",
             "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==",
-            "dev": true,
             "dependencies": {
                 "@types/node": "*"
             }
@@ -8978,6 +9062,27 @@
                 "@types/node": "*"
             }
         },
+        "node_modules/@types/istanbul-lib-coverage": {
+            "version": "2.0.6",
+            "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz",
+            "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w=="
+        },
+        "node_modules/@types/istanbul-lib-report": {
+            "version": "3.0.3",
+            "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz",
+            "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==",
+            "dependencies": {
+                "@types/istanbul-lib-coverage": "*"
+            }
+        },
+        "node_modules/@types/istanbul-reports": {
+            "version": "3.0.4",
+            "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz",
+            "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==",
+            "dependencies": {
+                "@types/istanbul-lib-report": "*"
+            }
+        },
         "node_modules/@types/jquery": {
             "version": "3.5.29",
             "resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.29.tgz",
@@ -9359,6 +9464,11 @@
             "integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==",
             "optional": true
         },
+        "node_modules/@types/semver": {
+            "version": "7.5.8",
+            "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz",
+            "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ=="
+        },
         "node_modules/@types/send": {
             "version": "0.17.4",
             "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz",
@@ -9494,7 +9604,6 @@
             "version": "2.25.9",
             "resolved": "https://registry.npmjs.org/@types/webpack-hot-middleware/-/webpack-hot-middleware-2.25.9.tgz",
             "integrity": "sha512-fad4T9VfocBjS2fZxlqkGoXoVUAjVp0EEnKBRqPwnhEEDN/FqJoFkSP5t9O1gPH75qsyG2kkT/GSUqSNTn1ZPg==",
-            "dev": true,
             "dependencies": {
                 "@types/connect": "*",
                 "tapable": "^2.2.0",
@@ -9518,12 +9627,311 @@
                 "@types/node": "*"
             }
         },
+        "node_modules/@types/yargs": {
+            "version": "17.0.32",
+            "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz",
+            "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==",
+            "dependencies": {
+                "@types/yargs-parser": "*"
+            }
+        },
+        "node_modules/@types/yargs-parser": {
+            "version": "21.0.3",
+            "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz",
+            "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ=="
+        },
         "node_modules/@types/youtube": {
             "version": "0.0.50",
             "resolved": "https://registry.npmjs.org/@types/youtube/-/youtube-0.0.50.tgz",
             "integrity": "sha512-d4GpH4uPYp9W07kc487tiq6V/EUHl18vZWFMbQoe4Sk9LXEWzFi/BMf9x7TI4m7/j7gU3KeX8H6M8aPBgykeLw==",
             "dev": true
         },
+        "node_modules/@typescript-eslint/eslint-plugin": {
+            "version": "7.6.0",
+            "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.6.0.tgz",
+            "integrity": "sha512-gKmTNwZnblUdnTIJu3e9kmeRRzV2j1a/LUO27KNNAnIC5zjy1aSvXSRp4rVNlmAoHlQ7HzX42NbKpcSr4jF80A==",
+            "dependencies": {
+                "@eslint-community/regexpp": "^4.10.0",
+                "@typescript-eslint/scope-manager": "7.6.0",
+                "@typescript-eslint/type-utils": "7.6.0",
+                "@typescript-eslint/utils": "7.6.0",
+                "@typescript-eslint/visitor-keys": "7.6.0",
+                "debug": "^4.3.4",
+                "graphemer": "^1.4.0",
+                "ignore": "^5.3.1",
+                "natural-compare": "^1.4.0",
+                "semver": "^7.6.0",
+                "ts-api-utils": "^1.3.0"
+            },
+            "engines": {
+                "node": "^18.18.0 || >=20.0.0"
+            },
+            "funding": {
+                "type": "opencollective",
+                "url": "https://opencollective.com/typescript-eslint"
+            },
+            "peerDependencies": {
+                "@typescript-eslint/parser": "^7.0.0",
+                "eslint": "^8.56.0"
+            },
+            "peerDependenciesMeta": {
+                "typescript": {
+                    "optional": true
+                }
+            }
+        },
+        "node_modules/@typescript-eslint/eslint-plugin/node_modules/lru-cache": {
+            "version": "6.0.0",
+            "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
+            "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
+            "dependencies": {
+                "yallist": "^4.0.0"
+            },
+            "engines": {
+                "node": ">=10"
+            }
+        },
+        "node_modules/@typescript-eslint/eslint-plugin/node_modules/semver": {
+            "version": "7.6.0",
+            "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz",
+            "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==",
+            "dependencies": {
+                "lru-cache": "^6.0.0"
+            },
+            "bin": {
+                "semver": "bin/semver.js"
+            },
+            "engines": {
+                "node": ">=10"
+            }
+        },
+        "node_modules/@typescript-eslint/eslint-plugin/node_modules/yallist": {
+            "version": "4.0.0",
+            "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+            "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
+        },
+        "node_modules/@typescript-eslint/parser": {
+            "version": "7.6.0",
+            "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.6.0.tgz",
+            "integrity": "sha512-usPMPHcwX3ZoPWnBnhhorc14NJw9J4HpSXQX4urF2TPKG0au0XhJoZyX62fmvdHONUkmyUe74Hzm1//XA+BoYg==",
+            "dependencies": {
+                "@typescript-eslint/scope-manager": "7.6.0",
+                "@typescript-eslint/types": "7.6.0",
+                "@typescript-eslint/typescript-estree": "7.6.0",
+                "@typescript-eslint/visitor-keys": "7.6.0",
+                "debug": "^4.3.4"
+            },
+            "engines": {
+                "node": "^18.18.0 || >=20.0.0"
+            },
+            "funding": {
+                "type": "opencollective",
+                "url": "https://opencollective.com/typescript-eslint"
+            },
+            "peerDependencies": {
+                "eslint": "^8.56.0"
+            },
+            "peerDependenciesMeta": {
+                "typescript": {
+                    "optional": true
+                }
+            }
+        },
+        "node_modules/@typescript-eslint/scope-manager": {
+            "version": "7.6.0",
+            "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.6.0.tgz",
+            "integrity": "sha512-ngttyfExA5PsHSx0rdFgnADMYQi+Zkeiv4/ZxGYUWd0nLs63Ha0ksmp8VMxAIC0wtCFxMos7Lt3PszJssG/E6w==",
+            "dependencies": {
+                "@typescript-eslint/types": "7.6.0",
+                "@typescript-eslint/visitor-keys": "7.6.0"
+            },
+            "engines": {
+                "node": "^18.18.0 || >=20.0.0"
+            },
+            "funding": {
+                "type": "opencollective",
+                "url": "https://opencollective.com/typescript-eslint"
+            }
+        },
+        "node_modules/@typescript-eslint/type-utils": {
+            "version": "7.6.0",
+            "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.6.0.tgz",
+            "integrity": "sha512-NxAfqAPNLG6LTmy7uZgpK8KcuiS2NZD/HlThPXQRGwz6u7MDBWRVliEEl1Gj6U7++kVJTpehkhZzCJLMK66Scw==",
+            "dependencies": {
+                "@typescript-eslint/typescript-estree": "7.6.0",
+                "@typescript-eslint/utils": "7.6.0",
+                "debug": "^4.3.4",
+                "ts-api-utils": "^1.3.0"
+            },
+            "engines": {
+                "node": "^18.18.0 || >=20.0.0"
+            },
+            "funding": {
+                "type": "opencollective",
+                "url": "https://opencollective.com/typescript-eslint"
+            },
+            "peerDependencies": {
+                "eslint": "^8.56.0"
+            },
+            "peerDependenciesMeta": {
+                "typescript": {
+                    "optional": true
+                }
+            }
+        },
+        "node_modules/@typescript-eslint/types": {
+            "version": "7.6.0",
+            "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.6.0.tgz",
+            "integrity": "sha512-h02rYQn8J+MureCvHVVzhl69/GAfQGPQZmOMjG1KfCl7o3HtMSlPaPUAPu6lLctXI5ySRGIYk94clD/AUMCUgQ==",
+            "engines": {
+                "node": "^18.18.0 || >=20.0.0"
+            },
+            "funding": {
+                "type": "opencollective",
+                "url": "https://opencollective.com/typescript-eslint"
+            }
+        },
+        "node_modules/@typescript-eslint/typescript-estree": {
+            "version": "7.6.0",
+            "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.6.0.tgz",
+            "integrity": "sha512-+7Y/GP9VuYibecrCQWSKgl3GvUM5cILRttpWtnAu8GNL9j11e4tbuGZmZjJ8ejnKYyBRb2ddGQ3rEFCq3QjMJw==",
+            "dependencies": {
+                "@typescript-eslint/types": "7.6.0",
+                "@typescript-eslint/visitor-keys": "7.6.0",
+                "debug": "^4.3.4",
+                "globby": "^11.1.0",
+                "is-glob": "^4.0.3",
+                "minimatch": "^9.0.4",
+                "semver": "^7.6.0",
+                "ts-api-utils": "^1.3.0"
+            },
+            "engines": {
+                "node": "^18.18.0 || >=20.0.0"
+            },
+            "funding": {
+                "type": "opencollective",
+                "url": "https://opencollective.com/typescript-eslint"
+            },
+            "peerDependenciesMeta": {
+                "typescript": {
+                    "optional": true
+                }
+            }
+        },
+        "node_modules/@typescript-eslint/typescript-estree/node_modules/lru-cache": {
+            "version": "6.0.0",
+            "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
+            "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
+            "dependencies": {
+                "yallist": "^4.0.0"
+            },
+            "engines": {
+                "node": ">=10"
+            }
+        },
+        "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": {
+            "version": "9.0.4",
+            "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz",
+            "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==",
+            "dependencies": {
+                "brace-expansion": "^2.0.1"
+            },
+            "engines": {
+                "node": ">=16 || 14 >=14.17"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/isaacs"
+            }
+        },
+        "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": {
+            "version": "7.6.0",
+            "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz",
+            "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==",
+            "dependencies": {
+                "lru-cache": "^6.0.0"
+            },
+            "bin": {
+                "semver": "bin/semver.js"
+            },
+            "engines": {
+                "node": ">=10"
+            }
+        },
+        "node_modules/@typescript-eslint/typescript-estree/node_modules/yallist": {
+            "version": "4.0.0",
+            "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+            "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
+        },
+        "node_modules/@typescript-eslint/utils": {
+            "version": "7.6.0",
+            "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.6.0.tgz",
+            "integrity": "sha512-x54gaSsRRI+Nwz59TXpCsr6harB98qjXYzsRxGqvA5Ue3kQH+FxS7FYU81g/omn22ML2pZJkisy6Q+ElK8pBCA==",
+            "dependencies": {
+                "@eslint-community/eslint-utils": "^4.4.0",
+                "@types/json-schema": "^7.0.15",
+                "@types/semver": "^7.5.8",
+                "@typescript-eslint/scope-manager": "7.6.0",
+                "@typescript-eslint/types": "7.6.0",
+                "@typescript-eslint/typescript-estree": "7.6.0",
+                "semver": "^7.6.0"
+            },
+            "engines": {
+                "node": "^18.18.0 || >=20.0.0"
+            },
+            "funding": {
+                "type": "opencollective",
+                "url": "https://opencollective.com/typescript-eslint"
+            },
+            "peerDependencies": {
+                "eslint": "^8.56.0"
+            }
+        },
+        "node_modules/@typescript-eslint/utils/node_modules/lru-cache": {
+            "version": "6.0.0",
+            "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
+            "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
+            "dependencies": {
+                "yallist": "^4.0.0"
+            },
+            "engines": {
+                "node": ">=10"
+            }
+        },
+        "node_modules/@typescript-eslint/utils/node_modules/semver": {
+            "version": "7.6.0",
+            "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz",
+            "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==",
+            "dependencies": {
+                "lru-cache": "^6.0.0"
+            },
+            "bin": {
+                "semver": "bin/semver.js"
+            },
+            "engines": {
+                "node": ">=10"
+            }
+        },
+        "node_modules/@typescript-eslint/utils/node_modules/yallist": {
+            "version": "4.0.0",
+            "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+            "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
+        },
+        "node_modules/@typescript-eslint/visitor-keys": {
+            "version": "7.6.0",
+            "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.6.0.tgz",
+            "integrity": "sha512-4eLB7t+LlNUmXzfOu1VAIAdkjbu5xNSerURS9X/S5TUKWFRpXRQZbmtPqgKmYx8bj3J0irtQXSiWAOY82v+cgw==",
+            "dependencies": {
+                "@typescript-eslint/types": "7.6.0",
+                "eslint-visitor-keys": "^3.4.3"
+            },
+            "engines": {
+                "node": "^18.18.0 || >=20.0.0"
+            },
+            "funding": {
+                "type": "opencollective",
+                "url": "https://opencollective.com/typescript-eslint"
+            }
+        },
         "node_modules/@ungap/structured-clone": {
             "version": "1.2.0",
             "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz",
@@ -9993,7 +10401,6 @@
             "version": "0.0.8",
             "resolved": "https://registry.npmjs.org/ansi-html-community/-/ansi-html-community-0.0.8.tgz",
             "integrity": "sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==",
-            "dev": true,
             "engines": [
                 "node >= 0.8.0"
             ],
@@ -14367,6 +14774,20 @@
                 "node": ">=6.0"
             }
         },
+        "node_modules/ci-info": {
+            "version": "3.9.0",
+            "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz",
+            "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==",
+            "funding": [
+                {
+                    "type": "github",
+                    "url": "https://github.com/sponsors/sibiraj-s"
+                }
+            ],
+            "engines": {
+                "node": ">=8"
+            }
+        },
         "node_modules/class-transformer": {
             "version": "0.5.1",
             "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz",
@@ -15906,8 +16327,7 @@
         "node_modules/deep-is": {
             "version": "0.1.4",
             "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
-            "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
-            "dev": true
+            "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="
         },
         "node_modules/deepmerge": {
             "version": "4.3.1",
@@ -16188,6 +16608,17 @@
                 "node": ">=0.3.1"
             }
         },
+        "node_modules/dir-glob": {
+            "version": "3.0.1",
+            "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
+            "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==",
+            "dependencies": {
+                "path-type": "^4.0.0"
+            },
+            "engines": {
+                "node": ">=8"
+            }
+        },
         "node_modules/dns-packet": {
             "version": "5.6.1",
             "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz",
@@ -16204,7 +16635,6 @@
             "version": "3.0.0",
             "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
             "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==",
-            "dev": true,
             "dependencies": {
                 "esutils": "^2.0.2"
             },
@@ -16699,7 +17129,6 @@
             "version": "8.57.0",
             "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz",
             "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==",
-            "dev": true,
             "dependencies": {
                 "@eslint-community/eslint-utils": "^4.2.0",
                 "@eslint-community/regexpp": "^4.6.1",
@@ -17678,6 +18107,31 @@
                 "ms": "^2.1.1"
             }
         },
+        "node_modules/eslint-import-resolver-typescript": {
+            "version": "3.6.1",
+            "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.6.1.tgz",
+            "integrity": "sha512-xgdptdoi5W3niYeuQxKmzVDTATvLYqhpwmykwsh7f6HIOStGWEIL9iqZgQDF9u9OEzrRwR8no5q2VT+bjAujTg==",
+            "dev": true,
+            "dependencies": {
+                "debug": "^4.3.4",
+                "enhanced-resolve": "^5.12.0",
+                "eslint-module-utils": "^2.7.4",
+                "fast-glob": "^3.3.1",
+                "get-tsconfig": "^4.5.0",
+                "is-core-module": "^2.11.0",
+                "is-glob": "^4.0.3"
+            },
+            "engines": {
+                "node": "^14.18.0 || >=16.0.0"
+            },
+            "funding": {
+                "url": "https://opencollective.com/unts/projects/eslint-import-resolver-ts"
+            },
+            "peerDependencies": {
+                "eslint": "*",
+                "eslint-plugin-import": "*"
+            }
+        },
         "node_modules/eslint-module-utils": {
             "version": "2.8.1",
             "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.8.1.tgz",
@@ -18044,7 +18498,6 @@
             "version": "7.2.2",
             "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz",
             "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==",
-            "dev": true,
             "dependencies": {
                 "esrecurse": "^4.3.0",
                 "estraverse": "^5.2.0"
@@ -18084,7 +18537,6 @@
             "version": "3.4.3",
             "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
             "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
-            "dev": true,
             "engines": {
                 "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
             },
@@ -18092,11 +18544,69 @@
                 "url": "https://opencollective.com/eslint"
             }
         },
+        "node_modules/eslint-webpack-plugin": {
+            "version": "4.1.0",
+            "resolved": "https://registry.npmjs.org/eslint-webpack-plugin/-/eslint-webpack-plugin-4.1.0.tgz",
+            "integrity": "sha512-C3wAG2jyockIhN0YRLuKieKj2nx/gnE/VHmoHemD5ifnAtY6ZU+jNPfzPoX4Zd6RIbUyWTiZUh/ofUlBhoAX7w==",
+            "dependencies": {
+                "@types/eslint": "^8.56.5",
+                "jest-worker": "^29.7.0",
+                "micromatch": "^4.0.5",
+                "normalize-path": "^3.0.0",
+                "schema-utils": "^4.2.0"
+            },
+            "engines": {
+                "node": ">= 14.15.0"
+            },
+            "funding": {
+                "type": "opencollective",
+                "url": "https://opencollective.com/webpack"
+            },
+            "peerDependencies": {
+                "eslint": "^8.0.0",
+                "webpack": "^5.0.0"
+            }
+        },
+        "node_modules/eslint-webpack-plugin/node_modules/has-flag": {
+            "version": "4.0.0",
+            "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+            "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+            "engines": {
+                "node": ">=8"
+            }
+        },
+        "node_modules/eslint-webpack-plugin/node_modules/jest-worker": {
+            "version": "29.7.0",
+            "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz",
+            "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==",
+            "dependencies": {
+                "@types/node": "*",
+                "jest-util": "^29.7.0",
+                "merge-stream": "^2.0.0",
+                "supports-color": "^8.0.0"
+            },
+            "engines": {
+                "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+            }
+        },
+        "node_modules/eslint-webpack-plugin/node_modules/supports-color": {
+            "version": "8.1.1",
+            "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
+            "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
+            "dependencies": {
+                "has-flag": "^4.0.0"
+            },
+            "engines": {
+                "node": ">=10"
+            },
+            "funding": {
+                "url": "https://github.com/chalk/supports-color?sponsor=1"
+            }
+        },
         "node_modules/eslint/node_modules/ansi-styles": {
             "version": "4.3.0",
             "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
             "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
-            "dev": true,
             "dependencies": {
                 "color-convert": "^2.0.1"
             },
@@ -18110,14 +18620,12 @@
         "node_modules/eslint/node_modules/argparse": {
             "version": "2.0.1",
             "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
-            "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
-            "dev": true
+            "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
         },
         "node_modules/eslint/node_modules/brace-expansion": {
             "version": "1.1.11",
             "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
             "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
-            "dev": true,
             "dependencies": {
                 "balanced-match": "^1.0.0",
                 "concat-map": "0.0.1"
@@ -18127,7 +18635,6 @@
             "version": "4.1.2",
             "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
             "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
-            "dev": true,
             "dependencies": {
                 "ansi-styles": "^4.1.0",
                 "supports-color": "^7.1.0"
@@ -18143,7 +18650,6 @@
             "version": "2.0.1",
             "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
             "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
-            "dev": true,
             "dependencies": {
                 "color-name": "~1.1.4"
             },
@@ -18154,14 +18660,12 @@
         "node_modules/eslint/node_modules/color-name": {
             "version": "1.1.4",
             "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
-            "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
-            "dev": true
+            "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
         },
         "node_modules/eslint/node_modules/globals": {
             "version": "13.24.0",
             "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz",
             "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==",
-            "dev": true,
             "dependencies": {
                 "type-fest": "^0.20.2"
             },
@@ -18176,7 +18680,6 @@
             "version": "4.0.0",
             "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
             "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
-            "dev": true,
             "engines": {
                 "node": ">=8"
             }
@@ -18185,7 +18688,6 @@
             "version": "4.1.0",
             "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
             "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
-            "dev": true,
             "dependencies": {
                 "argparse": "^2.0.1"
             },
@@ -18197,7 +18699,6 @@
             "version": "3.1.2",
             "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
             "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
-            "dev": true,
             "dependencies": {
                 "brace-expansion": "^1.1.7"
             },
@@ -18209,7 +18710,6 @@
             "version": "7.2.0",
             "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
             "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
-            "dev": true,
             "dependencies": {
                 "has-flag": "^4.0.0"
             },
@@ -18221,7 +18721,6 @@
             "version": "9.6.1",
             "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz",
             "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==",
-            "dev": true,
             "dependencies": {
                 "acorn": "^8.9.0",
                 "acorn-jsx": "^5.3.2",
@@ -18250,7 +18749,6 @@
             "version": "1.5.0",
             "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz",
             "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==",
-            "dev": true,
             "dependencies": {
                 "estraverse": "^5.1.0"
             },
@@ -18599,6 +19097,32 @@
             "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz",
             "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ=="
         },
+        "node_modules/fast-glob": {
+            "version": "3.3.2",
+            "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz",
+            "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==",
+            "dependencies": {
+                "@nodelib/fs.stat": "^2.0.2",
+                "@nodelib/fs.walk": "^1.2.3",
+                "glob-parent": "^5.1.2",
+                "merge2": "^1.3.0",
+                "micromatch": "^4.0.4"
+            },
+            "engines": {
+                "node": ">=8.6.0"
+            }
+        },
+        "node_modules/fast-glob/node_modules/glob-parent": {
+            "version": "5.1.2",
+            "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+            "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+            "dependencies": {
+                "is-glob": "^4.0.1"
+            },
+            "engines": {
+                "node": ">= 6"
+            }
+        },
         "node_modules/fast-json-stable-stringify": {
             "version": "2.1.0",
             "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
@@ -18607,8 +19131,7 @@
         "node_modules/fast-levenshtein": {
             "version": "2.0.6",
             "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
-            "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
-            "dev": true
+            "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="
         },
         "node_modules/fastest-levenshtein": {
             "version": "1.0.16",
@@ -18622,7 +19145,6 @@
             "version": "1.17.1",
             "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz",
             "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==",
-            "dev": true,
             "dependencies": {
                 "reusify": "^1.0.4"
             }
@@ -18693,7 +19215,6 @@
             "version": "6.0.1",
             "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
             "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==",
-            "dev": true,
             "dependencies": {
                 "flat-cache": "^3.0.4"
             },
@@ -18848,7 +19369,6 @@
             "version": "5.0.0",
             "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
             "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
-            "dev": true,
             "dependencies": {
                 "locate-path": "^6.0.0",
                 "path-exists": "^4.0.0"
@@ -18891,7 +19411,6 @@
             "version": "3.2.0",
             "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz",
             "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==",
-            "dev": true,
             "dependencies": {
                 "flatted": "^3.2.9",
                 "keyv": "^4.5.3",
@@ -18905,7 +19424,6 @@
             "version": "1.1.11",
             "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
             "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
-            "dev": true,
             "dependencies": {
                 "balanced-match": "^1.0.0",
                 "concat-map": "0.0.1"
@@ -18915,7 +19433,6 @@
             "version": "7.2.3",
             "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
             "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
-            "dev": true,
             "dependencies": {
                 "fs.realpath": "^1.0.0",
                 "inflight": "^1.0.4",
@@ -18935,7 +19452,6 @@
             "version": "3.1.2",
             "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
             "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
-            "dev": true,
             "dependencies": {
                 "brace-expansion": "^1.1.7"
             },
@@ -18947,7 +19463,6 @@
             "version": "3.0.2",
             "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
             "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
-            "dev": true,
             "dependencies": {
                 "glob": "^7.1.3"
             },
@@ -18961,8 +19476,7 @@
         "node_modules/flatted": {
             "version": "3.3.1",
             "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz",
-            "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==",
-            "dev": true
+            "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw=="
         },
         "node_modules/flexlayout-react": {
             "version": "0.7.15",
@@ -19693,6 +20207,18 @@
                 "url": "https://github.com/sponsors/ljharb"
             }
         },
+        "node_modules/get-tsconfig": {
+            "version": "4.7.3",
+            "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.7.3.tgz",
+            "integrity": "sha512-ZvkrzoUA0PQZM6fy6+/Hce561s+faD1rsNwhnO5FelNjyy7EMGJ3Rz1AQ8GYDWjhRs/7dBLOEJvhK8MiEJOAFg==",
+            "dev": true,
+            "dependencies": {
+                "resolve-pkg-maps": "^1.0.0"
+            },
+            "funding": {
+                "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
+            }
+        },
         "node_modules/get-value": {
             "version": "2.0.6",
             "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz",
@@ -19748,7 +20274,6 @@
             "version": "6.0.2",
             "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
             "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
-            "dev": true,
             "dependencies": {
                 "is-glob": "^4.0.3"
             },
@@ -19846,6 +20371,25 @@
                 "url": "https://github.com/sponsors/ljharb"
             }
         },
+        "node_modules/globby": {
+            "version": "11.1.0",
+            "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz",
+            "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==",
+            "dependencies": {
+                "array-union": "^2.1.0",
+                "dir-glob": "^3.0.1",
+                "fast-glob": "^3.2.9",
+                "ignore": "^5.2.0",
+                "merge2": "^1.4.1",
+                "slash": "^3.0.0"
+            },
+            "engines": {
+                "node": ">=10"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/sindresorhus"
+            }
+        },
         "node_modules/golden-layout": {
             "version": "2.6.0",
             "resolved": "https://registry.npmjs.org/golden-layout/-/golden-layout-2.6.0.tgz",
@@ -19963,8 +20507,7 @@
         "node_modules/graphemer": {
             "version": "1.4.0",
             "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
-            "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
-            "dev": true
+            "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag=="
         },
         "node_modules/grid-index": {
             "version": "1.1.0",
@@ -20326,7 +20869,6 @@
             "version": "2.5.2",
             "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.5.2.tgz",
             "integrity": "sha512-K//PSRMQk4FZ78Kyau+mZurHn3FH0Vwr+H36eE0rPbeYkRRi9YxceYPhuN60UwWorxyKHhqoAJl2OFKa4BVtaA==",
-            "dev": true,
             "funding": [
                 {
                     "type": "github",
@@ -20973,7 +21515,6 @@
             "version": "0.1.4",
             "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
             "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
-            "dev": true,
             "engines": {
                 "node": ">=0.8.19"
             }
@@ -21700,7 +22241,6 @@
             "version": "3.0.3",
             "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz",
             "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==",
-            "dev": true,
             "engines": {
                 "node": ">=8"
             }
@@ -21987,6 +22527,86 @@
                 "@pkgjs/parseargs": "^0.11.0"
             }
         },
+        "node_modules/jest-util": {
+            "version": "29.7.0",
+            "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz",
+            "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==",
+            "dependencies": {
+                "@jest/types": "^29.6.3",
+                "@types/node": "*",
+                "chalk": "^4.0.0",
+                "ci-info": "^3.2.0",
+                "graceful-fs": "^4.2.9",
+                "picomatch": "^2.2.3"
+            },
+            "engines": {
+                "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+            }
+        },
+        "node_modules/jest-util/node_modules/ansi-styles": {
+            "version": "4.3.0",
+            "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+            "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+            "dependencies": {
+                "color-convert": "^2.0.1"
+            },
+            "engines": {
+                "node": ">=8"
+            },
+            "funding": {
+                "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+            }
+        },
+        "node_modules/jest-util/node_modules/chalk": {
+            "version": "4.1.2",
+            "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+            "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+            "dependencies": {
+                "ansi-styles": "^4.1.0",
+                "supports-color": "^7.1.0"
+            },
+            "engines": {
+                "node": ">=10"
+            },
+            "funding": {
+                "url": "https://github.com/chalk/chalk?sponsor=1"
+            }
+        },
+        "node_modules/jest-util/node_modules/color-convert": {
+            "version": "2.0.1",
+            "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+            "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+            "dependencies": {
+                "color-name": "~1.1.4"
+            },
+            "engines": {
+                "node": ">=7.0.0"
+            }
+        },
+        "node_modules/jest-util/node_modules/color-name": {
+            "version": "1.1.4",
+            "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+            "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
+        },
+        "node_modules/jest-util/node_modules/has-flag": {
+            "version": "4.0.0",
+            "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+            "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+            "engines": {
+                "node": ">=8"
+            }
+        },
+        "node_modules/jest-util/node_modules/supports-color": {
+            "version": "7.2.0",
+            "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+            "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+            "dependencies": {
+                "has-flag": "^4.0.0"
+            },
+            "engines": {
+                "node": ">=8"
+            }
+        },
         "node_modules/jest-worker": {
             "version": "27.5.1",
             "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz",
@@ -22243,8 +22863,7 @@
         "node_modules/json-stable-stringify-without-jsonify": {
             "version": "1.0.1",
             "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
-            "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
-            "dev": true
+            "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="
         },
         "node_modules/json-stringify-pretty-compact": {
             "version": "3.0.0",
@@ -22588,7 +23207,6 @@
             "version": "0.4.1",
             "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
             "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
-            "dev": true,
             "dependencies": {
                 "prelude-ls": "^1.2.1",
                 "type-check": "~0.4.0"
@@ -22650,7 +23268,6 @@
             "version": "6.0.0",
             "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
             "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
-            "dev": true,
             "dependencies": {
                 "p-locate": "^5.0.0"
             },
@@ -23341,6 +23958,14 @@
             "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
             "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="
         },
+        "node_modules/merge2": {
+            "version": "1.4.1",
+            "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
+            "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
+            "engines": {
+                "node": ">= 8"
+            }
+        },
         "node_modules/methods": {
             "version": "1.1.2",
             "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
@@ -24484,8 +25109,7 @@
         "node_modules/natural-compare": {
             "version": "1.4.0",
             "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
-            "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
-            "dev": true
+            "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="
         },
         "node_modules/needle": {
             "version": "2.9.1",
@@ -27705,7 +28329,6 @@
             "version": "0.9.3",
             "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz",
             "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==",
-            "dev": true,
             "dependencies": {
                 "@aashutoshrathi/word-wrap": "^1.2.3",
                 "deep-is": "^0.1.3",
@@ -27758,7 +28381,6 @@
             "version": "5.0.0",
             "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
             "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
-            "dev": true,
             "dependencies": {
                 "p-limit": "^3.0.2"
             },
@@ -27773,7 +28395,6 @@
             "version": "3.1.0",
             "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
             "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
-            "dev": true,
             "dependencies": {
                 "yocto-queue": "^0.1.0"
             },
@@ -27788,7 +28409,6 @@
             "version": "0.1.0",
             "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
             "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
-            "dev": true,
             "engines": {
                 "node": ">=10"
             },
@@ -28519,7 +29139,6 @@
             "version": "1.2.1",
             "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
             "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
-            "dev": true,
             "engines": {
                 "node": ">= 0.8.0"
             }
@@ -28941,7 +29560,6 @@
             "version": "1.2.3",
             "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
             "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
-            "dev": true,
             "funding": [
                 {
                     "type": "github",
@@ -30155,6 +30773,15 @@
                 "node": ">=8"
             }
         },
+        "node_modules/resolve-pkg-maps": {
+            "version": "1.0.0",
+            "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
+            "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
+            "dev": true,
+            "funding": {
+                "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
+            }
+        },
         "node_modules/resolve-protobuf-schema": {
             "version": "2.1.0",
             "resolved": "https://registry.npmjs.org/resolve-protobuf-schema/-/resolve-protobuf-schema-2.1.0.tgz",
@@ -30209,7 +30836,6 @@
             "version": "1.0.4",
             "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
             "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==",
-            "dev": true,
             "engines": {
                 "iojs": ">=1.0.0",
                 "node": ">=0.10.0"
@@ -30281,7 +30907,6 @@
             "version": "1.2.0",
             "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
             "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
-            "dev": true,
             "funding": [
                 {
                     "type": "github",
@@ -31026,6 +31651,14 @@
             "resolved": "https://registry.npmjs.org/skmeans/-/skmeans-0.9.7.tgz",
             "integrity": "sha512-hNj1/oZ7ygsfmPZ7ZfN5MUBRoGg1gtpnImuJBgLO0ljQ67DtJuiQaiYdS4lUA6s0KCwnPhGivtC/WRwIZLkHyg=="
         },
+        "node_modules/slash": {
+            "version": "3.0.0",
+            "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
+            "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
+            "engines": {
+                "node": ">=8"
+            }
+        },
         "node_modules/slice-ansi": {
             "version": "2.1.0",
             "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz",
@@ -31688,7 +32321,6 @@
             "version": "3.1.1",
             "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
             "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
-            "dev": true,
             "engines": {
                 "node": ">=8"
             },
@@ -32090,8 +32722,7 @@
         "node_modules/text-table": {
             "version": "0.2.0",
             "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
-            "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
-            "dev": true
+            "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw=="
         },
         "node_modules/textarea-caret": {
             "version": "3.1.0",
@@ -32333,6 +32964,17 @@
             "resolved": "https://registry.npmjs.org/tryit/-/tryit-1.0.3.tgz",
             "integrity": "sha512-6C5h3CE+0qjGp+YKYTs74xR0k/Nw/ePtl/Lp6CCf44hqBQ66qnH1sDFR5mV/Gc48EsrHLB53lCFSffQCkka3kg=="
         },
+        "node_modules/ts-api-utils": {
+            "version": "1.3.0",
+            "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz",
+            "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==",
+            "engines": {
+                "node": ">=16"
+            },
+            "peerDependencies": {
+                "typescript": ">=4.2.0"
+            }
+        },
         "node_modules/ts-loader": {
             "version": "9.5.1",
             "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.5.1.tgz",
@@ -32934,7 +33576,6 @@
             "version": "0.4.0",
             "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
             "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
-            "dev": true,
             "dependencies": {
                 "prelude-ls": "^1.2.1"
             },
@@ -32946,7 +33587,6 @@
             "version": "0.20.2",
             "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
             "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
-            "dev": true,
             "engines": {
                 "node": ">=10"
             },
@@ -33056,6 +33696,31 @@
             "resolved": "https://registry.npmjs.org/typescript-collections/-/typescript-collections-1.3.3.tgz",
             "integrity": "sha512-7sI4e/bZijOzyURng88oOFZCISQPTHozfE2sUu5AviFYk5QV7fYGb6YiDl+vKjF/pICA354JImBImL9XJWUvdQ=="
         },
+        "node_modules/typescript-eslint": {
+            "version": "7.6.0",
+            "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-7.6.0.tgz",
+            "integrity": "sha512-LY6vH6F1l5jpGqRtU+uK4+mOecIb4Cd4kaz1hAiJrgnNiHUA8wiw8BkJyYS+MRLM69F1QuSKwtGlQqnGl1Rc6w==",
+            "dependencies": {
+                "@typescript-eslint/eslint-plugin": "7.6.0",
+                "@typescript-eslint/parser": "7.6.0",
+                "@typescript-eslint/utils": "7.6.0"
+            },
+            "engines": {
+                "node": "^18.18.0 || >=20.0.0"
+            },
+            "funding": {
+                "type": "opencollective",
+                "url": "https://opencollective.com/typescript-eslint"
+            },
+            "peerDependencies": {
+                "eslint": "^8.56.0"
+            },
+            "peerDependenciesMeta": {
+                "typescript": {
+                    "optional": true
+                }
+            }
+        },
         "node_modules/typescript-language-server": {
             "version": "4.3.3",
             "resolved": "https://registry.npmjs.org/typescript-language-server/-/typescript-language-server-4.3.3.tgz",
@@ -33952,7 +34617,6 @@
             "version": "2.26.1",
             "resolved": "https://registry.npmjs.org/webpack-hot-middleware/-/webpack-hot-middleware-2.26.1.tgz",
             "integrity": "sha512-khZGfAeJx6I8K9zKohEWWYN6KDlVw2DHownoe+6Vtwj1LP9WFgegXnVMSkZ/dBEBtXFwrkkydsaPFlB7f8wU2A==",
-            "dev": true,
             "dependencies": {
                 "ansi-html-community": "0.0.8",
                 "html-entities": "^2.1.0",
diff --git a/package.json b/package.json
index 13d97b45c..3f2f5a70f 100644
--- a/package.json
+++ b/package.json
@@ -3,6 +3,9 @@
     "version": "1.0.0",
     "description": "",
     "main": "index.js",
+    "engines": {
+        "node": ">=12.0.0"
+    },
     "browser": {
         "child_process": false
     },
@@ -62,7 +65,6 @@
         "@types/uuid": "^9.0.7",
         "@types/valid-url": "^1.0.7",
         "@types/webpack": "^5.28.5",
-        "@types/webpack-hot-middleware": "^2.25.9",
         "@types/youtube": "0.0.50",
         "chai": "^5.0.0",
         "cross-env": "^7.0.3",
@@ -71,7 +73,8 @@
         "eslint-config-airbnb": "^19.0.4",
         "eslint-config-node": "^4.1.0",
         "eslint-config-prettier": "^9.1.0",
-        "eslint-plugin-import": "^2.29.0",
+        "eslint-import-resolver-typescript": "^3.6.1",
+        "eslint-plugin-import": "^2.29.1",
         "eslint-plugin-jsx-a11y": "^6.8.0",
         "eslint-plugin-node": "^11.1.0",
         "eslint-plugin-prettier": "^5.0.1",
@@ -86,9 +89,7 @@
         "ts-loader": "^9.5.1",
         "ts-node": "^10.9.1",
         "ts-node-dev": "^2.0.0",
-        "typescript": "^5.3.3",
-        "webpack-dev-server": "^5.0.4",
-        "webpack-hot-middleware": "^2.25.4"
+        "webpack-dev-server": "^5.0.4"
     },
     "dependencies": {
         "@adobe/react-spectrum": "^3.32.2",
@@ -131,6 +132,8 @@
         "@types/reveal": "^4.2.0",
         "@types/supercluster": "^7.1.3",
         "@types/web": "^0.0.143",
+        "@types/webpack-hot-middleware": "^2.25.9",
+        "@typescript-eslint/parser": "^7.6.0",
         "@webscopeio/react-textarea-autocomplete": "^4.9.2",
         "adm-zip": "^0.5.10",
         "archiver": "^7.0.1",
@@ -167,6 +170,7 @@
         "D": "^1.0.0",
         "d3": "^7.8.5",
         "depcheck": "^1.4.7",
+        "eslint-webpack-plugin": "^4.1.0",
         "exif": "^0.6.0",
         "exifr": "^7.1.3",
         "express": "^4.18.2",
@@ -286,7 +290,9 @@
         "tough-cookie": "^4.1.3",
         "tslint": "^6.1.3",
         "tslint-loader": "^3.5.4",
+        "typescript": "^5.3.3",
         "typescript-collections": "^1.3.3",
+        "typescript-eslint": "^7.6.0",
         "typescript-language-server": "^4.1.3",
         "uninstall": "^0.0.0",
         "url": "^0.11.3",
@@ -298,6 +304,7 @@
         "webpack": "^5.89.0",
         "webpack-cli": "^5.1.4",
         "webpack-dev-middleware": "^7.0.0",
+        "webpack-hot-middleware": "^2.25.4",
         "wikijs": "^6.4.1",
         "words-to-numbers": "^1.5.1",
         "xoauth2": "^1.2.0",
diff --git a/src/ServerUtils.ts b/src/ServerUtils.ts
new file mode 100644
index 000000000..ade4ca35d
--- /dev/null
+++ b/src/ServerUtils.ts
@@ -0,0 +1,27 @@
+import { Socket } from 'socket.io';
+import { Message } from './server/Message';
+import { Utils } from './Utils';
+
+export namespace ServerUtils {
+    export function Emit<T>(socket: Socket, message: Message<T>, args: T) {
+        Utils.log('Emit', message.Name, args, false);
+        socket.emit(message.Message, args);
+    }
+
+    export function AddServerHandler<T>(socket: Socket, message: Message<T>, handler: (args: T) => any) {
+        socket.on(message.Message, Utils.loggingCallback('Incoming', handler, message.Name));
+    }
+
+    export function AddServerHandlerCallback<T>(socket: Socket, message: Message<T>, handler: (args: [T, (res: any) => any]) => any) {
+        socket.on(message.Message, (arg: T, fn: (res: any) => any) => {
+            Utils.log('S receiving', message.Name, arg, true);
+            handler([arg, Utils.loggingCallback('S sending', fn, message.Name)]);
+        });
+    }
+    export type RoomHandler = (socket: Socket, room: string) => any;
+    export type UsedSockets = Socket;
+    export type RoomMessage = 'create or join' | 'created' | 'joined';
+    export function AddRoomHandler(socket: Socket, message: RoomMessage, handler: RoomHandler) {
+        socket.on(message, (room: any) => handler(socket, room));
+    }
+}
diff --git a/src/Utils.ts b/src/Utils.ts
index 291d7c799..0455fd19a 100644
--- a/src/Utils.ts
+++ b/src/Utils.ts
@@ -1,52 +1,12 @@
-import * as Color from 'color';
-import { ColorResult } from 'react-color';
-import * as rp from 'request-promise';
-import { Socket } from 'socket.io';
 import * as uuid from 'uuid';
-import { DocumentType } from './client/documents/DocumentTypes';
-import { Colors } from './client/views/global/globalEnums';
-import { DocumentView } from './client/views/nodes/DocumentView';
-import { Message } from './server/Message';
 
+export function clamp(n: number, lower: number, upper: number) {
+    return Math.max(lower, Math.min(upper, n));
+}
 export namespace Utils {
-    export let CLICK_TIME = 300;
-    export let DRAG_THRESHOLD = 4;
-    export let SNAP_THRESHOLD = 10;
-    export function isClick(x: number, y: number, downX: number, downY: number, downTime: number) {
-        return Date.now() - downTime < Utils.CLICK_TIME && Math.abs(x - downX) < Utils.DRAG_THRESHOLD && Math.abs(y - downY) < Utils.DRAG_THRESHOLD;
-    }
-
-    export function cleanDocumentType(type: DocumentType) {
-        switch (type) {
-            case DocumentType.IMG:
-                return 'Image';
-            case DocumentType.AUDIO:
-                return 'Audio';
-            case DocumentType.COL:
-                return 'Collection';
-            case DocumentType.RTF:
-                return 'Text';
-            default:
-                return type.charAt(0).toUpperCase() + type.slice(1);
-        }
-    }
-
-    export function readUploadedFileAsText(inputFile: File) {
-        const temporaryFileReader = new FileReader();
-
-        return new Promise((resolve, reject) => {
-            temporaryFileReader.onerror = () => {
-                temporaryFileReader.abort();
-                reject(new DOMException('Problem parsing input file.'));
-            };
-
-            temporaryFileReader.onload = () => {
-                resolve(temporaryFileReader.result);
-            };
-            temporaryFileReader.readAsText(inputFile);
-        });
+    export function GuestID() {
+        return '__guest__';
     }
-
     export function GenerateGuid(): string {
         return uuid.v4();
     }
@@ -55,49 +15,33 @@ export namespace Utils {
         return uuid.v5(seed, uuid.v5.URL);
     }
 
-    export function GuestID() {
-        return '__guest__';
-    }
+    export const loggingEnabled: Boolean = false;
+    export const logFilter: number | undefined = undefined;
 
-    /**
-     * Uploads an image buffer to the server and stores with specified filename.  by default the image
-     * is stored at multiple resolutions each retrieved by using the filename appended with _o, _s, _m, _l (indicating original, small, medium, or large)
-     * @param imageUri the bytes of the image
-     * @param returnedFilename the base filename to store the image on the server
-     * @param nosuffix optionally suppress creating multiple resolution images
-     */
-    export async function convertDataUri(imageUri: string, returnedFilename: string, nosuffix = false, replaceRootFilename?: string) {
-        try {
-            const posting = Utils.prepend('/uploadURI');
-            const returnedUri = await rp.post(posting, {
-                body: {
-                    uri: imageUri,
-                    name: returnedFilename,
-                    nosuffix,
-                    replaceRootFilename,
-                },
-                json: true,
-            });
-            return returnedUri;
-        } catch (e) {
-            console.log('ConvertDataURI :' + e);
+    export function log(prefixIn: string, messageName: string, messageIn: any, receiving: boolean) {
+        let prefix = prefixIn;
+        let message = messageIn;
+        if (!loggingEnabled) {
+            return;
         }
-    }
-
-    export function GetScreenTransform(ele?: HTMLElement | null): { scale: number; translateX: number; translateY: number } {
-        if (!ele) {
-            return { scale: 1, translateX: 1, translateY: 1 };
+        message = message || {};
+        if (logFilter !== undefined && logFilter !== message.type) {
+            return;
         }
-        const rect = ele.getBoundingClientRect();
-        const scale = ele.offsetWidth === 0 && rect.width === 0 ? 1 : rect.width / ele.offsetWidth;
-        const translateX = rect.left;
-        const translateY = rect.top;
+        const idString = (message.id || '').padStart(36, ' ');
+        prefix = prefix.padEnd(16, ' ');
+        console.log(`${prefix}: ${idString}, ${receiving ? 'receiving' : 'sending'} ${messageName} with data ${JSON.stringify(message)} `);
+    }
 
-        return { scale, translateX, translateY };
+    export function loggingCallback(prefix: string, func: (args: any) => any, messageName: string) {
+        return (args: any) => {
+            log(prefix, messageName, args, true);
+            func(args);
+        };
     }
 
     export function TraceConsoleLog() {
-        ['log', 'warn'].forEach(function (method) {
+        ['log', 'warn'].forEach(method => {
             const old = (console as any)[method];
             (console as any)[method] = function () {
                 let stack = new Error('').stack?.split(/\n/);
@@ -112,342 +56,97 @@ export namespace Utils {
         });
     }
 
-    /**
-     * A convenience method. Prepends the full path (i.e. http://localhost:<port>) to the
-     * requested extension
-     * @param extension the specified sub-path to append to the window origin
-     */
-    export function prepend(extension: string): string {
-        return window.location.origin + extension;
-    }
-    export function fileUrl(filename: string): string {
-        return prepend(`/files/${filename}`);
-    }
-
-    export function shareUrl(documentId: string): string {
-        return prepend(`/doc/${documentId}?sharing=true`);
-    }
-
-    export function CorsProxy(url: string): string {
-        return prepend('/corsProxy/') + encodeURIComponent(url);
-    }
-
-    export function CopyText(text: string) {
-        navigator.clipboard.writeText(text);
-    }
-
-    export function decimalToHexString(number: number) {
-        if (number < 0) {
-            number = 0xffffffff + number + 1;
-        }
-        return (number < 16 ? '0' : '') + number.toString(16).toUpperCase();
-    }
-
-    export function colorString(color: ColorResult) {
-        return color.hex.startsWith('#') && color.hex.length < 8 ? color.hex + (color.rgb.a ? decimalToHexString(Math.round(color.rgb.a * 255)) : 'ff') : color.hex;
-    }
-
-    export function fromRGBAstr(rgba: string) {
-        const rm = rgba.match(/rgb[a]?\(([ 0-9]+)/);
-        const r = rm ? Number(rm[1]) : 0;
-        const gm = rgba.match(/rgb[a]?\([ 0-9]+,([ 0-9]+)/);
-        const g = gm ? Number(gm[1]) : 0;
-        const bm = rgba.match(/rgb[a]?\([ 0-9]+,[ 0-9]+,([ 0-9]+)/);
-        const b = bm ? Number(bm[1]) : 0;
-        const am = rgba.match(/rgba?\([ 0-9]+,[ 0-9]+,[ 0-9]+,([ .0-9]+)/);
-        const a = am ? Number(am[1]) : 1;
-        return { r: r, g: g, b: b, a: a };
-    }
-
-    const isTransparentFunctionHack = 'isTransparent(__value__)';
-    export const noRecursionHack = '__noRecursion';
-
-    // special case filters
-    export const noDragDocsFilter = 'noDragDocs::any::check';
-    export const TransparentBackgroundFilter = `backgroundColor::${isTransparentFunctionHack},${noRecursionHack}::check`; // bcz: hack.  noRecursion should probably be either another ':' delimited field, or it should be a modifier to the comparision (eg., check, x, etc) field
-    export const OpaqueBackgroundFilter = `backgroundColor::${isTransparentFunctionHack},${noRecursionHack}::x`; // bcz: hack.  noRecursion should probably be either another ':' delimited field, or it should be a modifier to the comparision (eg., check, x, etc) field
-
-    export function IsRecursiveFilter(val: string) {
-        return !val.includes(noRecursionHack);
-    }
-    export function HasFunctionFilter(val: string) {
-        if (val.includes(isTransparentFunctionHack)) return (color: string) => color !== '' && DashColor(color).alpha() !== 1;
-        // add other function filters here...
-        return undefined;
-    }
-
-    export function toRGBAstr(col: { r: number; g: number; b: number; a?: number }) {
-        return 'rgba(' + col.r + ',' + col.g + ',' + col.b + (col.a !== undefined ? ',' + col.a : '') + ')';
-    }
-
-    export function HSLtoRGB(h: number, s: number, l: number) {
-        // Must be fractions of 1
-        // s /= 100;
-        // l /= 100;
-
-        const c = (1 - Math.abs(2 * l - 1)) * s,
-            x = c * (1 - Math.abs(((h / 60) % 2) - 1)),
-            m = l - c / 2;
-        let r = 0,
-            g = 0,
-            b = 0;
-        if (0 <= h && h < 60) {
-            r = c;
-            g = x;
-            b = 0;
-        } else if (60 <= h && h < 120) {
-            r = x;
-            g = c;
-            b = 0;
-        } else if (120 <= h && h < 180) {
-            r = 0;
-            g = c;
-            b = x;
-        } else if (180 <= h && h < 240) {
-            r = 0;
-            g = x;
-            b = c;
-        } else if (240 <= h && h < 300) {
-            r = x;
-            g = 0;
-            b = c;
-        } else if (300 <= h && h < 360) {
-            r = c;
-            g = 0;
-            b = x;
-        }
-        r = Math.round((r + m) * 255);
-        g = Math.round((g + m) * 255);
-        b = Math.round((b + m) * 255);
-        return { r: r, g: g, b: b };
-    }
-
-    export function RGBToHSL(r: number, g: number, b: number) {
-        // Make r, g, and b fractions of 1
-        r /= 255;
-        g /= 255;
-        b /= 255;
-
-        // Find greatest and smallest channel values
-        const cmin = Math.min(r, g, b),
-            cmax = Math.max(r, g, b),
-            delta = cmax - cmin;
-        let h = 0,
-            s = 0,
-            l = 0;
-        // Calculate hue
-
-        // No difference
-        if (delta === 0) h = 0;
-        // Red is max
-        else if (cmax === r) h = ((g - b) / delta) % 6;
-        // Green is max
-        else if (cmax === g) h = (b - r) / delta + 2;
-        // Blue is max
-        else h = (r - g) / delta + 4;
-
-        h = Math.round(h * 60);
-
-        // Make negative hues positive behind 360°
-        if (h < 0) h += 360; // Calculate lightness
-
-        l = (cmax + cmin) / 2;
-
-        // Calculate saturation
-        s = delta === 0 ? 0 : delta / (1 - Math.abs(2 * l - 1));
-
-        // Multiply l and s by 100
-        // s = +(s * 100).toFixed(1);
-        // l = +(l * 100).toFixed(1);
-
-        return { h: h, s: s, l: l };
-    }
-
-    export function scrollIntoView(targetY: number, targetHgt: number, scrollTop: number, contextHgt: number, minSpacing: number, scrollHeight: number) {
-        if (!targetHgt) return targetY; // if there's no height, then assume that
-        if (scrollTop + contextHgt < Math.min(scrollHeight, targetY + minSpacing + targetHgt)) {
-            return Math.ceil(targetY + minSpacing + targetHgt - contextHgt);
-        }
-        if (scrollTop >= Math.max(0, targetY - minSpacing)) {
-            return Math.max(0, Math.floor(targetY - minSpacing));
-        }
-    }
-
-    export function clamp(n: number, lower: number, upper: number) {
-        return Math.max(lower, Math.min(upper, n));
-    }
-
-    export function distanceBetweenHorizontalLines(xs: number, xe: number, y: number, xs2: number, xe2: number, y2: number): [number, number[]] {
-        if ((xs2 <= xs && xe2 >= xs) || (xs2 <= xe && xe2 >= xe) || (xs2 >= xs && xe2 <= xe)) return [Math.abs(y - y2), [Math.max(xs, xs2), y, Math.min(xe, xe2), y]];
-        if (xe2 <= xs) return [Math.sqrt((xe2 - xs) * (xe2 - xs) + (y2 - y) * (y2 - y)), [xs, y, xs, y]];
-        //if (xs2 > xe)
-        return [Math.sqrt((xs2 - xe) * (xs2 - xe) + (y2 - y) * (y2 - y)), [xe, y, xe, y]];
-    }
-    export function distanceBetweenVerticalLines(x: number, ys: number, ye: number, x2: number, ys2: number, ye2: number): [number, number[]] {
-        if ((ys2 <= ys && ye2 >= ys) || (ys2 <= ye && ye2 >= ye) || (ys2 >= ys && ye2 <= ye)) return [Math.abs(x - x2), [x, Math.max(ys, ys2), x, Math.min(ye, ye2)]];
-        if (ye2 <= ys) return [Math.sqrt((ye2 - ys) * (ye2 - ys) + (x2 - x) * (x2 - x)), [x, ys, x, ys]];
-        //if (ys2 > ye)
-        return [Math.sqrt((ys2 - ye) * (ys2 - ye) + (x2 - x) * (x2 - x)), [x, ye, x, ye]];
-    }
-
     export function rotPt(x: number, y: number, radAng: number) {
         return { x: x * Math.cos(radAng) - y * Math.sin(radAng), y: x * Math.sin(radAng) + y * Math.cos(radAng) };
     }
 
-    function project(px: number, py: number, ax: number, ay: number, bx: number, by: number) {
-        if (ax === bx && ay === by) return { point: { x: ax, y: ay }, left: false, dot: 0, t: 0 };
-        const atob = { x: bx - ax, y: by - ay };
-        const atop = { x: px - ax, y: py - ay };
-        const len = atob.x * atob.x + atob.y * atob.y;
-        var dot = atop.x * atob.x + atop.y * atob.y;
-        const t = Math.min(1, Math.max(0, dot / len));
-
-        dot = (bx - ax) * (py - ay) - (by - ay) * (px - ax);
-
-        return {
-            point: {
-                x: ax + atob.x * t,
-                y: ay + atob.y * t,
-            },
-            left: dot < 1,
-            dot: dot,
-            t: t,
-        };
-    }
-
-    export function closestPtBetweenRectangles(l: number, t: number, w: number, h: number, l1: number, t1: number, w1: number, h1: number, x: number, y: number) {
-        const r = l + w,
-            b = t + h;
-        const r1 = l1 + w1,
-            b1 = t1 + h1;
-        const hsegs = [
-            [l, r, t, l1, r1, t1],
-            [l, r, b, l1, r1, t1],
-            [l, r, t, l1, r1, b1],
-            [l, r, b, l1, r1, b1],
-        ];
-        const vsegs = [
-            [l, t, b, l1, t1, b1],
-            [r, t, b, l1, t1, b1],
-            [l, t, b, r1, t1, b1],
-            [r, t, b, r1, t1, b1],
-        ];
-        const res = hsegs.reduce(
-            (closest, seg) => {
-                const res = distanceBetweenHorizontalLines(seg[0], seg[1], seg[2], seg[3], seg[4], seg[5]);
-                return res[0] < closest[0] ? res : closest;
-            },
-            [Number.MAX_VALUE, []] as [number, number[]]
-        );
-        const fres = vsegs.reduce((closest, seg) => {
-            const res = distanceBetweenVerticalLines(seg[0], seg[1], seg[2], seg[3], seg[4], seg[5]);
-            return res[0] < closest[0] ? res : closest;
-        }, res);
+    export function getNearestPointInPerimeter(l: number, t: number, w: number, h: number, xIn: number, yIn: number) {
+        const r = l + w;
+        const b = t + h;
 
-        const near = project(x, y, fres[1][0], fres[1][1], fres[1][2], fres[1][3]);
-        return project(near.point.x, near.point.y, fres[1][0], fres[1][1], fres[1][2], fres[1][3]);
-    }
+        const x = clamp(xIn, l, r);
+        const y = clamp(yIn, t, b);
 
-    export function getNearestPointInPerimeter(l: number, t: number, w: number, h: number, x: number, y: number) {
-        const r = l + w,
-            b = t + h;
-
-        (x = clamp(x, l, r)), (y = clamp(y, t, b));
-
-        const dl = Math.abs(x - l),
-            dr = Math.abs(x - r),
-            dt = Math.abs(y - t),
-            db = Math.abs(y - b);
+        const dl = Math.abs(x - l);
+        const dr = Math.abs(x - r);
+        const dt = Math.abs(y - t);
+        const db = Math.abs(y - b);
 
         const m = Math.min(dl, dr, dt, db);
 
         return m === dt ? [x, t] : m === db ? [x, b] : m === dl ? [l, y] : [r, y];
     }
+}
+export function decimalToHexString(numberIn: number) {
+    const number = numberIn < 0 ? 0xffffffff + numberIn + 1 : numberIn;
+    return (number < 16 ? '0' : '') + number.toString(16).toUpperCase();
+}
 
-    export function GetClipboardText(): string {
-        const textArea = document.createElement('textarea');
-        document.body.appendChild(textArea);
-        textArea.focus();
-        textArea.select();
-
-        try {
-            document.execCommand('paste');
-        } catch (err) {}
-
-        const val = textArea.value;
-        document.body.removeChild(textArea);
-        return val;
-    }
-
-    export const loggingEnabled: Boolean = false;
-    export const logFilter: number | undefined = undefined;
-
-    export function log(prefix: string, messageName: string, message: any, receiving: boolean) {
-        if (!loggingEnabled) {
-            return;
-        }
-        message = message || {};
-        if (logFilter !== undefined && logFilter !== message.type) {
-            return;
-        }
-        const idString = (message.id || '').padStart(36, ' ');
-        prefix = prefix.padEnd(16, ' ');
-        console.log(`${prefix}: ${idString}, ${receiving ? 'receiving' : 'sending'} ${messageName} with data ${JSON.stringify(message)} `);
-    }
-
-    export function loggingCallback(prefix: string, func: (args: any) => any, messageName: string) {
-        return (args: any) => {
-            log(prefix, messageName, args, true);
-            func(args);
-        };
-    }
+export function distanceBetweenHorizontalLines(xs: number, xe: number, y: number, xs2: number, xe2: number, y2: number): [number, number[]] {
+    if ((xs2 <= xs && xe2 >= xs) || (xs2 <= xe && xe2 >= xe) || (xs2 >= xs && xe2 <= xe)) return [Math.abs(y - y2), [Math.max(xs, xs2), y, Math.min(xe, xe2), y]];
+    if (xe2 <= xs) return [Math.sqrt((xe2 - xs) * (xe2 - xs) + (y2 - y) * (y2 - y)), [xs, y, xs, y]];
+    // if (xs2 > xe)
+    return [Math.sqrt((xs2 - xe) * (xs2 - xe) + (y2 - y) * (y2 - y)), [xe, y, xe, y]];
+}
+export function distanceBetweenVerticalLines(x: number, ys: number, ye: number, x2: number, ys2: number, ye2: number): [number, number[]] {
+    if ((ys2 <= ys && ye2 >= ys) || (ys2 <= ye && ye2 >= ye) || (ys2 >= ys && ye2 <= ye)) return [Math.abs(x - x2), [x, Math.max(ys, ys2), x, Math.min(ye, ye2)]];
+    if (ye2 <= ys) return [Math.sqrt((ye2 - ys) * (ye2 - ys) + (x2 - x) * (x2 - x)), [x, ys, x, ys]];
+    // if (ys2 > ye)
+    return [Math.sqrt((ys2 - ye) * (ys2 - ye) + (x2 - x) * (x2 - x)), [x, ye, x, ye]];
+}
 
-    export function Emit<T>(socket: Socket, message: Message<T>, args: T) {
-        log('Emit', message.Name, args, false);
-        socket.emit(message.Message, args);
-    }
+function project(px: number, py: number, ax: number, ay: number, bx: number, by: number) {
+    if (ax === bx && ay === by) return { point: { x: ax, y: ay }, left: false, dot: 0, t: 0 };
+    const atob = { x: bx - ax, y: by - ay };
+    const atop = { x: px - ax, y: py - ay };
+    const len = atob.x * atob.x + atob.y * atob.y;
+    let dot = atop.x * atob.x + atop.y * atob.y;
+    const t = Math.min(1, Math.max(0, dot / len));
 
-    export function AddServerHandler<T>(socket: Socket, message: Message<T>, handler: (args: T) => any) {
-        socket.on(message.Message, loggingCallback('Incoming', handler, message.Name));
-    }
+    dot = (bx - ax) * (py - ay) - (by - ay) * (px - ax);
 
-    export function AddServerHandlerCallback<T>(socket: Socket, message: Message<T>, handler: (args: [T, (res: any) => any]) => any) {
-        socket.on(message.Message, (arg: T, fn: (res: any) => any) => {
-            log('S receiving', message.Name, arg, true);
-            handler([arg, loggingCallback('S sending', fn, message.Name)]);
-        });
-    }
-    export type RoomHandler = (socket: Socket, room: string) => any;
-    export type UsedSockets = Socket;
-    export type RoomMessage = 'create or join' | 'created' | 'joined';
-    export function AddRoomHandler(socket: Socket, message: RoomMessage, handler: RoomHandler) {
-        socket.on(message, (room: any) => handler(socket, room));
-    }
+    return {
+        point: {
+            x: ax + atob.x * t,
+            y: ay + atob.y * t,
+        },
+        left: dot < 1,
+        dot: dot,
+        t: t,
+    };
 }
 
-export function OmitKeys(obj: any, keys: string[], pattern?: string, addKeyFunc?: (dup: any) => void): { omit: any; extract: any } {
-    const omit: any = { ...obj };
-    const extract: any = {};
-    keys.forEach(key => {
-        extract[key] = omit[key];
-        delete omit[key];
-    });
-    pattern &&
-        Array.from(Object.keys(omit))
-            .filter(key => key.match(pattern))
-            .forEach(key => {
-                extract[key] = omit[key];
-                delete omit[key];
-            });
-    addKeyFunc?.(omit);
-    return { omit, extract };
-}
+export function closestPtBetweenRectangles(l: number, t: number, w: number, h: number, l1: number, t1: number, w1: number, h1: number, x: number, y: number) {
+    const r = l + w;
+    const b = t + h;
+    const r1 = l1 + w1;
+    const b1 = t1 + h1;
+    const hsegs = [
+        [l, r, t, l1, r1, t1],
+        [l, r, b, l1, r1, t1],
+        [l, r, t, l1, r1, b1],
+        [l, r, b, l1, r1, b1],
+    ];
+    const vsegs = [
+        [l, t, b, l1, t1, b1],
+        [r, t, b, l1, t1, b1],
+        [l, t, b, r1, t1, b1],
+        [r, t, b, r1, t1, b1],
+    ];
+    const res = hsegs.reduce(
+        (closest, seg) => {
+            const res = distanceBetweenHorizontalLines(seg[0], seg[1], seg[2], seg[3], seg[4], seg[5]);
+            return res[0] < closest[0] ? res : closest;
+        },
+        [Number.MAX_VALUE, []] as [number, number[]]
+    );
+    const fres = vsegs.reduce((closest, seg) => {
+        const res = distanceBetweenVerticalLines(seg[0], seg[1], seg[2], seg[3], seg[4], seg[5]);
+        return res[0] < closest[0] ? res : closest;
+    }, res);
 
-export function WithKeys(obj: any, keys: string[], addKeyFunc?: (dup: any) => void) {
-    const dup: any = {};
-    keys.forEach(key => (dup[key] = obj[key]));
-    addKeyFunc && addKeyFunc(dup);
-    return dup;
+    const near = project(x, y, fres[1][0], fres[1][1], fres[1][2], fres[1][3]);
+    return project(near.point.x, near.point.y, fres[1][0], fres[1][1], fres[1][2], fres[1][3]);
 }
 
 export function timenow() {
@@ -455,7 +154,6 @@ export function timenow() {
     let ampm = 'am';
     let h = now.getHours();
     let m: any = now.getMinutes();
-    const s: any = now.getSeconds();
     if (h >= 12) {
         if (h > 12) h -= 12;
         ampm = 'pm';
@@ -464,14 +162,8 @@ export function timenow() {
     return now.toLocaleDateString() + ' ' + h + ':' + m + ' ' + ampm;
 }
 
-export function incrementTitleCopy(title: string) {
-    const numstr = title.match(/.*(\{([0-9]*)\})+/);
-    const copyNumStr = `{${1 + (numstr ? +numstr[2] : 0)}}`;
-    return (numstr ? title.replace(numstr[1], '') : title) + copyNumStr;
-}
-
-export function formatTime(time: number) {
-    time = Math.round(time);
+export function formatTime(timeIn: number) {
+    const time = Math.round(timeIn);
     const hours = Math.floor(time / 60 / 60);
     const minutes = Math.floor(time / 60) - hours * 60;
     const seconds = time % 60;
@@ -503,93 +195,31 @@ export function intersectRect(r1: { left: number; top: number; width: number; he
 }
 
 export function stringHash(s?: string) {
+    // eslint-disable-next-line no-bitwise
     return !s ? undefined : Math.abs(s.split('').reduce((a: any, b: any) => (a => a & a)((a << 5) - a + b.charCodeAt(0)), 0));
 }
 
 export function percent2frac(percent: string) {
     return Number(percent.substr(0, percent.length - 1)) / 100;
 }
+export type Without<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
+
+export type Predicate<K, V> = (entry: [K, V]) => boolean;
 
 export function numberRange(num: number) {
     return num > 0 && num < 1000 ? Array.from(Array(num)).map((v, i) => i) : [];
 }
 
-export function returnTransparent() {
-    return 'transparent';
-}
-
-export function returnTrue() {
-    return true;
-}
-
-export function returnIgnore(): 'ignore' {
-    return 'ignore';
-}
-export function returnAlways(): 'always' {
-    return 'always';
-}
-export function returnNever(): 'never' {
-    return 'never';
-}
-
-export function returnDefault(): 'default' {
-    return 'default';
-}
-
-export function return18() {
-    return 18;
-}
-
-export function returnFalse() {
-    return false;
-}
-
-export function returnAll(): 'all' {
-    return 'all';
-}
-
-export function returnNone(): 'none' {
-    return 'none';
-}
-
-export function returnVal(val1?: number, val2?: number) {
-    return val1 ? val1 : val2 !== undefined ? val2 : 0;
-}
-
-export function returnOne() {
-    return 1;
-}
-
-export function returnZero() {
-    return 0;
-}
-
-export function returnEmptyString() {
-    return '';
-}
-
-export function returnEmptyFilter() {
-    return [] as string[];
-}
-
-export function returnEmptyDoclist() {
-    return [] as any[];
-}
-
-export let emptyPath: DocumentView[] = [];
-
 export function emptyFunction() {
     return undefined;
 }
 
+export const emptyPath: any[] = [];
+
 export function unimplementedFunction() {
     throw new Error('This function is not implemented, but should be.');
 }
 
-export type Without<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
-
-export type Predicate<K, V> = (entry: [K, V]) => boolean;
-
 export function DeepCopy<K, V>(source: Map<K, V>, predicate?: Predicate<K, V>) {
     const deepCopy = new Map<K, V>();
     const entries = source.entries();
@@ -616,197 +246,6 @@ export namespace JSONUtils {
     }
 }
 
-const easeFunc = (transition: 'ease' | 'linear' | undefined, currentTime: number, start: number, change: number, duration: number) => {
-    if (transition === 'linear') {
-        let newCurrentTime = currentTime / duration; //  currentTime / (duration / 2);
-        return start + newCurrentTime * change;
-    }
-
-    let newCurrentTime = currentTime / (duration / 2);
-    if (newCurrentTime < 1) {
-        return (change / 2) * newCurrentTime * newCurrentTime + start;
-    }
-
-    newCurrentTime -= 1;
-    return (-change / 2) * (newCurrentTime * (newCurrentTime - 2) - 1) + start;
-};
-
-export function smoothScroll(duration: number, element: HTMLElement | HTMLElement[], to: number, transition: 'ease' | 'linear' | undefined, stopper?: () => void) {
-    stopper?.();
-    const elements = element instanceof HTMLElement ? [element] : element;
-    const starts = elements.map(element => element.scrollTop);
-    const startDate = new Date().getTime();
-    let _stop = false;
-    const stop = () => (_stop = true);
-    const animateScroll = () => {
-        const currentDate = new Date().getTime();
-        const currentTime = currentDate - startDate;
-        const setScrollTop = (element: HTMLElement, value: number) => (element.scrollTop = value);
-        if (!_stop) {
-            if (currentTime < duration) {
-                elements.forEach((element, i) => currentTime && setScrollTop(element, easeFunc(transition, Math.min(currentTime, duration), starts[i], to - starts[i], duration)));
-                requestAnimationFrame(animateScroll);
-            } else {
-                elements.forEach(element => setScrollTop(element, to));
-            }
-        }
-    };
-    animateScroll();
-    return stop;
-}
-
-export function smoothScrollHorizontal(duration: number, element: HTMLElement | HTMLElement[], to: number) {
-    const elements = element instanceof HTMLElement ? [element] : element;
-    const starts = elements.map(element => element.scrollLeft);
-    const startDate = new Date().getTime();
-
-    const animateScroll = () => {
-        const currentDate = new Date().getTime();
-        const currentTime = currentDate - startDate;
-        elements.map((element, i) => (element.scrollLeft = easeFunc('ease', currentTime, starts[i], to - starts[i], duration)));
-
-        if (currentTime < duration) {
-            requestAnimationFrame(animateScroll);
-        } else {
-            elements.forEach(element => (element.scrollLeft = to));
-        }
-    };
-    animateScroll();
-}
-
-export function addStyleSheet(styleType: string = 'text/css') {
-    const style = document.createElement('style');
-    style.type = styleType;
-    const sheets = document.head.appendChild(style);
-    return (sheets as any).sheet;
-}
-export function addStyleSheetRule(sheet: any, selector: any, css: any, selectorPrefix = '.') {
-    const propText =
-        typeof css === 'string'
-            ? css
-            : Object.keys(css)
-                  .map(p => p + ':' + (p === 'content' ? "'" + css[p] + "'" : css[p]))
-                  .join(';');
-    return sheet.insertRule(selectorPrefix + selector + '{' + propText + '}', sheet.cssRules.length);
-}
-export function removeStyleSheetRule(sheet: any, rule: number) {
-    if (sheet.rules.length) {
-        sheet.removeRule(rule);
-        return true;
-    }
-    return false;
-}
-export function clearStyleSheetRules(sheet: any) {
-    if (sheet.rules.length) {
-        numberRange(sheet.rules.length).map(n => sheet.removeRule(0));
-        return true;
-    }
-    return false;
-}
-
-export function simulateMouseClick(element: Element | null | undefined, x: number, y: number, sx: number, sy: number, rightClick = true) {
-    if (!element) return;
-    ['pointerdown', 'pointerup'].map(event => {
-        const me = new PointerEvent(event, {
-            view: window,
-            bubbles: true,
-            cancelable: true,
-            button: 2,
-            pointerType: 'mouse',
-            clientX: x,
-            clientY: y,
-            screenX: sx,
-            screenY: sy,
-        });
-        (me as any).dash = true;
-        element.dispatchEvent(me);
-    });
-
-    if (rightClick) {
-        const me = new MouseEvent('contextmenu', {
-            view: window,
-            bubbles: true,
-            cancelable: true,
-            button: 2,
-            clientX: x,
-            clientY: y,
-            movementX: 0,
-            movementY: 0,
-            screenX: sx,
-            screenY: sy,
-        });
-        (me as any).dash = true;
-        element.dispatchEvent(me);
-    }
-}
-
-export function DashColor(color: string) {
-    try {
-        return color ? Color(color.toLowerCase()) : Color('transparent');
-    } catch (e) {
-        if (color.includes('gradient')) console.log("using color 'white' in place of :" + color);
-        else console.log('COLOR error:', e);
-        return Color('white');
-    }
-}
-
-export function lightOrDark(color: any) {
-    if (color === 'transparent' || !color) return Colors.BLACK;
-    if (color.startsWith?.('linear')) return Colors.BLACK;
-    if (DashColor(color).isLight()) return Colors.BLACK;
-    return Colors.WHITE;
-}
-
-export function getWordAtPoint(elem: any, x: number, y: number): string | undefined {
-    if (elem.tagName === 'INPUT') return 'input';
-    if (elem.tagName === 'TEXTAREA') return 'textarea';
-    if (elem.nodeType === elem.TEXT_NODE) {
-        const range = elem.ownerDocument.createRange();
-        range.selectNodeContents(elem);
-        var currentPos = 0;
-        const endPos = range.endOffset;
-        while (currentPos + 1 <= endPos) {
-            range.setStart(elem, currentPos);
-            range.setEnd(elem, currentPos + 1);
-            const rangeRect = range.getBoundingClientRect();
-            if (rangeRect.left <= x && rangeRect.right >= x && rangeRect.top <= y && rangeRect.bottom >= y) {
-                range.expand?.('word'); // doesn't exist in firefox
-                const ret = range.toString();
-                range.detach();
-                return ret;
-            }
-            currentPos += 1;
-        }
-    } else {
-        for (const childNode of elem.childNodes) {
-            const range = childNode.ownerDocument.createRange();
-            range.selectNodeContents(childNode);
-            const rangeRect = range.getBoundingClientRect();
-            if (rangeRect.left <= x && rangeRect.right >= x && rangeRect.top <= y && rangeRect.bottom >= y) {
-                range.detach();
-                const word = getWordAtPoint(childNode, x, y);
-                if (word) return word;
-            } else {
-                range.detach();
-            }
-        }
-    }
-    return undefined;
-}
-
-export function isTargetChildOf(ele: HTMLDivElement | null, target: Element | null) {
-    let entered = false;
-    for (let child = target; !entered && child; child = child.parentElement) {
-        entered = child === ele;
-    }
-    return entered;
-}
-
-export function StopEvent(e: React.PointerEvent | React.MouseEvent) {
-    e.stopPropagation();
-    e.preventDefault();
-}
-
 /**
  * Helper method for converting pixel string eg. '32px' into number eg. 32
  * @param value: string with 'px' ending
@@ -816,100 +255,10 @@ export function StopEvent(e: React.PointerEvent | React.MouseEvent) {
  * '32px' -> 32
  */
 export function numberValue(value: string | undefined): number {
-    if (value == undefined) return 0;
+    if (value === undefined) return 0;
     return parseInt(value);
 }
 
 export function numbersAlmostEqual(num1: number, num2: number) {
     return Math.abs(num1 - num2) < 0.2;
 }
-
-export function setupMoveUpEvents(
-    target: object,
-    e: React.PointerEvent,
-    moveEvent: (e: PointerEvent, down: number[], delta: number[]) => boolean,
-    upEvent: (e: PointerEvent, movement: number[], isClick: boolean) => any,
-    clickEvent: (e: PointerEvent, doubleTap?: boolean) => any,
-    stopPropagation: boolean = true,
-    stopMovePropagation: boolean = true,
-    noDoubleTapTimeout?: () => void
-) {
-    const doubleTapTimeout = 300;
-    (target as any)._doubleTap = Date.now() - (target as any)._lastTap < doubleTapTimeout;
-    (target as any)._lastTap = Date.now();
-    (target as any)._downX = (target as any)._lastX = e.clientX;
-    (target as any)._downY = (target as any)._lastY = e.clientY;
-    (target as any)._noClick = false;
-    var moving = false;
-
-    const _moveEvent = (e: PointerEvent): void => {
-        if (moving || Math.abs(e.clientX - (target as any)._downX) > Utils.DRAG_THRESHOLD || Math.abs(e.clientY - (target as any)._downY) > Utils.DRAG_THRESHOLD) {
-            moving = true;
-            if ((target as any)._doubleTime) {
-                clearTimeout((target as any)._doubleTime);
-                (target as any)._doubleTime = undefined;
-            }
-            if (moveEvent(e, [(target as any)._downX, (target as any)._downY], [e.clientX - (target as any)._lastX, e.clientY - (target as any)._lastY])) {
-                document.removeEventListener('pointermove', _moveEvent);
-                document.removeEventListener('pointerup', _upEvent);
-            }
-        }
-        (target as any)._lastX = e.clientX;
-        (target as any)._lastY = e.clientY;
-        stopMovePropagation && e.stopPropagation();
-    };
-    const _upEvent = (e: PointerEvent): void => {
-        const isClick = Math.abs(e.clientX - (target as any)._downX) < 4 && Math.abs(e.clientY - (target as any)._downY) < 4;
-        upEvent(e, [e.clientX - (target as any)._downX, e.clientY - (target as any)._downY], isClick);
-        if (isClick) {
-            if (!(target as any)._doubleTime && noDoubleTapTimeout) {
-                (target as any)._doubleTime = setTimeout(() => {
-                    noDoubleTapTimeout?.();
-                    (target as any)._doubleTime = undefined;
-                }, doubleTapTimeout);
-            }
-            if ((target as any)._doubleTime && (target as any)._doubleTap) {
-                clearTimeout((target as any)._doubleTime);
-                (target as any)._doubleTime = undefined;
-            }
-            (target as any)._noClick = clickEvent(e, (target as any)._doubleTap);
-        }
-        document.removeEventListener('pointermove', _moveEvent);
-        document.removeEventListener('pointerup', _upEvent, true);
-    };
-    const _clickEvent = (e: MouseEvent): void => {
-        if ((target as any)._noClick) e.stopPropagation();
-        document.removeEventListener('click', _clickEvent, true);
-    };
-    if (stopPropagation) {
-        e.stopPropagation();
-        e.preventDefault();
-    }
-    document.addEventListener('pointermove', _moveEvent);
-    document.addEventListener('pointerup', _upEvent, true);
-    document.addEventListener('click', _clickEvent, true);
-}
-
-export function DivHeight(ele: HTMLElement): number {
-    return Number(getComputedStyle(ele).height.replace('px', ''));
-}
-export function DivWidth(ele: HTMLElement): number {
-    return Number(getComputedStyle(ele).width.replace('px', ''));
-}
-
-export function dateRangeStrToDates(dateStr: string) {
-    // dateStr in yyyy-mm-dd format
-    const dateRangeParts = dateStr.split('|'); // splits into from and to date
-    const fromParts = dateRangeParts[0].split('-');
-    const toParts = dateRangeParts[1].split('-');
-
-    const fromYear = parseInt(fromParts[0]);
-    const fromMonth = parseInt(fromParts[1]) - 1;
-    const fromDay = parseInt(fromParts[2]);
-
-    const toYear = parseInt(toParts[0]);
-    const toMonth = parseInt(toParts[1]) - 1;
-    const toDay = parseInt(toParts[2]);
-
-    return [new Date(fromYear, fromMonth, fromDay), new Date(toYear, toMonth, toDay)];
-}
diff --git a/src/client/DocServer.ts b/src/client/DocServer.ts
index 321572071..804e6d1d7 100644
--- a/src/client/DocServer.ts
+++ b/src/client/DocServer.ts
@@ -1,20 +1,15 @@
 import { runInAction } from 'mobx';
-import * as rp from 'request-promise';
-import { Doc, DocListCast, Opt } from '../fields/Doc';
+import { Socket, io } from 'socket.io-client';
+import { ClientUtils } from '../ClientUtils';
+import { Utils, emptyFunction } from '../Utils';
+import { Doc, Opt } from '../fields/Doc';
 import { UpdatingFromServer } from '../fields/DocSymbols';
 import { FieldLoader } from '../fields/FieldLoader';
 import { HandleUpdate, Id, Parent } from '../fields/FieldSymbols';
-import { ObjectField } from '../fields/ObjectField';
+import { ObjectField, SetObjGetRefField, SetObjGetRefFields } from '../fields/ObjectField';
 import { RefField } from '../fields/RefField';
-import { DocCast, StrCast } from '../fields/Types';
-//import MobileInkOverlay from '../mobile/MobileInkOverlay';
-import { io, Socket } from 'socket.io-client';
-import { emptyFunction, Utils } from '../Utils';
 import { GestureContent, Message, MessageStore, MobileDocumentUploadContent, MobileInkOverlayContent, UpdateMobileInkOverlayPositionContent, YoutubeQueryTypes } from './../server/Message';
-import { DocumentType } from './documents/DocumentTypes';
-import { LinkManager } from './util/LinkManager';
 import { SerializationHelper } from './util/SerializationHelper';
-//import { GestureOverlay } from './views/GestureOverlay';
 
 /**
  * This class encapsulates the transfer and cross-client synchronization of
@@ -30,23 +25,32 @@ import { SerializationHelper } from './util/SerializationHelper';
  * or update ourselves based on the server's update message, that occurs here
  */
 export namespace DocServer {
-    let _cache: { [id: string]: RefField | Promise<Opt<RefField>> } = {};
+    // eslint-disable-next-line import/no-mutable-exports
+    export let _cache: { [id: string]: RefField | Promise<Opt<RefField>> } = {};
+
+    function errorFunc(): never {
+        throw new Error("Can't use DocServer without calling init first");
+    }
+    let _UpdateField: (id: string, diff: any) => void = errorFunc;
+    let _CreateField: (field: RefField) => void = errorFunc;
 
     export function AddServerHandler<T>(socket: Socket, message: Message<T>, handler: (args: T) => any) {
         socket.on(message.Message, Utils.loggingCallback('Incoming', handler, message.Name));
     }
     export function Emit<T>(socket: Socket, message: Message<T>, args: T) {
-        //log('Emit', message.Name, args, false);
+        // log('Emit', message.Name, args, false);
         socket.emit(message.Message, args);
     }
     export function EmitCallback<T>(socket: Socket, message: Message<T>, args: T): Promise<any>;
     export function EmitCallback<T>(socket: Socket, message: Message<T>, args: T, fn: (args: any) => any): void;
     export function EmitCallback<T>(socket: Socket, message: Message<T>, args: T, fn?: (args: any) => any): void | Promise<any> {
-        //log('Emit', message.Name, args, false);
+        // log('Emit', message.Name, args, false);
         if (fn) {
             socket.emit(message.Message, args, Utils.loggingCallback('Receiving', fn, message.Name));
         } else {
-            return new Promise<any>(res => socket.emit(message.Message, args, Utils.loggingCallback('Receiving', res, message.Name)));
+            return new Promise<any>(res => {
+                socket.emit(message.Message, args, Utils.loggingCallback('Receiving', res, message.Name));
+            });
         }
     }
 
@@ -59,57 +63,21 @@ export namespace DocServer {
 
         return foundDocId ? (_cache[foundDocId] as Doc) : undefined;
     }
-    let cacheDocumentIds = ''; // ; separate string of all documents ids in the user's working set (cached on the server)
-    export let CacheNeedsUpdate = false;
-    export function UPDATE_SERVER_CACHE() {
-        const prototypes = Object.values(DocumentType)
-            .filter(type => type !== DocumentType.NONE)
-            .map(type => _cache[type + 'Proto'])
-            .filter(doc => doc instanceof Doc)
-            .map(doc => doc as Doc);
-        const references = new Set<Doc>(prototypes);
-        Doc.FindReferences(Doc.UserDoc(), references, undefined);
-        DocListCast(DocCast(Doc.UserDoc().myLinkDatabase).data).forEach(link => {
-            if (!references.has(DocCast(link.link_anchor_1)) && !references.has(DocCast(link.link_anchor_2))) {
-                Doc.RemoveDocFromList(DocCast(Doc.UserDoc().myLinkDatabase), 'data', link);
-                Doc.AddDocToList(Doc.MyRecentlyClosed, undefined, link);
-            }
-        });
-        LinkManager.Instance.userLinkDBs.forEach(linkDb => Doc.FindReferences(linkDb, references, undefined));
-        const filtered = Array.from(references);
-
-        const newCacheUpdate = filtered.map(doc => doc[Id]).join(';');
-        if (newCacheUpdate === cacheDocumentIds) return;
-        cacheDocumentIds = newCacheUpdate;
-
-        // print out 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)'));
-        Doc.MyDockedBtns.linearView_IsOpen && strings.sort().forEach((str, i) => console.log(i.toString() + ' ' + str));
-
-        rp.post(Utils.prepend('/setCacheDocumentIds'), {
-            body: {
-                cacheDocumentIds,
-            },
-            json: true,
-        });
-    }
     export let _socket: Socket;
     // this client's distinct GUID created at initialization
     let USER_ID: string;
     // indicates whether or not a document is currently being udpated, and, if so, its id
 
     export enum WriteMode {
-        Default = 0, //Anything goes
-        Playground = 1, //Playground (write own/no read other updates)
-        LiveReadonly = 2, //Live Readonly (no write/read others)
-        LivePlayground = 3, //Live Playground (write own/read others)
+        Default = 0, // Anything goes
+        Playground = 1, // Playground (write own/no read other updates)
+        LiveReadonly = 2, // Live Readonly (no write/read others)
+        LivePlayground = 3, // Live Playground (write own/read others)
     }
     const fieldWriteModes: { [field: string]: WriteMode } = {};
     const docsWithUpdates: { [field: string]: Set<Doc> } = {};
 
-    export var PlaygroundFields: string[] = [];
+    export const PlaygroundFields: string[] = [];
     export function setLivePlaygroundFields(livePlaygroundFields: string[]) {
         DocServer.PlaygroundFields.push(...livePlaygroundFields);
         livePlaygroundFields.forEach(f => DocServer.setFieldWriteMode(f, DocServer.WriteMode.LivePlayground));
@@ -134,7 +102,7 @@ export namespace DocServer {
     }
 
     export function getFieldWriteMode(field: string) {
-        return Doc.CurrentUserEmail === 'guest' ? WriteMode.LivePlayground : fieldWriteModes[field] || WriteMode.Default;
+        return ClientUtils.CurrentUserEmail === 'guest' ? WriteMode.LivePlayground : fieldWriteModes[field] || WriteMode.Default;
     }
 
     export function registerDocWithCachedUpdate(doc: Doc, field: string, oldValue: any) {
@@ -185,56 +153,14 @@ export namespace DocServer {
         window.location.reload();
     }
 
-    export function init(protocol: string, hostname: string, port: number, identifier: string) {
-        _cache = {};
-        USER_ID = identifier;
-        protocol = protocol.startsWith('https') ? 'wss' : 'ws';
-        _socket = io(`${protocol}://${hostname}:${port}`, { transports: ['websocket'], rejectUnauthorized: false });
-        _socket.on("connect_error", (err:any) => console.log(err));
-        // io.connect(`https://7f079dda.ngrok.io`);// if using ngrok, create a special address for the websocket
-
-        _GetCachedRefField = _GetCachedRefFieldImpl;
-        _GetRefField = _GetRefFieldImpl;
-        _GetRefFields = _GetRefFieldsImpl;
-        _CreateField = _CreateFieldImpl;
-        _UpdateField = _UpdateFieldImpl;
-
-        /**
-         * Whenever the server sends us its handshake message on our
-         * websocket, we use the above function to return the handshake.
-         */
-        DocServer.AddServerHandler(_socket, MessageStore.Foo, onConnection);
-        DocServer.AddServerHandler(_socket, MessageStore.UpdateField, respondToUpdate);
-        DocServer.AddServerHandler(_socket, MessageStore.DeleteField, respondToDelete);
-        DocServer.AddServerHandler(_socket, MessageStore.DeleteFields, respondToDelete);
-        DocServer.AddServerHandler(_socket, MessageStore.ConnectionTerminated, alertUser);
-
-        // // mobile ink overlay socket events to communicate between mobile view and desktop view
-        // _socket.addEventListener('receiveGesturePoints', (content: GestureContent) => {
-        //     //   MobileInkOverlay.Instance.drawStroke(content);
-        // });
-        // _socket.addEventListener('receiveOverlayTrigger', (content: MobileInkOverlayContent) => {
-        //     //GestureOverlay.Instance.enableMobileInkOverlay(content);
-        //     //    MobileInkOverlay.Instance.initMobileInkOverlay(content);
-        // });
-        // _socket.addEventListener('receiveUpdateOverlayPosition', (content: UpdateMobileInkOverlayPositionContent) => {
-        //     //   MobileInkOverlay.Instance.updatePosition(content);
-        // });
-        // _socket.addEventListener('receiveMobileDocumentUpload', (content: MobileDocumentUploadContent) => {
-        //     //   MobileInkOverlay.Instance.uploadDocument(content);
-        // });
-    }
-
-    function errorFunc(): never {
-        throw new Error("Can't use DocServer without calling init first");
-    }
-
     export namespace Control {
         let _isReadOnly = false;
         export function makeReadOnly() {
             if (!_isReadOnly) {
                 _isReadOnly = true;
-                _CreateField = field => (_cache[field[Id]] = field);
+                _CreateField = field => {
+                    _cache[field[Id]] = field;
+                };
                 _UpdateField = emptyFunction;
                 // _RespondToUpdate = emptyFunction; // bcz: option: don't clear RespondToUpdate to continue to receive updates as others change the DB
             }
@@ -313,7 +239,8 @@ export namespace DocServer {
                     });
                     cached[UpdatingFromServer] = false;
                     return cached;
-                } else if (field !== undefined) {
+                }
+                if (field !== undefined) {
                     _cache[id] = field;
                 } else {
                     delete _cache[id];
@@ -327,24 +254,25 @@ export namespace DocServer {
             // being retrieved and cached
             !force && (_cache[id] = deserializeField);
             return force ? (cached as any) : deserializeField;
-        } else if (cached instanceof Promise) {
+        }
+        if (cached instanceof Promise) {
             // BEING RETRIEVED AND CACHED => some other caller previously (likely recently) called GetRefField(s),
             // and requested the document I'm looking for. Shouldn't fetch again, just
             // return this promise which will resolve to the field itself (see 7)
             return cached;
-        } else {
-            // CACHED => great, let's just return the cached field we have
-            return Promise.resolve(cached).then(field => {
-                //(field instanceof Doc) && fetchProto(field);
-                return field;
-            });
         }
+        // CACHED => great, let's just return the cached field we have
+        return Promise.resolve(cached).then(
+            field => field
+            // (field instanceof Doc) && fetchProto(field);
+        );
     };
     const _GetCachedRefFieldImpl = (id: string): Opt<RefField> => {
         const cached = _cache[id];
         if (cached !== undefined && !(cached instanceof Promise)) {
             return cached;
         }
+        return undefined;
     };
 
     let _GetRefField: (id: string, force: boolean) => Promise<Opt<RefField>> = errorFunc;
@@ -358,7 +286,7 @@ export namespace DocServer {
     }
 
     export async function getYoutubeChannels() {
-        return await DocServer.EmitCallback(_socket, MessageStore.YoutubeApiQuery, { type: YoutubeQueryTypes.Channels });
+        return DocServer.EmitCallback(_socket, MessageStore.YoutubeApiQuery, { type: YoutubeQueryTypes.Channels });
     }
 
     export function getYoutubeVideos(videoTitle: string, callBack: (videos: any[]) => void) {
@@ -379,21 +307,24 @@ export namespace DocServer {
         const requestedIds: string[] = [];
         const promises: Promise<any>[] = [];
 
-        let defaultRes: any = undefined;
-        const defaultPromise = new Promise<any>(res => (defaultRes = res));
-        let defaultPromises: { p: Promise<any>; id: string }[] = [];
+        let defaultRes: any;
+        const defaultPromise = new Promise<any>(res => {
+            defaultRes = res;
+        });
+        const defaultPromises: { p: Promise<any>; id: string }[] = [];
         // 1) an initial pass through the cache to determine
         // i) which documents need to be fetched
         // ii) which are already in the process of being fetched
         // iii) which already exist in the cache
+        // eslint-disable-next-line no-restricted-syntax
         for (const id of ids.filter(id => id)) {
             const cached = _cache[id];
             if (cached === undefined) {
                 defaultPromises.push({
                     id,
-                    p: (_cache[id] = new Promise<any>(async res => {
-                        await defaultPromise;
-                        res(_cache[id]);
+                    // eslint-disable-next-line no-loop-func
+                    p: (_cache[id] = new Promise<any>(res => {
+                        defaultPromise.then(() => res(_cache[id]));
                     })),
                 });
                 // NOT CACHED => we'll have to send a request to the server
@@ -415,7 +346,11 @@ export namespace DocServer {
             // fields for the given ids. This returns a promise, which, when resolved, indicates that all the JSON serialized versions of
             // the fields have been returned from the server
             console.log('Requesting ' + requestedIds.length);
-            setTimeout(() => runInAction(() => (FieldLoader.ServerLoadStatus.requested = requestedIds.length)));
+            setTimeout(() =>
+                runInAction(() => {
+                    FieldLoader.ServerLoadStatus.requested = requestedIds.length;
+                })
+            );
             const serializedFields = await DocServer.EmitCallback(_socket, MessageStore.GetRefFields, requestedIds);
 
             // 3) when the serialized RefFields have been received, go head and begin deserializing them into objects.
@@ -423,11 +358,17 @@ export namespace DocServer {
             // future .proto calls on the Doc won't have to go farther than the cache to get their actual value.
             let processed = 0;
             console.log('deserializing ' + serializedFields.length + ' fields');
+            // eslint-disable-next-line no-restricted-syntax
             for (const field of serializedFields) {
                 processed++;
                 if (processed % 150 === 0) {
-                    runInAction(() => (FieldLoader.ServerLoadStatus.retrieved = processed));
-                    await new Promise(res => setTimeout(res)); // force loading to yield to splash screen rendering to update progress
+                    runInAction(() => {
+                        FieldLoader.ServerLoadStatus.retrieved = processed;
+                    });
+                    // eslint-disable-next-line no-await-in-loop
+                    await new Promise(res => {
+                        setTimeout(res);
+                    }); // force loading to yield to splash screen rendering to update progress
                 }
                 const cached = _cache[field.id];
                 if (!cached || (cached instanceof Promise && defaultPromises.some(dp => dp.p === cached))) {
@@ -435,7 +376,7 @@ export namespace DocServer {
                     // adds to a list of promises that will be awaited asynchronously
                     promises.push(
                         (_cache[field.id] = SerializationHelper.Deserialize(field).then(deserialized => {
-                            //overwrite or delete any promises (that we inserted as flags
+                            // overwrite or delete any promises (that we inserted as flags
                             // to indicate that the field was in the process of being fetched). Now everything
                             // should be an actual value within or entirely absent from the cache.
                             if (deserialized !== undefined) {
@@ -455,7 +396,7 @@ export namespace DocServer {
                     // get the resolved field
                 } else if (cached instanceof Promise) {
                     console.log('.');
-                    //promises.push(cached);
+                    // promises.push(cached);
                 } else if (field) {
                     // console.log('-');
                 }
@@ -472,7 +413,7 @@ export namespace DocServer {
         // 6) with this confidence, we can now go through and update the cache at the ids of the fields that
         // we explicitly had to fetch. To finish it off, we add whatever value we've come up with for a given
         // id to the soon-to-be-returned field mapping.
-        //ids.forEach(id => (map[id] = _cache[id] as any));
+        // ids.forEach(id => (map[id] = _cache[id] as any));
 
         // 7) those promises we encountered in the else if of 1), which represent
         // other callers having already submitted a request to the server for (a) document(s)
@@ -481,7 +422,7 @@ export namespace DocServer {
         //
         // fortunately, those other callers will also hit their own version of 6) and clean up
         // the shared cache when these promises resolve, so all we have to do is...
-        //const otherCallersFetching = await Promise.all(promises);
+        // const otherCallersFetching = await Promise.all(promises);
         // ...extract the RefFields returned from the resolution of those promises and add them to our
         // own map.
         // waitingIds.forEach((id, index) => (map[id] = otherCallersFetching[index]));
@@ -506,6 +447,7 @@ export namespace DocServer {
     }
 
     // WRITE A NEW DOCUMENT TO THE SERVER
+    export let CacheNeedsUpdate = false;
 
     /**
      * A wrapper around the function local variable _createField.
@@ -521,11 +463,9 @@ export namespace DocServer {
     function _CreateFieldImpl(field: RefField) {
         _cache[field[Id]] = field;
         const initialState = SerializationHelper.Serialize(field);
-        Doc.CurrentUserEmail !== 'guest' && DocServer.Emit(_socket, MessageStore.CreateField, initialState);
+        ClientUtils.CurrentUserEmail !== 'guest' && DocServer.Emit(_socket, MessageStore.CreateField, initialState);
     }
 
-    let _CreateField: (field: RefField) => void = errorFunc;
-
     // NOTIFY THE SERVER OF AN UPDATE TO A DOC'S STATE
 
     /**
@@ -541,13 +481,11 @@ export namespace DocServer {
     }
 
     function _UpdateFieldImpl(id: string, diff: any) {
-        !DocServer.Control.isReadOnly() && Doc.CurrentUserEmail !== 'guest' && DocServer.Emit(_socket, MessageStore.UpdateField, { id, diff });
+        !DocServer.Control.isReadOnly() && ClientUtils.CurrentUserEmail !== 'guest' && DocServer.Emit(_socket, MessageStore.UpdateField, { id, diff });
     }
 
-    let _UpdateField: (id: string, diff: any) => void = errorFunc;
-
     function _respondToUpdateImpl(diff: any) {
-        const id = diff.id;
+        const { id } = diff;
         // to be valid, the Diff object must reference
         // a document's id
         if (id === undefined) {
@@ -578,11 +516,11 @@ export namespace DocServer {
     }
 
     export function DeleteDocument(id: string) {
-        Doc.CurrentUserEmail !== 'guest' && DocServer.Emit(_socket, MessageStore.DeleteField, id);
+        ClientUtils.CurrentUserEmail !== 'guest' && DocServer.Emit(_socket, MessageStore.DeleteField, id);
     }
 
     export function DeleteDocuments(ids: string[]) {
-        Doc.CurrentUserEmail !== 'guest' && DocServer.Emit(_socket, MessageStore.DeleteFields, ids);
+        ClientUtils.CurrentUserEmail !== 'guest' && DocServer.Emit(_socket, MessageStore.DeleteFields, ids);
     }
 
     function _respondToDeleteImpl(ids: string | string[]) {
@@ -596,7 +534,7 @@ export namespace DocServer {
         }
     }
 
-    let _RespondToUpdate = _respondToUpdateImpl;
+    const _RespondToUpdate = _respondToUpdateImpl;
     const _respondToDelete = _respondToDeleteImpl;
 
     function respondToUpdate(diff: any) {
@@ -606,4 +544,28 @@ export namespace DocServer {
     function respondToDelete(ids: string | string[]) {
         _respondToDelete(ids);
     }
+
+    export function init(protocol: string, hostname: string, port: number, identifier: string) {
+        _cache = {};
+        USER_ID = identifier;
+        _socket = io(`${protocol.startsWith('https') ? 'wss' : 'ws'}://${hostname}:${port}`, { transports: ['websocket'], rejectUnauthorized: false });
+        _socket.on('connect_error', (err: any) => console.log(err));
+        // io.connect(`https://7f079dda.ngrok.io`);// if using ngrok, create a special address for the websocket
+
+        _GetCachedRefField = _GetCachedRefFieldImpl;
+        SetObjGetRefField((_GetRefField = _GetRefFieldImpl));
+        SetObjGetRefFields((_GetRefFields = _GetRefFieldsImpl));
+        _CreateField = _CreateFieldImpl;
+        _UpdateField = _UpdateFieldImpl;
+
+        /**
+         * Whenever the server sends us its handshake message on our
+         * websocket, we use the above function to return the handshake.
+         */
+        DocServer.AddServerHandler(_socket, MessageStore.Foo, onConnection);
+        DocServer.AddServerHandler(_socket, MessageStore.UpdateField, respondToUpdate);
+        DocServer.AddServerHandler(_socket, MessageStore.DeleteField, respondToDelete);
+        DocServer.AddServerHandler(_socket, MessageStore.DeleteFields, respondToDelete);
+        DocServer.AddServerHandler(_socket, MessageStore.ConnectionTerminated, alertUser);
+    }
 }
diff --git a/src/client/Network.ts b/src/client/Network.ts
index cdcd5225a..968c407b2 100644
--- a/src/client/Network.ts
+++ b/src/client/Network.ts
@@ -1,5 +1,6 @@
-import { Utils } from '../Utils';
 import * as requestPromise from 'request-promise';
+import { ClientUtils } from '../ClientUtils';
+import { Utils } from '../Utils';
 import { Upload } from '../server/SharedMediaTypes';
 
 /**
@@ -14,7 +15,7 @@ export namespace Networking {
 
     export async function PostToServer(relativeRoute: string, body?: any) {
         const options = {
-            uri: Utils.prepend(relativeRoute),
+            uri: ClientUtils.prepend(relativeRoute),
             method: 'POST',
             body,
             json: true,
@@ -49,14 +50,14 @@ export namespace Networking {
             }
             const maxFileSize = 50000000;
             if (fileguidpairs.some(f => f.file.size > maxFileSize)) {
-                return new Promise<any>(res =>
+                return new Promise<any>(res => {
                     res([
                         {
                             source: { name: '', type: '', size: 0, toJson: () => ({ name: '', type: '' }) },
                             result: { name: '', message: `max file size (${maxFileSize / 1000000}MB) exceeded` },
                         },
-                    ])
-                );
+                    ]);
+                });
             }
             formData.set('fileguids', fileguidpairs.map(pair => pair.guid).join(';'));
             formData.set('filesize', fileguidpairs.reduce((sum, pair) => sum + pair.file.size, 0).toString());
diff --git a/src/client/apis/google_docs/GooglePhotosClientUtils.ts b/src/client/apis/google_docs/GooglePhotosClientUtils.ts
index e8fd8fb8a..757031fec 100644
--- a/src/client/apis/google_docs/GooglePhotosClientUtils.ts
+++ b/src/client/apis/google_docs/GooglePhotosClientUtils.ts
@@ -1,5 +1,6 @@
 import { AssertionError } from 'assert';
 import { EditorState } from 'prosemirror-state';
+import { ClientUtils } from '../../../ClientUtils';
 import { Doc, DocListCastAsync, Opt } from '../../../fields/Doc';
 import { Id } from '../../../fields/FieldSymbols';
 import { RichTextField } from '../../../fields/RichTextField';
@@ -7,9 +8,8 @@ import { RichTextUtils } from '../../../fields/RichTextUtils';
 import { Cast, StrCast } from '../../../fields/Types';
 import { ImageField } from '../../../fields/URLField';
 import { MediaItem, NewMediaItemResult } from '../../../server/apis/google/SharedTypes';
-import { Utils } from '../../../Utils';
-import { Docs, DocumentOptions, DocUtils } from '../../documents/Documents';
 import { Networking } from '../../Network';
+import { DocUtils, Docs, DocumentOptions } from '../../documents/Documents';
 import { FormattedTextBox } from '../../views/nodes/formattedText/FormattedTextBox';
 import { GoogleAuthenticationManager } from '../GoogleAuthenticationManager';
 import Photos = require('googlephotos');
@@ -111,7 +111,7 @@ export namespace GooglePhotos {
                     await Query.TagChildImages(collection);
                 }
                 collection.albumId = id;
-                Transactions.AddTextEnrichment(collection, `Find me at ${Utils.prepend(`/doc/${collection[Id]}?sharing=true`)}`);
+                Transactions.AddTextEnrichment(collection, `Find me at ${ClientUtils.prepend(`/doc/${collection[Id]}?sharing=true`)}`);
                 return { albumId: id, mediaItems };
             }
         };
@@ -124,7 +124,7 @@ export namespace GooglePhotos {
             await GoogleAuthenticationManager.Instance.fetchOrGenerateAccessToken();
             const response = await Query.ContentSearch(requested);
             const uploads = await Transactions.WriteMediaItemsToServer(response);
-            const children = uploads.map((upload: Transactions.UploadInformation) => Docs.Create.ImageDocument(Utils.fileUrl(upload.fileNames.clean) /*, {"data_contentSize":upload.contentSize}*/));
+            const children = uploads.map((upload: Transactions.UploadInformation) => Docs.Create.ImageDocument(ClientUtils.fileUrl(upload.fileNames.clean) /*, {"data_contentSize":upload.contentSize}*/));
             const options = { _width: 500, _height: 500 };
             return constructor(children, options);
         };
@@ -327,7 +327,7 @@ export namespace GooglePhotos {
         };
 
         const parseDescription = (document: Doc, descriptionKey: string) => {
-            let description: string = Utils.prepend(`/doc/${document[Id]}?sharing=true`);
+            let description: string = ClientUtils.prepend(`/doc/${document[Id]}?sharing=true`);
             const target = document[descriptionKey];
             if (typeof target === 'string') {
                 description = target;
diff --git a/src/client/apis/gpt/GPT.ts b/src/client/apis/gpt/GPT.ts
index fb51278ae..63563cb79 100644
--- a/src/client/apis/gpt/GPT.ts
+++ b/src/client/apis/gpt/GPT.ts
@@ -25,14 +25,18 @@ const callTypeMap: { [type: string]: GPTCallOpts } = {
  * @param inputText Text to process
  * @returns AI Output
  */
+let lastCall = '';
+let lastResp = '';
 const gptAPICall = async (inputText: string, callType: GPTCallType) => {
     if (callType === GPTCallType.SUMMARY) inputText += '.';
     const opts: GPTCallOpts = callTypeMap[callType];
+    if (lastCall === inputText) return lastResp;
     try {
         const configuration: ClientOptions = {
             apiKey: process.env.OPENAI_KEY,
             dangerouslyAllowBrowser: true,
         };
+        lastCall = inputText;
         const openai = new OpenAI(configuration);
         const response = await openai.completions.create({
             model: opts.model,
@@ -40,6 +44,7 @@ const gptAPICall = async (inputText: string, callType: GPTCallType) => {
             temperature: opts.temp,
             prompt: `${opts.prompt}${inputText}`,
         });
+        lastResp = response.choices[0].text;
         return response.choices[0].text;
     } catch (err) {
         console.log(err);
diff --git a/src/client/apis/youtube/YoutubeBox.scss b/src/client/apis/youtube/YoutubeBox.scss
deleted file mode 100644
index eabdbb1ac..000000000
--- a/src/client/apis/youtube/YoutubeBox.scss
+++ /dev/null
@@ -1,126 +0,0 @@
-.youtubeBox-cont {
-    ul {
-        list-style-type: none;
-        padding-inline-start: 10px;
-    }
-
-
-    li {
-        margin: 4px;
-        display: inline-flex;
-    }
-
-    li:hover {
-        cursor: pointer;
-        opacity: 0.8;
-    }
-
-    .search_wrapper {
-        width: 100%;
-        display: inline-flex;
-        height: 175px;
-
-        .video_duration {
-            // margin: 0;
-            // padding: 0;
-            border: 0;
-            background: transparent;
-            display: inline-block;
-            position: relative;
-            bottom: 25px;
-            left: 85%;
-            margin: 4px;
-            color: #FFFFFF;
-            background-color: rgba(0, 0, 0, 0.80);
-            padding: 2px 4px;
-            border-radius: 2px;
-            letter-spacing: .5px;
-            font-size: 1.2rem;
-            font-weight: 500;
-            line-height: 1.2rem;
-
-        }
-
-        .textual_info {
-            font-family: Arial, Helvetica, sans-serif;
-
-            .videoTitle {
-                margin-left: 4px;
-                // display: inline-block;
-                color: #0D0D0D;
-                -webkit-line-clamp: 2;
-                display: block;
-                max-height: 4.8rem;
-                overflow: hidden;
-                font-size: 1.8rem;
-                font-weight: 400;
-                line-height: 2.4rem;
-                -webkit-box-orient: vertical;
-                text-overflow: ellipsis;
-                white-space: normal;
-                display: -webkit-box;
-            }
-
-            .channelName {
-                color: #606060;
-                margin-left: 4px;
-                font-size: 1.3rem;
-                font-weight: 400;
-                line-height: 1.8rem;
-                text-transform: none;
-                margin-top: 0px;
-                display: inline-block;
-            }
-
-            .video_description {
-                margin-left: 4px;
-                // font-size: 12px;
-                color: #606060;
-                padding-top: 8px;
-                margin-bottom: 8px;
-                display: block;
-                line-height: 1.8rem;
-                max-height: 4.2rem;
-                overflow: hidden;
-                font-size: 1.3rem;
-                font-weight: 400;
-                text-transform: none;
-            }
-
-            .publish_time {
-                //display: inline-block;
-                margin-left: 8px;
-                padding: 0;
-                border: 0;
-                background: transparent;
-                color: #606060;
-                max-width: 100%;
-                line-height: 1.8rem;
-                max-height: 3.6rem;
-                overflow: hidden;
-                font-size: 1.3rem;
-                font-weight: 400;
-                text-transform: none;
-            }
-
-            .viewCount {
-
-                margin-left: 8px;
-                padding: 0;
-                border: 0;
-                background: transparent;
-                color: #606060;
-                max-width: 100%;
-                line-height: 1.8rem;
-                max-height: 3.6rem;
-                overflow: hidden;
-                font-size: 1.3rem;
-                font-weight: 400;
-                text-transform: none;
-            }
-
-
-
-        }
-    }
-}
\ No newline at end of file
diff --git a/src/client/apis/youtube/YoutubeBox.tsx b/src/client/apis/youtube/YoutubeBox.tsx
deleted file mode 100644
index d3a15cd84..000000000
--- a/src/client/apis/youtube/YoutubeBox.tsx
+++ /dev/null
@@ -1,369 +0,0 @@
-import { action, observable, runInAction } from 'mobx';
-import { observer } from 'mobx-react';
-import { Doc, DocListCastAsync } from '../../../fields/Doc';
-import { InkTool } from '../../../fields/InkField';
-import { Cast, NumCast, StrCast } from '../../../fields/Types';
-import { Utils } from '../../../Utils';
-import { DocServer } from '../../DocServer';
-import { Docs } from '../../documents/Documents';
-import { DocumentView } from '../../views/nodes/DocumentView';
-import { FieldView, FieldViewProps } from '../../views/nodes/FieldView';
-import '../../views/nodes/WebBox.scss';
-import './YoutubeBox.scss';
-import * as React from 'react';
-import { SnappingManager } from '../../util/SnappingManager';
-
-interface VideoTemplate {
-    thumbnailUrl: string;
-    videoTitle: string;
-    videoId: string;
-    duration: string;
-    channelTitle: string;
-    viewCount: string;
-    publishDate: string;
-    videoDescription: string;
-}
-
-/**
- * This class models the youtube search document that can be dropped on to canvas.
- */
-@observer
-export class YoutubeBox extends React.Component<FieldViewProps> {
-    @observable YoutubeSearchElement: HTMLInputElement | undefined;
-    @observable searchResultsFound: boolean = false;
-    @observable searchResults: any[] = [];
-    @observable videoClicked: boolean = false;
-    @observable selectedVideoUrl: string = '';
-    @observable lisOfBackUp: JSX.Element[] = [];
-    @observable videoIds: string | undefined;
-    @observable videoDetails: any[] = [];
-    @observable curVideoTemplates: VideoTemplate[] = [];
-
-    public static LayoutString(fieldKey: string) {
-        return FieldView.LayoutString(YoutubeBox, fieldKey);
-    }
-
-    /**
-     * When component mounts, last search's results are laoded in based on the back up stored
-     * in the document of the props.
-     */
-    async componentDidMount() {
-        //DocServer.getYoutubeChannels();
-        const castedSearchBackUp = Cast(this.props.Document.cachedSearchResults, Doc);
-        const awaitedBackUp = await castedSearchBackUp;
-        const castedDetailBackUp = Cast(this.props.Document.cachedDetails, Doc);
-        const awaitedDetails = await castedDetailBackUp;
-
-        if (awaitedBackUp) {
-            const jsonList = await DocListCastAsync(awaitedBackUp.json);
-            const jsonDetailList = await DocListCastAsync(awaitedDetails!.json);
-
-            if (jsonList!.length !== 0) {
-                runInAction(() => (this.searchResultsFound = true));
-                let index = 0;
-                //getting the necessary information from backUps and building templates that will be used to map in render
-                for (const video of jsonList!) {
-                    const videoId = await Cast(video.id, Doc);
-                    const id = StrCast(videoId!.videoId);
-                    const snippet = await Cast(video.snippet, Doc);
-                    const videoTitle = this.filterYoutubeTitleResult(StrCast(snippet!.title));
-                    const thumbnail = await Cast(snippet!.thumbnails, Doc);
-                    const thumbnailMedium = await Cast(thumbnail!.medium, Doc);
-                    const thumbnailUrl = StrCast(thumbnailMedium!.url);
-                    const videoDescription = StrCast(snippet!.description);
-                    const pusblishDate = this.roundPublishTime(StrCast(snippet!.publishedAt))!;
-                    const channelTitle = StrCast(snippet!.channelTitle);
-                    let duration: string = '';
-                    let viewCount: string = '';
-                    if (jsonDetailList!.length !== 0) {
-                        const contentDetails = await Cast(jsonDetailList![index].contentDetails, Doc);
-                        const statistics = await Cast(jsonDetailList![index].statistics, Doc);
-                        duration = this.convertIsoTimeToDuration(StrCast(contentDetails!.duration));
-                        viewCount = this.abbreviateViewCount(parseInt(StrCast(statistics!.viewCount)))!;
-                    }
-                    index = index + 1;
-                    const newTemplate: VideoTemplate = {
-                        videoId: id,
-                        videoTitle: videoTitle,
-                        thumbnailUrl: thumbnailUrl,
-                        publishDate: pusblishDate,
-                        channelTitle: channelTitle,
-                        videoDescription: videoDescription,
-                        duration: duration,
-                        viewCount: viewCount,
-                    };
-                    runInAction(() => this.curVideoTemplates.push(newTemplate));
-                }
-            }
-        }
-    }
-
-    _ignore = 0;
-    onPreWheel = (e: React.WheelEvent) => {
-        this._ignore = e.timeStamp;
-    };
-    onPrePointer = (e: React.PointerEvent) => {
-        this._ignore = e.timeStamp;
-    };
-    onPostPointer = (e: React.PointerEvent) => {
-        if (this._ignore !== e.timeStamp) {
-            e.stopPropagation();
-        }
-    };
-    onPostWheel = (e: React.WheelEvent) => {
-        if (this._ignore !== e.timeStamp) {
-            e.stopPropagation();
-        }
-    };
-
-    /**
-     * Function that submits the title entered by user on enter press.
-     */
-    onEnterKeyDown = (e: React.KeyboardEvent) => {
-        if (e.keyCode === 13) {
-            const submittedTitle = this.YoutubeSearchElement!.value;
-            this.YoutubeSearchElement!.value = '';
-            this.YoutubeSearchElement!.blur();
-            DocServer.getYoutubeVideos(submittedTitle, this.processesVideoResults);
-        }
-    };
-
-    /**
-     * The callback that is passed in to server, which functions as a way to
-     * get videos that is returned by search. It also makes a call to server
-     * to get details for the videos found.
-     */
-    @action
-    processesVideoResults = (videos: any[]) => {
-        this.searchResults = videos;
-        if (this.searchResults.length > 0) {
-            this.searchResultsFound = true;
-            this.videoIds = '';
-            videos.forEach(video => {
-                if (this.videoIds === '') {
-                    this.videoIds = video.id.videoId;
-                } else {
-                    this.videoIds = this.videoIds! + ', ' + video.id.videoId;
-                }
-            });
-            //Asking for details that include duration and viewCount from server for videoIds
-            DocServer.getYoutubeVideoDetails(this.videoIds, this.processVideoDetails);
-            this.backUpSearchResults(videos);
-            if (this.videoClicked) {
-                this.videoClicked = false;
-            }
-        }
-    };
-
-    /**
-     * The callback that is given to server to process and receive returned details about the videos.
-     */
-    @action
-    processVideoDetails = (videoDetails: any[]) => {
-        this.videoDetails = videoDetails;
-        this.props.Document.cachedDetails = Doc.Get.FromJson({ data: videoDetails, title: 'detailBackUp' });
-    };
-
-    /**
-     * The function that stores the search results in the props document.
-     */
-    backUpSearchResults = (videos: any[]) => {
-        this.props.Document.cachedSearchResults = Doc.Get.FromJson({ data: videos, title: 'videosBackUp' });
-    };
-
-    /**
-     * The function that filters out escaped characters returned by the api
-     * in the title of the videos.
-     */
-    filterYoutubeTitleResult = (resultTitle: string) => {
-        let processedTitle: string = resultTitle.replace(/&amp;/g, '&'); //.ReplaceAll("&amp;", "&");
-        processedTitle = processedTitle.replace(/"&#39;/g, "'");
-        processedTitle = processedTitle.replace(/&quot;/g, '"');
-        return processedTitle;
-    };
-
-    /**
-     * The function that converts ISO date, which is passed in, to normal date and finds the
-     * difference between today's date and that date, in terms of "ago" to imitate youtube.
-     */
-    roundPublishTime = (publishTime: string) => {
-        const date = new Date(publishTime).getTime();
-        const curDate = new Date().getTime();
-        const timeDif = curDate - date;
-        const totalSeconds = timeDif / 1000;
-        const totalMin = totalSeconds / 60;
-        const totalHours = totalMin / 60;
-        const totalDays = totalHours / 24;
-        const totalMonths = totalDays / 30.417;
-        const totalYears = totalMonths / 12;
-
-        const truncYears = Math.trunc(totalYears);
-        const truncMonths = Math.trunc(totalMonths);
-        const truncDays = Math.trunc(totalDays);
-        const truncHours = Math.trunc(totalHours);
-        const truncMin = Math.trunc(totalMin);
-        const truncSec = Math.trunc(totalSeconds);
-
-        let pluralCase = '';
-
-        if (truncYears !== 0) {
-            truncYears > 1 ? (pluralCase = 's') : (pluralCase = '');
-            return truncYears + ' year' + pluralCase + ' ago';
-        } else if (truncMonths !== 0) {
-            truncMonths > 1 ? (pluralCase = 's') : (pluralCase = '');
-            return truncMonths + ' month' + pluralCase + ' ago';
-        } else if (truncDays !== 0) {
-            truncDays > 1 ? (pluralCase = 's') : (pluralCase = '');
-            return truncDays + ' day' + pluralCase + ' ago';
-        } else if (truncHours !== 0) {
-            truncHours > 1 ? (pluralCase = 's') : (pluralCase = '');
-            return truncHours + ' hour' + pluralCase + ' ago';
-        } else if (truncMin !== 0) {
-            truncMin > 1 ? (pluralCase = 's') : (pluralCase = '');
-            return truncMin + ' minute' + pluralCase + ' ago';
-        } else if (truncSec !== 0) {
-            truncSec > 1 ? (pluralCase = 's') : (pluralCase = '');
-            return truncSec + ' second' + pluralCase + ' ago';
-        }
-    };
-
-    /**
-     * The function that converts the passed in ISO time to normal duration time.
-     */
-    convertIsoTimeToDuration = (isoDur: string) => {
-        const convertedTime = isoDur.replace(/D|H|M/g, ':').replace(/P|T|S/g, '').split(':');
-
-        if (1 === convertedTime.length) {
-            2 !== convertedTime[0].length && (convertedTime[0] = '0' + convertedTime[0]), (convertedTime[0] = '0:' + convertedTime[0]);
-        } else {
-            for (var r = 1, l = convertedTime.length - 1; l >= r; r++) {
-                2 !== convertedTime[r].length && (convertedTime[r] = '0' + convertedTime[r]);
-            }
-        }
-
-        return convertedTime.join(':');
-    };
-
-    /**
-     * The function that rounds the viewCount to the nearest
-     * thousand, million or billion, given a viewCount number.
-     */
-    abbreviateViewCount = (viewCount: number) => {
-        if (viewCount < 1000) {
-            return viewCount.toString();
-        } else if (viewCount >= 1000 && viewCount < 1000000) {
-            return Math.trunc(viewCount / 1000) + 'K';
-        } else if (viewCount >= 1000000 && viewCount < 1000000000) {
-            return Math.trunc(viewCount / 1000000) + 'M';
-        } else if (viewCount >= 1000000000) {
-            return Math.trunc(viewCount / 1000000000) + 'B';
-        }
-    };
-
-    /**
-     * The function that is called to decide on what'll be rendered by the component.
-     * It renders search Results if found. If user didn't do a new search, it renders from the videoTemplates
-     * generated by the backUps. If none present, renders nothing.
-     */
-    renderSearchResultsOrVideo = () => {
-        if (this.searchResultsFound) {
-            if (this.searchResults.length !== 0) {
-                return (
-                    <ul>
-                        {this.searchResults.map((video, index) => {
-                            const filteredTitle = this.filterYoutubeTitleResult(video.snippet.title);
-                            const channelTitle = video.snippet.channelTitle;
-                            const videoDescription = video.snippet.description;
-                            const pusblishDate = this.roundPublishTime(video.snippet.publishedAt);
-                            let duration;
-                            let viewCount;
-                            if (this.videoDetails.length !== 0) {
-                                duration = this.convertIsoTimeToDuration(this.videoDetails[index].contentDetails.duration);
-                                viewCount = this.abbreviateViewCount(this.videoDetails[index].statistics.viewCount);
-                            }
-
-                            return (
-                                <li onClick={() => this.embedVideoOnClick(video.id.videoId, filteredTitle)} key={Utils.GenerateGuid()}>
-                                    <div className="search_wrapper">
-                                        <div style={{ backgroundColor: 'yellow' }}>
-                                            <img src={video.snippet.thumbnails.medium.url} />
-                                            <span className="video_duration">{duration}</span>
-                                        </div>
-                                        <div className="textual_info">
-                                            <span className="videoTitle">{filteredTitle}</span>
-                                            <span className="channelName">{channelTitle}</span>
-                                            <span className="viewCount">{viewCount}</span>
-                                            <span className="publish_time">{pusblishDate}</span>
-                                            <p className="video_description">{videoDescription}</p>
-                                        </div>
-                                    </div>
-                                </li>
-                            );
-                        })}
-                    </ul>
-                );
-            } else if (this.curVideoTemplates.length !== 0) {
-                return (
-                    <ul>
-                        {this.curVideoTemplates.map((video: VideoTemplate) => {
-                            return (
-                                <li onClick={() => this.embedVideoOnClick(video.videoId, video.videoTitle)} key={Utils.GenerateGuid()}>
-                                    <div className="search_wrapper">
-                                        <div style={{ backgroundColor: 'yellow' }}>
-                                            <img src={video.thumbnailUrl} />
-                                            <span className="video_duration">{video.duration}</span>
-                                        </div>
-                                        <div className="textual_info">
-                                            <span className="videoTitle">{video.videoTitle}</span>
-                                            <span className="channelName">{video.channelTitle}</span>
-                                            <span className="viewCount">{video.viewCount}</span>
-                                            <span className="publish_time">{video.publishDate}</span>
-                                            <p className="video_description">{video.videoDescription}</p>
-                                        </div>
-                                    </div>
-                                </li>
-                            );
-                        })}
-                    </ul>
-                );
-            }
-        } else {
-            return null;
-        }
-    };
-
-    /**
-     * Given a videoId and title, creates a new youtube embedded url, and uses that
-     * to create a new video document.
-     */
-    @action
-    embedVideoOnClick = (videoId: string, filteredTitle: string) => {
-        const embeddedUrl = 'https://www.youtube.com/embed/' + videoId;
-        this.selectedVideoUrl = embeddedUrl;
-        const addFunction = this.props.addDocument!;
-        const newVideoX = NumCast(this.props.Document.x);
-        const newVideoY = NumCast(this.props.Document.y) + NumCast(this.props.Document.height);
-
-        addFunction(Docs.Create.VideoDocument(embeddedUrl, { title: filteredTitle, _width: 400, _height: 315, x: newVideoX, y: newVideoY }));
-        this.videoClicked = true;
-    };
-
-    render() {
-        const content = (
-            <div className="youtubeBox-cont" style={{ width: '100%', height: '100%', position: 'absolute' }} onWheel={this.onPostWheel} onPointerDown={this.onPostPointer} onPointerMove={this.onPostPointer} onPointerUp={this.onPostPointer}>
-                <input type="text" placeholder="Search for a video" onKeyDown={this.onEnterKeyDown} style={{ height: 40, width: '100%', border: '1px solid black', padding: 5, textAlign: 'center' }} ref={e => (this.YoutubeSearchElement = e!)} />
-                {this.renderSearchResultsOrVideo()}
-            </div>
-        );
-
-        const frozen = !this.props.isSelected() || SnappingManager.IsResizing;
-
-        const classname = 'webBox-cont' + (this.props.isSelected() && Doc.ActiveTool === InkTool.None && !SnappingManager.IsResizing ? '-interactive' : '');
-        return (
-            <>
-                <div className={classname}>{content}</div>
-                {!frozen ? null : <div className="webBox-overlay" onWheel={this.onPreWheel} onPointerDown={this.onPrePointer} onPointerMove={this.onPrePointer} onPointerUp={this.onPrePointer} />}
-            </>
-        );
-    }
-}
diff --git a/src/client/cognitive_services/CognitiveServices.ts b/src/client/cognitive_services/CognitiveServices.ts
index 408903324..4ad03aab4 100644
--- a/src/client/cognitive_services/CognitiveServices.ts
+++ b/src/client/cognitive_services/CognitiveServices.ts
@@ -1,18 +1,16 @@
-import * as request from 'request-promise';
-import { Doc, Field } from '../../fields/Doc';
-import { Cast } from '../../fields/Types';
-import { Utils } from '../../Utils';
+import * as rp from 'request-promise';
+import { Doc, FieldType } from '../../fields/Doc';
 import { InkData } from '../../fields/InkField';
-import { UndoManager } from '../util/UndoManager';
-import requestPromise = require('request-promise');
 import { List } from '../../fields/List';
+import { Cast } from '../../fields/Types';
+import { UndoManager } from '../util/UndoManager';
 
 type APIManager<D> = { converter: BodyConverter<D>; requester: RequestExecutor };
 type RequestExecutor = (apiKey: string, body: string, service: Service) => Promise<string>;
 type AnalysisApplier<D> = (target: Doc, relevantKeys: string[], data: D, ...args: any) => any;
 type BodyConverter<D> = (data: D) => string;
-type Converter = (results: any) => Field;
-type TextConverter = (results: any, data: string) => Promise<{ keyterms: Field; external_recommendations: any; kp_string: string[] }>;
+type Converter = (results: any) => FieldType;
+type TextConverter = (results: any, data: string) => Promise<{ keyterms: FieldType; external_recommendations: any; kp_string: string[] }>;
 type BingConverter = (results: any) => Promise<{ title_vals: string[]; url_vals: string[] }>;
 
 export type Tag = { name: string; confidence: number };
@@ -95,7 +93,7 @@ export namespace CognitiveServices {
                     },
                 };
 
-                return request.post(options);
+                return rp.post(options);
             },
         };
 
@@ -344,10 +342,10 @@ export namespace CognitiveServices {
         export namespace Appliers {
             export async function vectorize(keyterms: any, dataDoc: Doc, mainDoc: boolean = false) {
                 console.log('vectorizing...');
-                //keyterms = ["father", "king"];
+                // keyterms = ["father", "king"];
 
-                const args = { method: 'POST', uri: Utils.prepend('/recommender'), body: { keyphrases: keyterms }, json: true };
-                await requestPromise.post(args).then(async wordvecs => {
+                const args = { method: 'POST', uri: ClientUtils.prepend('/recommender'), body: { keyphrases: keyterms }, json: true };
+                await rp.post(args).then(async wordvecs => {
                     if (wordvecs) {
                         const indices = Object.keys(wordvecs);
                         console.log('successful vectorization!');
@@ -355,7 +353,7 @@ export namespace CognitiveServices {
                         indices.forEach((ind: any) => {
                             vectorValues.push(wordvecs[ind]);
                         });
-                        //ClientRecommender.Instance.processVector(vectorValues, dataDoc, mainDoc);
+                        // ClientRecommender.Instance.processVector(vectorValues, dataDoc, mainDoc);
                     } // adds document to internal doc set
                     else {
                         console.log('unsuccessful :( word(s) not in vocabulary');
diff --git a/src/client/documents/DocumentTypes.ts b/src/client/documents/DocumentTypes.ts
index 1123bcac9..4e3496608 100644
--- a/src/client/documents/DocumentTypes.ts
+++ b/src/client/documents/DocumentTypes.ts
@@ -25,14 +25,13 @@ export enum DocumentType {
     MAP = 'map',
     DATAVIZ = 'dataviz',
     LOADING = 'loading',
-    SIMULATION = 'simulation', //physics simulation
+    SIMULATION = 'simulation', // physics simulation
 
     // special purpose wrappers that either take no data or are compositions of lower level types
     LINK = 'link',
     IMPORT = 'import',
     PRES = 'presentation',
     PRESELEMENT = 'preselement',
-    YOUTUBE = 'youtube',
     COMPARISON = 'comparison',
     GROUP = 'group',
     PUSHPIN = 'pushpin',
@@ -56,11 +55,10 @@ export enum CollectionViewType {
     Carousel = 'carousel',
     Carousel3D = '3D Carousel',
     Linear = 'linear',
-    //Staff = "staff",
     Map = 'map',
     Grid = 'grid',
     Pile = 'pileup',
     StackedTimeline = 'stacked timeline',
     NoteTaking = 'notetaking',
-    Calendar = 'calendar'
+    Calendar = 'calendar',
 }
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index b160379df..d41a96db2 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -1,28 +1,35 @@
 import { IconProp } from '@fortawesome/fontawesome-svg-core';
+import { saveAs } from 'file-saver';
+import * as JSZip from 'jszip';
 import { action, reaction, runInAction } from 'mobx';
 import { basename } from 'path';
-import { OmitKeys, Utils } from '../../Utils';
+import { ClientUtils, OmitKeys } from '../../ClientUtils';
+import * as JSZipUtils from '../../JSZipUtils';
+import { decycle } from '../../decycler/decycler';
 import { DateField } from '../../fields/DateField';
-import { Doc, DocListCast, Field, LinkedTo, Opt, StrListCast, updateCachedAcls } from '../../fields/Doc';
+import { Doc, DocListCast, Field, FieldType, LinkedTo, Opt, StrListCast, updateCachedAcls } from '../../fields/Doc';
 import { DocData, Initializing } from '../../fields/DocSymbols';
 import { Id } from '../../fields/FieldSymbols';
 import { HtmlField } from '../../fields/HtmlField';
-import { InkField, PointData } from '../../fields/InkField';
-import { List } from '../../fields/List';
+import { InkDataFieldName, InkField } from '../../fields/InkField';
+import { List, ListFieldName } from '../../fields/List';
+import { ProxyField } from '../../fields/Proxy';
 import { RichTextField } from '../../fields/RichTextField';
 import { SchemaHeaderField } from '../../fields/SchemaHeaderField';
 import { ComputedField, ScriptField } from '../../fields/ScriptField';
 import { BoolCast, Cast, DocCast, FieldValue, NumCast, ScriptCast, StrCast } from '../../fields/Types';
-import { AudioField, CsvField, ImageField, PdfField, VideoField, WebField, YoutubeField } from '../../fields/URLField';
+import { AudioField, CsvField, ImageField, PdfField, VideoField, WebField } from '../../fields/URLField';
 import { SharingPermissions, inheritParentAcls } from '../../fields/util';
+import { PointData } from '../../pen-gestures/GestureTypes';
 import { Upload } from '../../server/SharedMediaTypes';
 import { DocServer } from '../DocServer';
 import { Networking } from '../Network';
-import { YoutubeBox } from '../apis/youtube/YoutubeBox';
-import { DragManager, dropActionType } from '../util/DragManager';
+import { DragManager } from '../util/DragManager';
+import { dropActionType } from '../util/DropActionTypes';
 import { FollowLinkScript } from '../util/LinkFollower';
 import { LinkManager } from '../util/LinkManager';
 import { ScriptingGlobals } from '../util/ScriptingGlobals';
+import { SerializationHelper } from '../util/SerializationHelper';
 import { UndoManager, undoable } from '../util/UndoManager';
 import { ContextMenu } from '../views/ContextMenu';
 import { ContextMenuProps } from '../views/ContextMenuItem';
@@ -30,7 +37,7 @@ import { ActiveArrowEnd, ActiveArrowStart, ActiveDash, ActiveFillColor, ActiveIn
 import { CollectionDockingView } from '../views/collections/CollectionDockingView';
 import { CollectionView } from '../views/collections/CollectionView';
 import { DimUnit } from '../views/collections/collectionMulticolumn/CollectionMulticolumnView';
-import { AudioBox, media_state } from '../views/nodes/AudioBox';
+import { AudioBox, mediaState } from '../views/nodes/AudioBox';
 import { ComparisonBox } from '../views/nodes/ComparisonBox';
 import { DataVizBox } from '../views/nodes/DataVizBox/DataVizBox';
 import { OpenWhere } from '../views/nodes/DocumentView';
@@ -60,6 +67,7 @@ import { PresBox } from '../views/nodes/trails/PresBox';
 import { PresElementBox } from '../views/nodes/trails/PresElementBox';
 import { SearchBox } from '../views/search/SearchBox';
 import { CollectionViewType, DocumentType } from './DocumentTypes';
+
 const { DFLT_IMAGE_NATIVE_DIM } = require('../views/global/globalCssVariables.module.scss'); // prettier-ignore
 
 const defaultNativeImageDim = Number(DFLT_IMAGE_NATIVE_DIM.replace('px', ''));
@@ -83,7 +91,7 @@ export class FInfo {
     description: string = '';
     readOnly: boolean = false;
     fieldType?: FInfoFieldType;
-    values?: Field[];
+    values?: FieldType[];
 
     filterable?: boolean = true; // can be used as a Filter in FilterPanel
     // format?: string; // format to display values (e.g, decimal places, $, etc)
@@ -252,8 +260,8 @@ export class DocumentOptions {
     opacity?: NUMt = new NumInfo('document opacity', false);
     viewTransitionTime?: NUMt = new NumInfo('transition duration for view parameters', false);
     dontRegisterView?: BOOLt = new BoolInfo('are views of this document registered so that they can be found when following links, etc', false);
-    _undoIgnoreFields?: List<string>; //'fields that should not be added to the undo stack (opacity for Undo/Redo/and sidebar) AND whether modifications to document are undoable (true for linearview menu buttons to prevent open/close from entering undo stack)'
-    undoIgnoreFields?: List<string>; //'fields that should not be added to the undo stack (opacity for Undo/Redo/and sidebar) AND whether modifications to document are undoable (true for linearview menu buttons to prevent open/close from entering undo stack)'
+    _undoIgnoreFields?: List<string>; // 'fields that should not be added to the undo stack (opacity for Undo/Redo/and sidebar) AND whether modifications to document are undoable (true for linearview menu buttons to prevent open/close from entering undo stack)'
+    undoIgnoreFields?: List<string>; // 'fields that should not be added to the undo stack (opacity for Undo/Redo/and sidebar) AND whether modifications to document are undoable (true for linearview menu buttons to prevent open/close from entering undo stack)'
     _header_height?: NUMt = new NumInfo('height of document header used for displaying title', false);
     _header_fontSize?: NUMt = new NumInfo('font size of header of custom notes', false);
     _header_pointerEvents?: PEVt = new PEInfo('types of events the header of a custom text document can consume');
@@ -321,7 +329,7 @@ export class DocumentOptions {
     _label_maxFontSize?: NUMt = new NumInfo('maximum font size for labelBoxes', false);
     stroke_width?: NUMt = new NumInfo('width of an ink stroke', false);
     stroke_showLabel?: BOOLt = new BoolInfo('show label inside of stroke');
-    mediaState?: STRt = new StrInfo(`status of audio/video media document: ${media_state.PendingRecording}, ${media_state.Recording}, ${media_state.Paused}, ${media_state.Playing}`, false);
+    mediaState?: STRt = new StrInfo(`status of audio/video media document: ${mediaState.PendingRecording}, ${mediaState.Recording}, ${mediaState.Paused}, ${mediaState.Playing}`, false);
     recording?: BOOLt = new BoolInfo('whether WebCam is recording or not');
     slides?: DOCt = new DocInfo('presentation slide associated with video recording (bcz: should be renamed!!)');
     autoPlayAnchors?: BOOLt = new BoolInfo('whether to play audio/video when an anchor is clicked in a stackedTimeline.');
@@ -401,7 +409,7 @@ export class DocumentOptions {
     _freeform_noZoom?: BOOLt = new BoolInfo('disables zooming (used by Pile docs)');
     _freeform_fitContentsToBox?: BOOLt = new BoolInfo('whether a freeformview should zoom/scale to create a shrinkwrapped view of its content');
 
-    //BUTTONS
+    // BUTTONS
     buttonText?: string;
     btnType?: string;
     btnList?: List<string>;
@@ -413,7 +421,7 @@ export class DocumentOptions {
     switchToggle?: boolean;
     badgeValue?: ScriptField;
 
-    //LINEAR VIEW
+    // LINEAR VIEW
     linearView_IsOpen?: BOOLt = new BoolInfo('is linear view open');
     linearView_Expandable?: BOOLt = new BoolInfo('can linear view be expanded');
     linearView_Dropdown?: BOOLt = new BoolInfo('can linear view be opened as a dropdown');
@@ -431,7 +439,7 @@ export class DocumentOptions {
     link_anchor_1_useSmallAnchor?: BOOLt = new BoolInfo('whether link_anchor_1 of a link should use a miniature anchor dot (as when the anchor is a text selection)');
     link_anchor_2_useSmallAnchor?: BOOLt = new BoolInfo('whether link_anchor_1 of a link should use a miniature anchor dot (as when the anchor is a text selection)');
     link_relationshipList?: List<string>; // for storing different link relationships (when set by user in the link editor)
-    link_relationshipSizes?: List<number>; //stores number of links contained in each relationship
+    link_relationshipSizes?: List<number>; // stores number of links contained in each relationship
     link_colorList?: List<string>; // colors of links corresponding to specific link relationships
     followLinkZoom?: BOOLt = new BoolInfo('whether to zoom to the target of a link');
     followLinkToggle?: BOOLt = new BoolInfo('whether target of link should be toggled on and off when following a link to it');
@@ -463,7 +471,7 @@ export class DocumentOptions {
     dragFactory_count?: NUMt = new NumInfo('number of items created from a drag button (used for setting title with incrementing index)', false, true);
     dragFactory?: DOCt = new DocInfo('document to create when dragging with a suitable onDragStart script', false);
     clickFactory?: DOCt = new DocInfo('document to create when clicking on a button with a suitable onClick script', false);
-    onDragStart?: ScriptField; //script to execute at start of drag operation --  e.g., when a "creator" button is dragged this script generates a different document to drop
+    onDragStart?: ScriptField; // script to execute at start of drag operation --  e.g., when a "creator" button is dragged this script generates a different document to drop
     target?: Doc; // available for use in scripts.  used to provide a document parameter to the script (Note, this is a convenience entry since any field could be used for parameterizing a script)
     tags?: LISTt = new ListInfo('hashtags added to document, typically using a text view', true);
     treeView_HideTitle?: BOOLt = new BoolInfo('whether to hide the top document title of a tree view');
@@ -502,8 +510,6 @@ export class DocumentOptions {
 
 export const DocOptions = new DocumentOptions();
 export namespace Docs {
-    export let newAccount: boolean = false;
-
     export namespace Prototypes {
         type LayoutSource = { LayoutString: (key: string) => string };
         type PrototypeTemplate = {
@@ -646,12 +652,6 @@ export namespace Docs {
                     options: { systemIcon: 'BsFileEarmarkCodeFill' },
                 },
             ],
-            [
-                DocumentType.YOUTUBE,
-                {
-                    layout: { view: YoutubeBox, dataField: defaultDataKey },
-                },
-            ],
             [
                 DocumentType.LABEL,
                 {
@@ -670,7 +670,7 @@ export namespace Docs {
                         layout_nativeDimEditable: true,
                         layout_hideDecorationTitle: true,
                         systemIcon: 'BsCalculatorFill',
-                    }, ///systemIcon: 'BsSuperscript' + BsSubscript
+                    }, // systemIcon: 'BsSuperscript' + BsSubscript
                 },
             ],
             [
@@ -850,7 +850,7 @@ export namespace Docs {
             // fetch the actual prototype documents from the server
             const actualProtos = await DocServer.GetRefFields(prototypeIds);
             // update this object to include any default values: DocumentOptions for all prototypes
-            prototypeIds.map(id => {
+            prototypeIds.forEach(id => {
                 const existing = actualProtos[id] as Doc;
                 const type = id.replace(suffix, '') as DocumentType;
                 // get or create prototype of the specified type...
@@ -908,7 +908,7 @@ export namespace Docs {
             if (!template) {
                 return undefined;
             }
-            const layout = template.layout;
+            const { layout } = template;
             // create title
             const upper = suffix.toUpperCase();
             const title = prototypeId.toUpperCase().replace(upper, `_${upper}`);
@@ -928,7 +928,7 @@ export namespace Docs {
             };
             Object.entries(options)
                 .filter(pair => typeof pair[1] === 'string' && pair[1].startsWith('@'))
-                .map(pair => {
+                .forEach(pair => {
                     if (!existing || ScriptCast(existing[pair[0]])?.script.originalScript !== pair[1].substring(1)) {
                         (options as any)[pair[0]] = ComputedField.MakeFunction(pair[1].substring(1));
                     }
@@ -960,7 +960,9 @@ export namespace Docs {
          * only when creating a DockDocument from the current user's already existing
          * main document.
          */
-        function InstanceFromProto(proto: Doc, data: Field | undefined, options: DocumentOptions, delegId?: string, fieldKey: string = 'data', protoId?: string, placeholderDoc?: Doc, noView?: boolean) {
+        // eslint-disable-next-line default-param-last
+        function InstanceFromProto(proto: Doc, data: FieldType | undefined, options: DocumentOptions, delegId?: string, fieldKey: string = 'data', protoId?: string, placeholderDocIn?: Doc, noView?: boolean) {
+            const placeholderDoc = placeholderDocIn;
             const viewKeys = ['x', 'y', 'isSystem']; // keys that should be addded to the view document even though they don't begin with an "_"
             const { omit: dataProps, extract: viewProps } = OmitKeys(options, viewKeys, '^_');
 
@@ -968,7 +970,7 @@ export namespace Docs {
             dataProps['acl-Guest'] = options['acl-Guest'] ?? (Doc.defaultAclPrivate ? SharingPermissions.None : SharingPermissions.View);
             dataProps.isSystem = viewProps.isSystem;
             dataProps.isDataDoc = true;
-            dataProps.author = Doc.CurrentUserEmail;
+            dataProps.author = ClientUtils.CurrentUserEmail;
             dataProps.author_date = new DateField();
             if (fieldKey) {
                 dataProps[`${fieldKey}_modificationDate`] = new DateField();
@@ -988,7 +990,7 @@ export namespace Docs {
             }
 
             if (!noView) {
-                const viewFirstProps: { [id: string]: any } = { author: Doc.CurrentUserEmail };
+                const viewFirstProps: { [id: string]: any } = { author: ClientUtils.CurrentUserEmail };
                 viewFirstProps['acl-Guest'] = options['_acl-Guest'] ?? (Doc.defaultAclPrivate ? SharingPermissions.None : SharingPermissions.View);
                 let viewDoc: Doc;
                 // determines whether viewDoc should be created using placeholder Doc or default
@@ -998,7 +1000,9 @@ export namespace Docs {
                     viewDoc = Doc.assign(placeholderDoc, viewFirstProps, true, true);
                     Array.from(Object.keys(placeholderDoc))
                         .filter(key => key.startsWith('acl'))
-                        .forEach(key => (dataDoc[key] = viewDoc[key] = placeholderDoc[key]));
+                        .forEach(key => {
+                            dataDoc[key] = viewDoc[key] = placeholderDoc[key];
+                        });
                 } else {
                     viewDoc = Doc.assign(Doc.MakeDelegate(dataDoc, delegId), viewFirstProps, true, true);
                 }
@@ -1017,6 +1021,7 @@ export namespace Docs {
             return dataDoc;
         }
 
+        // eslint-disable-next-line default-param-last
         export function ImageDocument(url: string | ImageField, options: DocumentOptions = {}, overwriteDoc?: Doc) {
             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);
@@ -1035,18 +1040,16 @@ export namespace Docs {
          * @param fieldKey the field that the compiled script is written into.
          * @returns the Scripting Doc
          */
+        // eslint-disable-next-line default-param-last
         export function ScriptingDocument(script: Opt<ScriptField> | null, options: DocumentOptions = {}, fieldKey?: string) {
-            return InstanceFromProto(Prototypes.get(DocumentType.SCRIPTING), script ? script : undefined, { ...options, layout: fieldKey ? ScriptingBox.LayoutString(fieldKey) : undefined });
+            return InstanceFromProto(Prototypes.get(DocumentType.SCRIPTING), script || undefined, { ...options, layout: fieldKey ? ScriptingBox.LayoutString(fieldKey) : undefined });
         }
 
+        // eslint-disable-next-line default-param-last
         export function VideoDocument(url: string, options: DocumentOptions = {}, overwriteDoc?: Doc) {
             return InstanceFromProto(Prototypes.get(DocumentType.VID), new VideoField(url), options, undefined, undefined, undefined, overwriteDoc);
         }
 
-        export function YoutubeDocument(url: string, options: DocumentOptions = {}, overwriteDoc?: Doc) {
-            return InstanceFromProto(Prototypes.get(DocumentType.YOUTUBE), new YoutubeField(url), options, undefined, undefined, undefined, overwriteDoc);
-        }
-
         export function WebCamDocument(url: string, options: DocumentOptions = {}) {
             return InstanceFromProto(Prototypes.get(DocumentType.WEBCAM), '', options);
         }
@@ -1059,6 +1062,7 @@ export namespace Docs {
             return InstanceFromProto(Prototypes.get(DocumentType.COMPARISON), undefined, options);
         }
 
+        // eslint-disable-next-line default-param-last
         export function AudioDocument(url: string, options: DocumentOptions = {}, overwriteDoc?: Doc) {
             return InstanceFromProto(Prototypes.get(DocumentType.AUDIO), new AudioField(url), options, undefined, undefined, undefined, overwriteDoc);
         }
@@ -1072,7 +1076,7 @@ export namespace Docs {
         }
 
         export function LoadingDocument(file: File | string, options: DocumentOptions) {
-            return InstanceFromProto(Prototypes.get(DocumentType.LOADING), undefined, { _height: 150, _width: 200, title: typeof file == 'string' ? file : file.name, ...options }, undefined, '');
+            return InstanceFromProto(Prototypes.get(DocumentType.LOADING), undefined, { _height: 150, _width: 200, title: typeof file === 'string' ? file : file.name, ...options }, undefined, '');
         }
 
         export function RTFDocument(field: RichTextField, options: DocumentOptions = {}, fieldKey: string = 'text') {
@@ -1101,6 +1105,7 @@ export namespace Docs {
             return InstanceFromProto(Prototypes.get(DocumentType.RTF), field, options, undefined, fieldKey);
         }
 
+        // eslint-disable-next-line default-param-last
         export function LinkDocument(source: Doc, target: Doc, options: DocumentOptions = {}, id?: string) {
             const linkDoc = InstanceFromProto(
                 Prototypes.get(DocumentType.LINK),
@@ -1124,7 +1129,7 @@ export namespace Docs {
             options: DocumentOptions = {},
             strokeWidth = ActiveInkWidth(),
             color = ActiveInkColor(),
-            stroke_bezier = ActiveInkBezierApprox(),
+            strokeBezier = ActiveInkBezierApprox(),
             fillColor = ActiveFillColor(),
             arrowStart = ActiveArrowStart(),
             arrowEnd = ActiveArrowEnd(),
@@ -1138,7 +1143,7 @@ export namespace Docs {
             I.fillColor = fillColor;
             I.stroke = new InkField(points);
             I.stroke_width = strokeWidth;
-            I.stroke_bezier = stroke_bezier;
+            I.stroke_bezier = strokeBezier;
             I.stroke_startMarker = arrowStart;
             I.stroke_endMarker = arrowEnd;
             I.stroke_dash = dash;
@@ -1148,12 +1153,13 @@ export namespace Docs {
             I.defaultDoubleClick = 'ignore';
             I.author_date = new DateField();
             I['acl-Guest'] = Doc.defaultAclPrivate ? SharingPermissions.None : SharingPermissions.View;
-            //I['acl-Override'] = SharingPermissions.Unset;
+            // I['acl-Override'] = SharingPermissions.Unset;
             I[Initializing] = false;
 
             return ink;
         }
 
+        // eslint-disable-next-line default-param-last
         export function PdfDocument(url: string, options: DocumentOptions = {}, overwriteDoc?: Doc) {
             const width = options._width || undefined;
             const height = options._height || undefined;
@@ -1169,7 +1175,7 @@ export namespace Docs {
             const nwid = options._nativeWidth || undefined;
             const nhght = options._nativeHeight || undefined;
             if (!nhght && width && height && nwid) options._nativeHeight = (Number(nwid) * Number(height)) / Number(width);
-            return InstanceFromProto(Prototypes.get(DocumentType.WEB), new WebField(url ? url : 'https://www.wikipedia.org/'), options);
+            return InstanceFromProto(Prototypes.get(DocumentType.WEB), new WebField(url || 'https://www.wikipedia.org/'), options);
         }
 
         export function HtmlDocument(html: string, options: DocumentOptions = {}) {
@@ -1324,10 +1330,10 @@ export namespace Docs {
             const doc = DockDocument(
                 configs.map(c => c.doc),
                 JSON.stringify(layoutConfig),
-                Doc.CurrentUserEmail === 'guest' ? options : { 'acl-Guest': SharingPermissions.View, ...options },
+                ClientUtils.CurrentUserEmail === 'guest' ? options : { 'acl-Guest': SharingPermissions.View, ...options },
                 id
             );
-            configs.map(c => {
+            configs.forEach(c => {
                 Doc.SetContainer(c.doc, doc);
                 inheritParentAcls(doc, c.doc, false);
             });
@@ -1345,8 +1351,9 @@ export namespace Docs {
 }
 
 export namespace DocUtils {
-    function matchFieldValue(doc: Doc, key: string, value: any): boolean {
-        const hasFunctionFilter = Utils.HasFunctionFilter(value);
+    function matchFieldValue(doc: Doc, key: string, valueIn: any): boolean {
+        let value = valueIn;
+        const hasFunctionFilter = ClientUtils.HasFunctionFilter(value);
         if (hasFunctionFilter) {
             return hasFunctionFilter(StrCast(doc[key]));
         }
@@ -1365,7 +1372,7 @@ export namespace DocUtils {
                                            matchLink(value,DocCast(link.link_anchor_2)) ));
         }
         if (typeof value === 'string') {
-            value = value.replace(`,${Utils.noRecursionHack}`, '');
+            value = value.replace(`,${ClientUtils.noRecursionHack}`, '');
         }
         const fieldVal = doc[key];
         // prettier-ignore
@@ -1377,7 +1384,7 @@ export namespace DocUtils {
         if (vals.length) {
             return vals.some(v => typeof v === 'string' && v.includes(value)); // bcz: arghh: Todo: comparison should be parameterized as exact, or substring
         }
-        return Field.toString(fieldVal as Field).includes(value); // bcz: arghh: Todo: comparison should be parameterized as exact, or substring
+        return Field.toString(fieldVal as FieldType).includes(value); // bcz: arghh: Todo: comparison should be parameterized as exact, or substring
     }
     /**
      * @param docs
@@ -1410,7 +1417,9 @@ export namespace DocUtils {
                   if (d.cookies && (!filterFacets.cookies || !Object.keys(filterFacets.cookies).some(key => d.cookies === key))) {
                       return false;
                   }
-                  for (const facetKey of Object.keys(filterFacets).filter(fkey => fkey !== 'cookies' && fkey !== Utils.noDragDocsFilter.split(Doc.FilterSep)[0])) {
+                  const facetKeys = Object.keys(filterFacets).filter(fkey => fkey !== 'cookies' && fkey !== ClientUtils.noDragDocsFilter.split(Doc.FilterSep)[0]);
+                  // eslint-disable-next-line no-restricted-syntax
+                  for (const facetKey of facetKeys) {
                       const facet = filterFacets[facetKey];
 
                       // facets that match some value in the field of the document (e.g. some text field)
@@ -1431,8 +1440,8 @@ export namespace DocUtils {
                       if (!unsets.length && !exists.length && !xs.length && !checks.length && !matches.length) return true;
                       const failsNotEqualFacets = !xs.length ? false : xs.some(value => matchFieldValue(d, facetKey, value));
                       const satisfiesCheckFacets = !checks.length ? true : checks.some(value => matchFieldValue(d, facetKey, value));
-                      const satisfiesExistsFacets = !exists.length ? true : exists.some(value => (facetKey !== LinkedTo ? d[facetKey] !== undefined : LinkManager.Instance.getAllRelatedLinks(d).length));
-                      const satisfiesUnsetsFacets = !unsets.length ? true : unsets.some(value => d[facetKey] === undefined);
+                      const satisfiesExistsFacets = !exists.length ? true : facetKey !== LinkedTo ? d[facetKey] !== undefined : LinkManager.Instance.getAllRelatedLinks(d).length;
+                      const satisfiesUnsetsFacets = !unsets.length ? true : d[facetKey] === undefined;
                       const satisfiesMatchFacets = !matches.length
                           ? true
                           : matches.some(value => {
@@ -1441,20 +1450,18 @@ export namespace DocUtils {
                                     const allKeys = Array.from(Object.keys(d));
                                     allKeys.push(...Object.keys(Doc.GetProto(d)));
                                     const keys = allKeys.filter(key => key.includes(facetKey.substring(1)));
-                                    return keys.some(key => Field.toString(d[key] as Field).includes(value));
+                                    return keys.some(key => Field.toString(d[key] as FieldType).includes(value));
                                 }
-                                return Field.toString(d[facetKey] as Field).includes(value);
+                                return Field.toString(d[facetKey] as FieldType).includes(value);
                             });
                       // if we're ORing them together, the default return is false, and we return true for a doc if it satisfies any one set of criteria
                       if (parentCollection?.childFilters_boolean === 'OR') {
                           if (satisfiesUnsetsFacets && satisfiesExistsFacets && satisfiesCheckFacets && !failsNotEqualFacets && satisfiesMatchFacets) return true;
                       }
                       // if we're ANDing them together, the default return is true, and we return false for a doc if it doesn't satisfy any set of criteria
-                      else {
-                          if (!satisfiesUnsetsFacets || !satisfiesExistsFacets || !satisfiesCheckFacets || failsNotEqualFacets || (matches.length && !satisfiesMatchFacets)) return false;
-                      }
+                      else if (!satisfiesUnsetsFacets || !satisfiesExistsFacets || !satisfiesCheckFacets || failsNotEqualFacets || (matches.length && !satisfiesMatchFacets)) return false;
                   }
-                  return parentCollection?.childFilters_boolean === 'OR' ? false : true;
+                  return parentCollection?.childFilters_boolean !== 'OR';
               })
             : childDocs;
         const rangeFilteredDocs = filteredDocs.filter(d => {
@@ -1464,7 +1471,7 @@ export namespace DocUtils {
                 const max = Number(childFiltersByRanges[i + 2]);
                 const val = typeof d[key] === 'string' ? (Number(StrCast(d[key])).toString() === StrCast(d[key]) ? Number(StrCast(d[key])) : undefined) : Cast(d[key], 'number', null);
                 if (val === undefined) {
-                    //console.log("Should 'undefined' pass range filter or not?")
+                    // console.log("Should 'undefined' pass range filter or not?")
                 } else if (val < min || val > max) return false;
             }
             return true;
@@ -1472,10 +1479,10 @@ export namespace DocUtils {
         return rangeFilteredDocs;
     }
 
-    export let ActiveRecordings: { props: FieldViewProps; getAnchor: (addAsAnnotation: boolean) => Doc }[] = [];
+    export const ActiveRecordings: { props: FieldViewProps; getAnchor: (addAsAnnotation: boolean) => Doc }[] = [];
 
     export function MakeLinkToActiveAudio(getSourceDoc: () => Doc | undefined, broadcastEvent = true) {
-        broadcastEvent && runInAction(() => (Doc.RecordingEvent = Doc.RecordingEvent + 1));
+        broadcastEvent && runInAction(() => { Doc.RecordingEvent += 1; }); // prettier-ignore
         return DocUtils.ActiveRecordings.map(audio => {
             const sourceDoc = getSourceDoc();
             return sourceDoc && DocUtils.MakeLink(sourceDoc, audio.getAnchor(true) || audio.props.Document, { link_displayLine: false, link_relationship: 'recording annotation:linked recording', link_description: 'recording timeline' });
@@ -1510,7 +1517,9 @@ export namespace DocUtils {
                 }
 
                 setTimeout(
-                    action(() => (TaskCompletionBox.taskCompleted = false)),
+                    action(() => {
+                        TaskCompletionBox.taskCompleted = false;
+                    }),
                     2500
                 );
             }
@@ -1550,7 +1559,7 @@ export namespace DocUtils {
 
     export function AssignScripts(doc: Doc, scripts?: { [key: string]: string | undefined }, funcs?: { [key: string]: string }) {
         scripts &&
-            Object.keys(scripts).map(key => {
+            Object.keys(scripts).forEach(key => {
                 const script = scripts[key];
                 if (ScriptCast(doc[key])?.script.originalScript !== scripts[key] && script) {
                     (key.startsWith('_') ? doc : Doc.GetProto(doc))[key] = ScriptField.MakeScript(script, {
@@ -1573,7 +1582,7 @@ export namespace DocUtils {
         funcs &&
             Object.keys(funcs)
                 .filter(key => !key.endsWith('-setter'))
-                .map(key => {
+                .forEach(key => {
                     const cfield = ComputedField.WithoutComputed(() => FieldValue(doc[key]));
                     if (ScriptCast(cfield)?.script.originalScript !== funcs[key]) {
                         const setFunc = Cast(funcs[key + '-setter'], 'string', null);
@@ -1602,6 +1611,7 @@ export namespace DocUtils {
         return doc;
     }
     export function AssignDocField(doc: Doc, field: string, creator: (reqdOpts: DocumentOptions, items?: Doc[]) => Doc, reqdOpts: DocumentOptions, items?: Doc[], scripts?: { [key: string]: string }, funcs?: { [key: string]: string }) {
+        // eslint-disable-next-line no-return-assign
         return DocUtils.AssignScripts(DocUtils.AssignOpts(DocCast(doc[field]), reqdOpts, items) ?? (doc[field] = creator(reqdOpts, items)), scripts, funcs);
     }
 
@@ -1649,7 +1659,7 @@ export namespace DocUtils {
      * @returns
      */
     export async function DocumentFromType(type: string, path: string, options: DocumentOptions, overwriteDoc?: Doc): Promise<Opt<Doc>> {
-        let ctor: ((path: string, options: DocumentOptions, overwriteDoc?: Doc) => Doc | Promise<Doc | undefined>) | undefined = undefined;
+        let ctor: ((path: string, options: DocumentOptions, overwriteDoc?: Doc) => Doc | Promise<Doc | undefined>) | undefined;
         if (type.indexOf('image') !== -1) {
             ctor = Docs.Create.ImageDocument;
             if (!options._width) options._width = 300;
@@ -1672,7 +1682,7 @@ export namespace DocUtils {
             if (!options._width) options._width = 400;
             if (!options._height) options._height = ((options._width as number) * 1200) / 927;
         }
-        //TODO:al+glr
+        // TODO:al+glr
         // if (type.indexOf("map") !== -1) {
         //     ctor = Docs.Create.MapDocument;
         //     if (!options._width) options._width = 800;
@@ -1695,6 +1705,7 @@ export namespace DocUtils {
                 });
             }
             ctor = Docs.Create.WebDocument;
+            // eslint-disable-next-line no-param-reassign
             options = { ...options, _width: 400, _height: 512, title: path };
         }
 
@@ -1706,12 +1717,12 @@ export namespace DocUtils {
             .filter(btnDoc => !btnDoc.hidden)
             .map(btnDoc => Cast(btnDoc?.dragFactory, Doc, null))
             .filter(doc => doc && doc !== Doc.UserDoc().emptyTrail && doc.title)
-            .map((dragDoc, i) => ({
+            .map(dragDoc => ({
                 description: ':' + StrCast(dragDoc.title).replace('Untitled ', ''),
-                event: undoable((args: { x: number; y: number }) => {
+                event: undoable(() => {
                     const newDoc = DocUtils.copyDragFactory(dragDoc);
                     if (newDoc) {
-                        newDoc.author = Doc.CurrentUserEmail;
+                        newDoc.author = ClientUtils.CurrentUserEmail;
                         newDoc.x = x;
                         newDoc.y = y;
                         EquationBox.SelectOnLoad = newDoc[Id];
@@ -1732,9 +1743,9 @@ export namespace DocUtils {
         !simpleMenu &&
             ContextMenu.Instance.addItem({
                 description: 'Styled Notes',
-                subitems: DocListCast((Doc.UserDoc().template_notes as Doc).data).map((note, i) => ({
+                subitems: DocListCast((Doc.UserDoc().template_notes as Doc).data).map(note => ({
                     description: ':' + StrCast(note.title),
-                    event: undoable((args: { x: number; y: number }) => {
+                    event: undoable(() => {
                         const textDoc = Docs.Create.TextDocument('', {
                             _width: 200,
                             x,
@@ -1757,12 +1768,12 @@ export namespace DocUtils {
             .filter(btnDoc => !btnDoc.hidden)
             .map(btnDoc => Cast(btnDoc?.dragFactory, Doc, null))
             .filter(doc => doc && doc !== Doc.UserDoc().emptyTrail && doc !== Doc.UserDoc().emptyNote && doc.title)
-            .map((dragDoc, i) => ({
+            .map(dragDoc => ({
                 description: ':' + StrCast(dragDoc.title).replace('Untitled ', ''),
-                event: undoable((args: { x: number; y: number }) => {
+                event: undoable(() => {
                     const newDoc = DocUtils.delegateDragFactory(dragDoc);
                     if (newDoc) {
-                        newDoc.author = Doc.CurrentUserEmail;
+                        newDoc.author = ClientUtils.CurrentUserEmail;
                         newDoc.x = x;
                         newDoc.y = y;
                         EquationBox.SelectOnLoad = newDoc[Id];
@@ -1802,11 +1813,11 @@ export namespace DocUtils {
     }
     export function findTemplate(templateName: string, type: string, signature: string) {
         let docLayoutTemplate: Opt<Doc>;
-        const iconViews = DocListCast(Cast(Doc.UserDoc()['template_icons'], Doc, null)?.data);
-        const templBtns = DocListCast(Cast(Doc.UserDoc()['template_buttons'], Doc, null)?.data);
-        const noteTypes = DocListCast(Cast(Doc.UserDoc()['template_notes'], Doc, null)?.data);
-        const userTypes = DocListCast(Cast(Doc.UserDoc()['template_user'], Doc, null)?.data);
-        const clickFuncs = DocListCast(Cast(Doc.UserDoc()['template_clickFuncs'], Doc, null)?.data);
+        const iconViews = DocListCast(Cast(Doc.UserDoc().template_icons, Doc, null)?.data);
+        const templBtns = DocListCast(Cast(Doc.UserDoc().template_buttons, Doc, null)?.data);
+        const noteTypes = DocListCast(Cast(Doc.UserDoc().template_notes, Doc, null)?.data);
+        const userTypes = DocListCast(Cast(Doc.UserDoc().template_user, Doc, null)?.data);
+        const clickFuncs = DocListCast(Cast(Doc.UserDoc().template_clickFuncs, Doc, null)?.data);
         const allTemplates = iconViews
             .concat(templBtns)
             .concat(noteTypes)
@@ -1816,13 +1827,20 @@ export namespace DocUtils {
             .filter(doc => doc.isTemplateDoc);
         // bcz: this is hacky -- want to have different templates be applied depending on the "type" of a document.  but type is not reliable and there could be other types of template searches so this should be generalized
         // first try to find a template that matches the specific document type (<typeName>_<templateName>).  otherwise, fallback to a general match on <templateName>
-        !docLayoutTemplate && allTemplates.forEach(tempDoc => StrCast(tempDoc.title) === templateName + '_' + type && (docLayoutTemplate = tempDoc));
-        !docLayoutTemplate && allTemplates.forEach(tempDoc => StrCast(tempDoc.title) === templateName && (docLayoutTemplate = tempDoc));
+        !docLayoutTemplate &&
+            allTemplates.forEach(tempDoc => {
+                StrCast(tempDoc.title) === templateName + '_' + type && (docLayoutTemplate = tempDoc);
+            });
+        !docLayoutTemplate &&
+            allTemplates.forEach(tempDoc => {
+                StrCast(tempDoc.title) === templateName && (docLayoutTemplate = tempDoc);
+            });
         return docLayoutTemplate;
     }
     export function createCustomView(doc: Doc, creator: Opt<(documents: Array<Doc>, options: DocumentOptions, id?: string) => Doc>, templateSignature: string = 'custom', docLayoutTemplate?: Doc) {
         const templateName = templateSignature.replace(/\(.*\)/, '');
-        doc.layout_fieldKey = 'layout_' + (templateSignature || docLayoutTemplate?.title);
+        doc.layout_fieldKey = 'layout_' + (templateSignature || (docLayoutTemplate?.title ?? ''));
+        // eslint-disable-next-line no-param-reassign
         docLayoutTemplate = docLayoutTemplate || findTemplate(templateName, StrCast(doc.isGroup && doc.transcription ? 'transcription' : doc.type), templateSignature);
 
         const customName = 'layout_' + templateSignature;
@@ -1859,14 +1877,15 @@ export namespace DocUtils {
         }
     }
     export function iconify(doc: Doc) {
-        const layout_fieldKey = Cast(doc.layout_fieldKey, 'string', null);
+        const layoutFieldKey = Cast(doc.layout_fieldKey, 'string', null);
         DocUtils.makeCustomViewClicked(doc, Docs.Create.StackingDocument, 'icon', undefined);
-        if (layout_fieldKey && layout_fieldKey !== 'layout' && layout_fieldKey !== 'layout_icon') doc.deiconifyLayout = layout_fieldKey.replace('layout_', '');
+        if (layoutFieldKey && layoutFieldKey !== 'layout' && layoutFieldKey !== 'layout_icon') doc.deiconifyLayout = layoutFieldKey.replace('layout_', '');
     }
 
     export function pileup(docList: Doc[], x?: number, y?: number, size: number = 55, create: boolean = true) {
         runInAction(() => {
-            docList.forEach((d, i) => {
+            docList.forEach((doc, i) => {
+                const d = doc;
                 DocUtils.iconify(d);
                 d.x = Math.cos((Math.PI * 2 * i) / docList.length) * size - size;
                 d.y = Math.sin((Math.PI * 2 * i) / docList.length) * size - size;
@@ -1880,6 +1899,7 @@ export namespace DocUtils {
             newCollection._width = newCollection._height = size * 2;
             return newCollection;
         }
+        return undefined;
     }
     export function makeIntoPortal(doc: Doc, layoutDoc: Doc, allLinks: Doc[]) {
         const portalLink = allLinks.find(d => d.link_anchor_1 === doc && d.link_relationship === 'portal to:portal from');
@@ -1921,7 +1941,7 @@ export namespace DocUtils {
                 _timecodeToShow: Cast(doc._timecodeToShow, 'number', null),
             });
             Doc.AddDocToList(context, annotationField, pushpin);
-            const pushpinLink = DocUtils.MakeLink(pushpin, doc, { link_relationship: 'pushpin' }, '');
+            DocUtils.MakeLink(pushpin, doc, { link_relationship: 'pushpin' }, '');
             doc._timecodeToShow = undefined;
             return pushpin;
         }
@@ -1948,23 +1968,24 @@ export namespace DocUtils {
     // }
 
     function ConvertDMSToDD(degrees: number, minutes: number, seconds: number, direction: string) {
-        var dd = degrees + minutes / 60 + seconds / (60 * 60);
+        let dd = degrees + minutes / 60 + seconds / (60 * 60);
         if (direction === 'S' || direction === 'W') {
-            dd = dd * -1;
+            dd *= -1;
         } // Don't do anything for N or E
         return dd;
     }
 
-    export function assignImageInfo(result: Upload.FileInformation, proto: Doc) {
+    export function assignImageInfo(result: Upload.FileInformation, protoIn: Doc) {
+        const proto = protoIn;
         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_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;
@@ -2033,7 +2054,7 @@ export namespace DocUtils {
         const generatedDocuments: Doc[] = [];
         Networking.UploadYoutubeToServer(videoId, overwriteDoc?.[Id]).then(upfiles => {
             const {
-                source: { newFilename, originalFilename, mimetype },
+                source: { newFilename, mimetype },
                 result,
             } = upfiles.lastElement();
             if ((result as any).message) {
@@ -2061,12 +2082,9 @@ export namespace DocUtils {
         const fileNoGuidPairs: Networking.FileGuidPair[] = files.map(file => ({ file }));
 
         const upfiles = await Networking.UploadFilesToServer(fileNoGuidPairs);
-        for (const {
-            source: { newFilename, mimetype },
-            result,
-        } of upfiles) {
+        upfiles.forEach(({ source: { newFilename, mimetype }, result }) => {
             newFilename && mimetype && processFileupload(generatedDocuments, newFilename, mimetype, result, options);
-        }
+        });
         return generatedDocuments;
     }
 
@@ -2091,34 +2109,115 @@ export namespace DocUtils {
     export function copyDragFactory(dragFactory: Doc) {
         if (!dragFactory) return undefined;
         const ndoc = dragFactory.isTemplateDoc ? Doc.ApplyTemplate(dragFactory) : Doc.MakeCopy(dragFactory, true);
-        if (ndoc && dragFactory['dragFactory_count'] !== undefined) {
-            dragFactory['dragFactory_count'] = NumCast(dragFactory['dragFactory_count']) + 1;
-            Doc.SetInPlace(ndoc, 'title', ndoc.title + ' ' + NumCast(dragFactory['dragFactory_count']).toString(), true);
+        if (ndoc && dragFactory.dragFactory_count !== undefined) {
+            dragFactory.dragFactory_count = NumCast(dragFactory.dragFactory_count) + 1;
+            Doc.SetInPlace(ndoc, 'title', ndoc.title + ' ' + NumCast(dragFactory.dragFactory_count).toString(), true);
         }
 
         return ndoc;
     }
     export function delegateDragFactory(dragFactory: Doc) {
         const ndoc = Doc.MakeDelegateWithProto(dragFactory);
-        if (ndoc && dragFactory['dragFactory_count'] !== undefined) {
-            dragFactory['dragFactory_count'] = NumCast(dragFactory['dragFactory_count']) + 1;
-            Doc.GetProto(ndoc).title = ndoc.title + ' ' + NumCast(dragFactory['dragFactory_count']).toString();
+        if (ndoc && dragFactory.dragFactory_count !== undefined) {
+            dragFactory.dragFactory_count = NumCast(dragFactory.dragFactory_count) + 1;
+            Doc.GetProto(ndoc).title = ndoc.title + ' ' + NumCast(dragFactory.dragFactory_count).toString();
         }
         return ndoc;
     }
+
+    export async function Zip(doc: Doc, zipFilename = 'dashExport.zip') {
+        const { clone, map, linkMap } = await Doc.MakeClone(doc);
+        const proms = new Set<string>();
+        function replacer(key: any, value: any) {
+            if (key && ['branchOf', 'cloneOf', 'cursors'].includes(key)) return undefined;
+            if (value?.__type === 'image') {
+                const extension = value.url.replace(/.*\./, '');
+                proms.add(value.url.replace('.' + extension, '_o.' + extension));
+                return SerializationHelper.Serialize(new ImageField(value.url));
+            }
+            if (value?.__type === 'pdf') {
+                proms.add(value.url);
+                return SerializationHelper.Serialize(new PdfField(value.url));
+            }
+            if (value?.__type === 'audio') {
+                proms.add(value.url);
+                return SerializationHelper.Serialize(new AudioField(value.url));
+            }
+            if (value?.__type === 'video') {
+                proms.add(value.url);
+                return SerializationHelper.Serialize(new VideoField(value.url));
+            }
+            if (
+                value instanceof Doc ||
+                value instanceof ScriptField ||
+                value instanceof RichTextField ||
+                value instanceof InkField ||
+                value instanceof CsvField ||
+                value instanceof WebField ||
+                value instanceof DateField ||
+                value instanceof ProxyField ||
+                value instanceof ComputedField
+            ) {
+                return SerializationHelper.Serialize(value);
+            }
+            if (value instanceof Array && key !== ListFieldName && key !== InkDataFieldName) return { fields: value, __type: 'list' };
+            return value;
+        }
+
+        const docs: { [id: string]: any } = {};
+        const links: { [id: string]: any } = {};
+        Array.from(map.entries()).forEach(f => (docs[f[0]] = f[1]));
+        Array.from(linkMap.entries()).forEach(l => (links[l[0]] = l[1]));
+        const jsonDocs = JSON.stringify({ id: clone[Id], docs, links }, decycle(replacer));
+
+        const zip = new JSZip();
+        var count = 0;
+        const promArr = Array.from(proms)
+            .filter(url => url?.startsWith('/files'))
+            .map(url => url.replace('/', '')); // window.location.origin));
+        console.log(promArr.length);
+        if (!promArr.length) {
+            zip.file('docs.json', jsonDocs);
+            zip.generateAsync({ type: 'blob' }).then(content => saveAs(content, zipFilename));
+        } else
+            promArr.forEach((url, i) => {
+                // loading a file and add it in a zip file
+                JSZipUtils.getBinaryContent(window.location.origin + '/' + url, (err: any, data: any) => {
+                    if (err) throw err; // or handle the error
+                    // // Generate a directory within the Zip file structure
+                    // const assets = zip.folder("assets");
+                    // assets.file(filename, data, {binary: true});
+                    const assetPathOnServer = promArr[i].replace(window.location.origin + '/', '').replace(/\//g, '%%%');
+                    zip.file(assetPathOnServer, data, { binary: true });
+                    console.log(' => ' + url);
+                    if (++count === promArr.length) {
+                        zip.file('docs.json', jsonDocs);
+                        zip.generateAsync({ type: 'blob' }).then(content => saveAs(content, zipFilename));
+                        // const a = document.createElement("a");
+                        // const url = Utils.prepend(`/downloadId/${this.props.Document[Id]}`);
+                        // a.href = url;
+                        // a.download = `DocExport-${this.props.Document[Id]}.zip`;
+                        // a.click();
+                    }
+                });
+            });
+    }
 }
 
 ScriptingGlobals.add('Docs', Docs);
+// eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function copyDragFactory(dragFactory: Doc, asDelegate?: boolean) {
     return dragFactory instanceof Doc ? (asDelegate ? DocUtils.delegateDragFactory(dragFactory) : DocUtils.copyDragFactory(dragFactory)) : dragFactory;
 });
+// eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function makeDelegate(proto: any) {
     const d = Docs.Create.DelegateDocument(proto, { title: 'child of ' + proto.title });
     return d;
 });
+// eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function generateLinkTitle(link: Doc) {
-    const link_anchor_1title = link.link_anchor_1 && link.link_anchor_1 !== link ? Cast(link.link_anchor_1, Doc, null)?.title : '<?>';
-    const link_anchor_2title = link.link_anchor_2 && link.link_anchor_2 !== link ? Cast(link.link_anchor_2, Doc, null)?.title : '<?>';
+    const linkAnchor1title = link.link_anchor_1 && link.link_anchor_1 !== link ? Cast(link.link_anchor_1, Doc, null)?.title : '<?>';
+    const linkAnchor2title = link.link_anchor_2 && link.link_anchor_2 !== link ? Cast(link.link_anchor_2, Doc, null)?.title : '<?>';
     const relation = link.link_relationship || 'to';
-    return `${link_anchor_1title} (${relation}) ${link_anchor_2title}`;
+    return `${linkAnchor1title} (${relation}) ${linkAnchor2title}`;
 });
diff --git a/src/client/util/CaptureManager.tsx b/src/client/util/CaptureManager.tsx
index 8451357ef..2939ba581 100644
--- a/src/client/util/CaptureManager.tsx
+++ b/src/client/util/CaptureManager.tsx
@@ -2,7 +2,7 @@ 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 { addStyleSheet } from '../../ClientUtils';
 import { Doc } from '../../fields/Doc';
 import { DocCast, StrCast } from '../../fields/Types';
 import { LightboxView } from '../views/LightboxView';
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index 081115879..7811f8605 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -1,6 +1,6 @@
 import { observable, reaction, runInAction } from "mobx";
 import * as rp from 'request-promise';
-import { OmitKeys, Utils } from "../../Utils";
+import { ClientUtils, OmitKeys } from "../../ClientUtils";
 import { Doc, DocListCast, DocListCastAsync, Opt } from "../../fields/Doc";
 import { DocData } from "../../fields/DocSymbols";
 import { InkTool } from "../../fields/InkField";
@@ -12,7 +12,7 @@ import { ScriptField } from "../../fields/ScriptField";
 import { Cast, DateCast, DocCast, StrCast } from "../../fields/Types";
 import { WebField, nullAudio } from "../../fields/URLField";
 import { SetCachedGroups, SharingPermissions } from "../../fields/util";
-import { GestureUtils } from "../../pen-gestures/GestureUtils";
+import { Gestures } from "../../pen-gestures/GestureTypes";
 import { DocServer } from "../DocServer";
 import { CollectionViewType, DocumentType } from "../documents/DocumentTypes";
 import { DocUtils, Docs, DocumentOptions, FInfo, FInfoFieldType } from "../documents/Documents";
@@ -20,20 +20,21 @@ import { DashboardView } from "../views/DashboardView";
 import { OverlayView } from "../views/OverlayView";
 import { CollectionTreeView, TreeViewType } from "../views/collections/CollectionTreeView";
 import { Colors } from "../views/global/globalEnums";
-import { media_state } from "../views/nodes/AudioBox";
+import { mediaState } from "../views/nodes/AudioBox";
 import { OpenWhere } from "../views/nodes/DocumentView";
 import { ButtonType, FontIconBox } from "../views/nodes/FontIconBox/FontIconBox";
+import { ImageBox } from "../views/nodes/ImageBox";
+import { LabelBox } from "../views/nodes/LabelBox";
 import { ImportElementBox } from "../views/nodes/importBox/ImportElementBox";
-import { DragManager, dropActionType } from "./DragManager";
+import { DragManager } from "./DragManager";
+import { dropActionType } from "./DropActionTypes";
 import { MakeTemplate } from "./DropConverter";
 import { FollowLinkScript } from "./LinkFollower";
-import { LinkManager } from "./LinkManager";
+import { LinkManager, UPDATE_SERVER_CACHE } from "./LinkManager";
 import { ScriptingGlobals } from "./ScriptingGlobals";
 import { ColorScheme } from "./SettingsManager";
 import { SnappingManager } from "./SnappingManager";
 import { UndoManager } from "./UndoManager";
-import { LabelBox } from "../views/nodes/LabelBox";
-import { ImageBox } from "../views/nodes/ImageBox";
 
 interface Button {
     // DocumentOptions fields a button can set
@@ -60,6 +61,7 @@ interface Button {
     subMenu?: Button[];
 }
 
+// eslint-disable-next-line import/no-mutable-exports
 export let resolvedPorts: { server: number, socket: number };
 export class CurrentUserUtils {
 
@@ -90,6 +92,7 @@ export class CurrentUserUtils {
         });
 
         const reqdOpts:DocumentOptions = { title: "child click editors", _height:75, isSystem: true};
+        // eslint-disable-next-line no-return-assign
         return DocUtils.AssignOpts(tempClicks, reqdOpts, reqdClickList) ?? (doc[field] = Docs.Create.TreeDocument(reqdClickList, reqdOpts));
     }
 
@@ -114,6 +117,7 @@ export class CurrentUserUtils {
         });
 
         const reqdOpts:DocumentOptions = {title: "click editor templates", _height:75, isSystem: true};
+        // eslint-disable-next-line no-return-assign
         return DocUtils.AssignOpts(tempClicks, reqdOpts, reqdClickList) ?? (doc[field] = Docs.Create.TreeDocument(reqdClickList, reqdOpts));
     }
 
@@ -131,6 +135,7 @@ export class CurrentUserUtils {
         }), ... DocListCast(tempNotes?.data).filter(note => !reqdTempOpts.find(reqd => reqd.title === note.title))];
 
         const reqdOpts:DocumentOptions = { title: "Note Layouts", _height: 75, isSystem: true };
+        // eslint-disable-next-line no-return-assign
         return DocUtils.AssignOpts(tempNotes, reqdOpts, reqdNoteList) ?? (doc[field] = Docs.Create.TreeDocument(reqdNoteList, reqdOpts));
     }
     static setupUserTemplates(doc: Doc, field="template_user") {
@@ -138,6 +143,7 @@ export class CurrentUserUtils {
         const reqdUserList = DocListCast(tempUsers?.data);
 
         const reqdOpts:DocumentOptions = { title: "User Layouts", _height: 75, isSystem: true };
+        // eslint-disable-next-line no-return-assign
         return DocUtils.AssignOpts(tempUsers, reqdOpts, reqdUserList) ?? (doc[field] = Docs.Create.TreeDocument(reqdUserList, reqdOpts));
     }
 
@@ -159,24 +165,26 @@ export class CurrentUserUtils {
         const reqdOpts = { title: "icon templates", _height: 75, isSystem: true };
         const templateIconsDoc = DocUtils.AssignOpts(DocCast(doc[field]), reqdOpts) ?? (doc[field] = Docs.Create.TreeDocument([], reqdOpts));
 
+        const labelBox = (opts: DocumentOptions, fieldKey:string) => Docs.Create.LabelDocument({
+            layout: LabelBox.LayoutString(fieldKey), textTransform: "unset", letterSpacing: "unset", _singleLine: false, _label_minFontSize: 14, _label_maxFontSize: 14, layout_borderRounding: "5px",  _width: 150, _height: 70, _xPadding: 10, _yPadding: 10, ...opts
+        });
+        const imageBox = (opts: DocumentOptions, fieldKey:string) => Docs.Create.ImageDocument( "http://www.cs.brown.edu/~bcz/noImage.png", { layout:ImageBox.LayoutString(fieldKey), "icon_nativeWidth": 360 / 4, "icon_nativeHeight": 270 / 4, iconTemplate:DocumentType.IMG,  _width: 360 / 4, _height: 270 / 4,  _layout_showTitle: "title", ...opts });
+        const fontBox = (opts:DocumentOptions, fieldKey:string) => Docs.Create.FontIconDocument({ layout:FontIconBox.LayoutString(fieldKey), _nativeHeight: 30, _nativeWidth: 30, _width: 30, _height: 30, ...opts  });
+        
         const makeIconTemplate = (type: DocumentType | undefined, templateField: string, opts:DocumentOptions) => {
             const title = "icon" + (type ? "_" + type : "");
             const curIcon = DocCast(templateIconsDoc[title]);
-            let creator = labelBox;
-            switch (opts.iconTemplate) {
-                case DocumentType.IMG : creator = imageBox; break;
-                case DocumentType.FONTICON: creator = fontBox; break;
-            }
+            const creator =  (() => { switch (opts.iconTemplate) {
+                                        case DocumentType.IMG : return imageBox;
+                                        case DocumentType.FONTICON: return fontBox;
+                                        default: return labelBox;
+                                    }})();
             const allopts = {isSystem: true, onClickScriptDisable: "never",  ...opts, title};
+            // eslint-disable-next-line no-return-assign
             return DocUtils.AssignScripts(  (curIcon?.iconTemplate === opts.iconTemplate ? 
                       DocUtils.AssignOpts(curIcon, allopts):undefined) ?? ((templateIconsDoc[title] = MakeTemplate(creator(allopts, templateField)))),
                 {onClick:"deiconifyView(documentView)", onDoubleClick: "deiconifyViewToLightbox(documentView)", });
         };
-        const labelBox = (opts: DocumentOptions, fieldKey:string) => Docs.Create.LabelDocument({
-            layout: LabelBox.LayoutString(fieldKey), textTransform: "unset", letterSpacing: "unset", _singleLine: false, _label_minFontSize: 14, _label_maxFontSize: 14, layout_borderRounding: "5px",  _width: 150, _height: 70, _xPadding: 10, _yPadding: 10, ...opts
-        });
-        const imageBox = (opts: DocumentOptions, fieldKey:string) => Docs.Create.ImageDocument( "http://www.cs.brown.edu/~bcz/noImage.png", { layout:ImageBox.LayoutString(fieldKey), "icon_nativeWidth": 360 / 4, "icon_nativeHeight": 270 / 4, iconTemplate:DocumentType.IMG,  _width: 360 / 4, _height: 270 / 4,  _layout_showTitle: "title", ...opts });
-        const fontBox = (opts:DocumentOptions, fieldKey:string) => Docs.Create.FontIconDocument({ layout:FontIconBox.LayoutString(fieldKey), _nativeHeight: 30, _nativeWidth: 30, _width: 30, _height: 30, ...opts  });
         const iconTemplates = [
             makeIconTemplate(undefined,          "title", { iconTemplate:DocumentType.LABEL, backgroundColor: "dimgray"}),
             makeIconTemplate(DocumentType.AUDIO, "title", { iconTemplate:DocumentType.LABEL, backgroundColor: "lightgreen"}),
@@ -188,9 +196,9 @@ export class CurrentUserUtils {
             makeIconTemplate(DocumentType.COL,   "icon",  { iconTemplate:DocumentType.IMG}),
             makeIconTemplate(DocumentType.VID,   "icon",  { iconTemplate:DocumentType.IMG}),
             makeIconTemplate(DocumentType.BUTTON,"title", { iconTemplate:DocumentType.FONTICON}),
-            //nasty hack .. templates are looked up exclusively by type -- but we want a template for a document with a certain field (transcription) .. so this hack and the companion hack in createCustomView does this for now
+            // nasty hack .. templates are looked up exclusively by type -- but we want a template for a document with a certain field (transcription) .. so this hack and the companion hack in createCustomView does this for now
             makeIconTemplate("transcription" as any, "transcription", { iconTemplate:DocumentType.LABEL, backgroundColor: "orange" }),
-            //makeIconTemplate(DocumentType.PDF, "icon", {iconTemplate:DocumentType.IMG}, (opts) => imageBox("http://www.cs.brown.edu/~bcz/noImage.png", opts))
+            // makeIconTemplate(DocumentType.PDF, "icon", {iconTemplate:DocumentType.IMG}, (opts) => imageBox("http://www.cs.brown.edu/~bcz/noImage.png", opts))
         ].filter(d => d).map(d => d!);
         DocUtils.AssignOpts(DocCast(doc[field]), {}, iconTemplates);
     }
@@ -241,14 +249,15 @@ export class CurrentUserUtils {
                         Docs.Create.TextDocument("", { title: "text", _layout_fitWidth:true,  _height: 100, isSystem: true, _text_fontFamily: StrCast(Doc.UserDoc().fontFamily), _text_fontSize: StrCast(Doc.UserDoc().fontSize) })
                     ], {...opts, title: "Slide View Template"}));
         const plotlyApi = () => {
-            var plotly = Doc.MyPublishedDocs.find(doc => doc.title === "@plotly");
+            let plotly = Doc.MyPublishedDocs.find(doc => doc.title === "@plotly");
             if (!plotly) {
-                const plotly = Docs.Create.TextDocument(
+                plotly = Docs.Create.TextDocument(
                     `await import("https://cdn.plot.ly/plotly-2.27.0.min.js");
                     Plotly.newPlot(dashDiv.id, [ --DOCDATA-- ])`
                     , {title: "@plotly", title_custom: true, _layout_showTitle:"title",  _width:300,_height:400});
                 Doc.AddToMyPublished(plotly);
             }
+            return plotly;
         }
         const plotlyView = (opts:DocumentOptions) => {
             const rtfield = new RichTextField(JSON.stringify(
@@ -280,10 +289,10 @@ export class CurrentUserUtils {
             return slide; 
         }
         const mermaidsApi = () => {
-            var mermaids = Doc.MyPublishedDocs.find(doc => doc.title === "@mermaids");
+            let mermaids = Doc.MyPublishedDocs.find(doc => doc.title === "@mermaids");
             if (!mermaids) {
-                const mermaids = Docs.Create.TextDocument(
-                `const mdef = (await import("https://cdn.jsdelivr.net/npm/mermaid\@10.8.0/dist/mermaid.esm.min.mjs")).default;
+                mermaids = Docs.Create.TextDocument(
+                `const mdef = (await import("https://cdn.jsdelivr.net/npm/mermaid@10.8.0/dist/mermaid.esm.min.mjs")).default;
                 window["callb"] = (x)  => { 
                    alert(x);
                 }
@@ -301,6 +310,7 @@ export class CurrentUserUtils {
                 , {title: "@mermaids", title_custom: true, _layout_showTitle:"title",  _width:300,_height:400});
                 Doc.AddToMyPublished(mermaids);
             }
+            return mermaids;
         }
         const mermaidsView = (opts:DocumentOptions) => {
             const rtfield = new RichTextField(JSON.stringify(
@@ -333,7 +343,7 @@ pie title Minerals in my tap water
             slide.onPaint = ScriptField.MakeScript(`toggleDetail(documentView, "textPainted")`, {documentView:"any"});
             return slide; 
         }
-        const apis = [plotlyApi(), mermaidsApi()]
+        plotlyApi(); mermaidsApi();
         const emptyThings:{key:string,  // the field name where the empty thing will be stored
                           opts:DocumentOptions,   // the document options that are required for the empty thing
                           funcs?:{[key:string]: any},  // computed fields that are rquired for the empth thing
@@ -465,83 +475,9 @@ pie title Minerals in my tap water
         return DocUtils.AssignDocField(doc, field, (opts, items) => Docs.Create.StackingDocument(items??[], opts), reqdStackOpts, menuBtns, { dropConverter: "convertToButtons(dragData)" });
     }
 
-    // Sets up mobile menu if it is undefined creates a new one, otherwise returns existing menu
-    static setupActiveMobileMenu(doc: Doc, field="activeMobileMenu") {
-        const reqdOpts = { _width: 980, ignoreClick: true, _lockedPosition: false, title: "home", _yMargin: 100, isSystem: true, _chromeHidden: true,};
-        DocUtils.AssignDocField(doc, field, (opts, items) => Docs.Create.StackingDocument(this.setupMobileButtons(), opts), reqdOpts);
-    }
-
-    // Sets up mobile buttons for inside mobile menu
-    static setupMobileButtons(doc?: Doc, buttons?: string[]) {
-        return [];
-        const docProtoData: { title: string, icon: string, drag?: string, ignoreClick?: boolean, click?: string, backgroundColor?: string, info: string, dragFactory?: Doc }[] = [
-            { title: "DASHBOARDS", icon: "bars", click: 'switchToMobileLibrary()', backgroundColor: "lightgrey", info: "Access your Dashboards from your mobile, and navigate through all of your documents. " },
-            { title: "UPLOAD", icon: "upload", click: 'openMobileUploads()', backgroundColor: "lightgrey", info: "Upload files from your mobile device so they can be accessed on Dash Web." },
-            { title: "MOBILE UPLOAD", icon: "mobile", click: 'switchToMobileUploadCollection()', backgroundColor: "lightgrey", info: "Access the collection of your mobile uploads." },
-            { title: "RECORD", icon: "microphone", click: 'openMobileAudio()', backgroundColor: "lightgrey", info: "Use your phone to record, dictate and then upload audio onto Dash Web." },
-            { title: "PRESENTATION", icon: "desktop", click: 'switchToMobilePresentation()', backgroundColor: "lightgrey", info: "Use your phone as a remote for you presentation." },
-            { title: "SETTINGS", icon: "cog", click: 'openMobileSettings()', backgroundColor: "lightgrey", info: "Change your password, log out, or manage your account security." }
-        ];
-        // returns a list of mobile buttons
-        return docProtoData.filter(d => !buttons || !buttons.includes(d.title)).map(data =>
-            this.mobileButton({
-                title: data.title,
-                _lockedPosition: true,
-                onClick: data.click ? ScriptField.MakeScript(data.click) : undefined,
-                backgroundColor: data.backgroundColor, isSystem: true
-            },
-                [this.createToolButton({ ignoreClick: true, icon: data.icon, backgroundColor: "rgba(0,0,0,0)", isSystem: true, btnType: ButtonType.ClickButton, }), this.mobileTextContainer({}, [this.mobileButtonText({}, data.title), this.mobileButtonInfo({}, data.info)])])
-        );
-    }
-
-    // sets up the main document for the mobile button
-    static mobileButton = (opts: DocumentOptions, docs: Doc[]) => Docs.Create.MulticolumnDocument(docs, {
-        ...opts,
-         _nativeWidth: 900, _nativeHeight: 250, _width: 900, _height: 250, _yMargin: 15,
-        layout_borderRounding: "5px", layout_boxShadow: "0 0", isSystem: true
-    }) as any as Doc
-
-    // sets up the text container for the information contained within the mobile button
-    static mobileTextContainer = (opts: DocumentOptions, docs: Doc[]) => Docs.Create.MultirowDocument(docs, {
-        ...opts,
-        _nativeWidth: 450, _nativeHeight: 250, _width: 450, _height: 250, _yMargin: 25,
-        backgroundColor: "rgba(0,0,0,0)", layout_borderRounding: "0", layout_boxShadow: "0 0", ignoreClick: true, isSystem: true
-    }) as any as Doc
-
-    // Sets up the title of the button
-    static mobileButtonText = (opts: DocumentOptions, buttonTitle: string) => Docs.Create.TextDocument(buttonTitle, {
-        ...opts,
-        title: buttonTitle, _text_fontSize: "37px", _xMargin: 0, _yMargin: 0, ignoreClick: true, backgroundColor: "rgba(0,0,0,0)", isSystem: true
-    }) as any as Doc
-
-    // Sets up the description of the button
-    static mobileButtonInfo = (opts: DocumentOptions, buttonInfo: string) => Docs.Create.TextDocument(buttonInfo, {
-        ...opts,
-        title: "info", _text_fontSize: "25px", _xMargin: 0, _yMargin: 0, ignoreClick: true, backgroundColor: "rgba(0,0,0,0)", _dimMagnitude: 2, isSystem: true
-    }) as any as Doc
-
-
-
-    static setupMobileInkingDoc(userDoc: Doc) {
-        return Docs.Create.FreeformDocument([], { title: "Mobile Inking", backgroundColor: "white", isSystem: true });
-    }
-
-    static setupMobileUploadDoc(userDoc: Doc) {
-        // const addButton = Docs.Create.FontIconDocument({ onDragStart: ScriptField.MakeScript('addWebToMobileUpload()'), title: "Add Web Doc to Upload Collection", icon: "plus", backgroundColor: "black" })
-        const webDoc = Docs.Create.WebDocument("https://www.britannica.com/biography/Miles-Davis", {
-            title: "Upload Images From the Web", _lockedPosition: true, isSystem: true
-        });
-        const uploadDoc = Docs.Create.StackingDocument([], {
-            title: "Mobile Upload Collection", backgroundColor: "white", _lockedPosition: true, isSystem: true, _chromeHidden: true,
-        });
-        return Docs.Create.StackingDocument([webDoc, uploadDoc], {
-            _width: screen.width, _lockedPosition: true, title: "Upload", _layout_autoHeight: true, _yMargin: 80, backgroundColor: "lightgray", isSystem: true, _chromeHidden: true,
-        });
-    }
-
     /// Search option on the left side button panel
     static setupSearcher(doc: Doc, field:string) {
-        return DocUtils.AssignDocField(doc, field, (opts, items) => Docs.Create.SearchDocument(opts), {
+        return DocUtils.AssignDocField(doc, field, (opts) => Docs.Create.SearchDocument(opts), {
                 dontRegisterView: true, backgroundColor: "dimgray", ignoreClick: true, title: "Search Panel", isSystem: true, childDragAction: dropActionType.embed, 
                 _lockedPosition: true, _type_collection: CollectionViewType.Schema  });
     }
@@ -552,7 +488,7 @@ pie title Minerals in my tap water
         const creatorBtns = CurrentUserUtils.setupCreatorButtons(doc, allTools?.length ? allTools[0]:undefined);
         const userTools = allTools && allTools?.length > 1 ? allTools[1]:undefined;
         const userBtns = CurrentUserUtils.setupUserDocumentCreatorButtons(doc, userTools);
-        //doc.myUserBtns = new PrefetchProxy(userBtns);
+        // doc.myUserBtns = new PrefetchProxy(userBtns);
         const reqdToolOps:DocumentOptions = {
             title: "My Tools", isSystem: true, ignoreClick: true, layout_boxShadow: "0 0",
             layout_explainer: "This is a palette of documents that can be created.",
@@ -563,7 +499,7 @@ pie title Minerals in my tap water
 
     /// initializes the left sidebar dashboard pane
     static setupDashboards(doc: Doc, field:string) {
-        var myDashboards = DocCast(doc[field]);
+        let myDashboards = DocCast(doc[field]);
 
         const newDashboard = `createNewDashboard()`;
 
@@ -572,9 +508,9 @@ pie title Minerals in my tap water
         const reqdBtnScript = {onClick: newDashboard,}
         const newDashboardButton = DocUtils.AssignScripts(DocUtils.AssignOpts(DocCast(myDashboards?.layout_headerButton), reqdBtnOpts) ?? Docs.Create.FontIconDocument(reqdBtnOpts), reqdBtnScript);
        
-        const contextMenuScripts = [/*newDashboard*/] as string[];
-        const contextMenuLabels = [/*"Create New Dashboard"*/]  as string[];
-        const contextMenuIcons = [/*"plus"*/]  as string[];
+        const contextMenuScripts = [/* newDashboard */] as string[];
+        const contextMenuLabels = [/* "Create New Dashboard" */]  as string[];
+        const contextMenuIcons = [/* "plus" */]  as string[];
         const childContextMenuScripts = [`toggleComicMode()`, `snapshotDashboard()`, `shareDashboard(this)`, 'removeDashboard(this)', 'resetDashboard(this)']; // entries must be kept in synch with childContextMenuLabels, childContextMenuIcons, and childContextMenuFilters
         const childContextMenuFilters =  ['!IsNoviceMode()', '!IsNoviceMode()', undefined as any, undefined as any, '!IsNoviceMode()'];// entries must be kept in synch with childContextMenuLabels, childContextMenuIcons, and childContextMenuScripts
         const childContextMenuLabels =  ["Toggle Comic Mode", "Snapshot Dashboard", "Share Dashboard", "Remove Dashboard", "Reset Dashboard"];// entries must be kept in synch with childContextMenuScripts, childContextMenuIcons, and childContextMenuFilters
@@ -605,7 +541,7 @@ pie title Minerals in my tap water
 
     /// initializes the left sidebar File system pane
     static setupFilesystem(doc: Doc, field:string) {
-        var myFilesystem = DocCast(doc[field]);
+        const myFilesystem = DocCast(doc[field]);
         
         const newFolderOpts: DocumentOptions = {
             _forceActive: true, _dragOnlyWithinContainer: true, _embedContainer: Doc.MyFilesystem, _width: 30, _height: 30, undoIgnoreFields:new List<string>(['treeView_SortCriterion']),
@@ -650,7 +586,7 @@ pie title Minerals in my tap water
     /// initializes the left sidebar panel view of the UserDoc
     static setupUserDocView(doc: Doc, field:string) {
         const reqdOpts:DocumentOptions = {
-            _lockedPosition: true, _gridGap: 5, _forceActive: true, title: Doc.CurrentUserEmail +"-view",
+            _lockedPosition: true, _gridGap: 5, _forceActive: true, title: ClientUtils.CurrentUserEmail +"-view",
             layout_boxShadow: "0 0", childDontRegisterViews: true, dropAction: dropActionType.same, ignoreClick: true, isSystem: true,
             treeView_HideTitle: true, treeView_TruncateTitleWidth: 350
         };
@@ -671,7 +607,7 @@ pie title Minerals in my tap water
 
     static createToolButton = (opts: DocumentOptions) => Docs.Create.FontIconDocument({
         btnType: ButtonType.ToolButton, _dropPropertiesToRemove: new List<string>([ "layout_hideContextMenu"]), 
-        /*_nativeWidth: 40, _nativeHeight: 40, */ _width: 40, _height: 40, isSystem: true, ...opts,
+        /* _nativeWidth: 40, _nativeHeight: 40, */ _width: 40, _height: 40, isSystem: true, ...opts,
     })
 
     /// initializes the required buttons in the expanding button menu at the bottom of the Dash window
@@ -694,8 +630,8 @@ pie title Minerals in my tap water
             title: "docked buttons", _height: 40, flexGap: 0, layout_boxShadow: "standard", childDragAction: dropActionType.move,
             childDontRegisterViews: true, linearView_IsOpen: true, linearView_Expandable: true, ignoreClick: true
         };
-        reaction(() => UndoManager.redoStack.slice(), () => Doc.GetProto(btns.find(btn => btn.title === "Redo")!).opacity = UndoManager.CanRedo() ? 1 : 0.4, { fireImmediately: true });
-        reaction(() => UndoManager.undoStack.slice(), () => Doc.GetProto(btns.find(btn => btn.title === "Undo")!).opacity = UndoManager.CanUndo() ? 1 : 0.4, { fireImmediately: true });
+        reaction(() => UndoManager.redoStack.slice(), () => { Doc.GetProto(btns.find(btn => btn.title === "Redo")!).opacity = UndoManager.CanRedo() ? 1 : 0.4; }, { fireImmediately: true });
+        reaction(() => UndoManager.undoStack.slice(), () => { Doc.GetProto(btns.find(btn => btn.title === "Undo")!).opacity = UndoManager.CanUndo() ? 1 : 0.4; }, { fireImmediately: true });
         return DocUtils.AssignDocField(doc, field, (opts, items) => this.linearButtonList(opts, items??[]), dockBtnsReqdOpts, btns);
     }
 
@@ -752,9 +688,9 @@ pie title Minerals in my tap water
             { title: "Pen",    toolTip: "Pen (Ctrl+P)",                     btnType: ButtonType.ToggleButton, icon: "pen-nib",    toolType: "pen",                            scripts: {onClick:'{ return setActiveTool(this.toolType, false, _readOnly_);}' }},
             { title: "Write",  toolTip: "Write (Ctrl+Shift+P)",             btnType: ButtonType.ToggleButton, icon: "pen",        toolType: "write",                          scripts: {onClick:'{ return setActiveTool(this.toolType, false, _readOnly_);}' }, funcs: {hidden:"IsNoviceMode()" }},
             { title: "Eraser", toolTip: "Eraser (Ctrl+E)",                  btnType: ButtonType.ToggleButton, icon: "eraser",     toolType: "eraser",                         scripts: {onClick:'{ return setActiveTool(this.toolType, false, _readOnly_);}' }, funcs: {hidden:"IsNoviceMode()" }},
-            { title: "Circle", toolTip: "Circle (double tap to lock mode)", btnType: ButtonType.ToggleButton, icon: "circle",     toolType:GestureUtils.Gestures.Circle,      scripts: {onClick:`{ return setActiveTool(this.toolType, false, _readOnly_);}`, onDoubleClick:`{ return setActiveTool(this.toolType, true, _readOnly_);}`} },
-            { title: "Square", toolTip: "Square (double tap to lock mode)", btnType: ButtonType.ToggleButton, icon: "square",     toolType:GestureUtils.Gestures.Rectangle,   scripts: {onClick:`{ return setActiveTool(this.toolType, false, _readOnly_);}`, onDoubleClick:`{ return setActiveTool(this.toolType, true, _readOnly_);}`} },
-            { title: "Line",   toolTip: "Line (double tap to lock mode)",   btnType: ButtonType.ToggleButton, icon: "minus",      toolType:GestureUtils.Gestures.Line,        scripts: {onClick:`{ return setActiveTool(this.toolType, false, _readOnly_);}`, onDoubleClick:`{ return setActiveTool(this.toolType, true, _readOnly_);}`} },
+            { title: "Circle", toolTip: "Circle (double tap to lock mode)", btnType: ButtonType.ToggleButton, icon: "circle",     toolType: Gestures.Circle,      scripts: {onClick:`{ return setActiveTool(this.toolType, false, _readOnly_);}`, onDoubleClick:`{ return setActiveTool(this.toolType, true, _readOnly_);}`} },
+            { title: "Square", toolTip: "Square (double tap to lock mode)", btnType: ButtonType.ToggleButton, icon: "square",     toolType: Gestures.Rectangle,   scripts: {onClick:`{ return setActiveTool(this.toolType, false, _readOnly_);}`, onDoubleClick:`{ return setActiveTool(this.toolType, true, _readOnly_);}`} },
+            { title: "Line",   toolTip: "Line (double tap to lock mode)",   btnType: ButtonType.ToggleButton, icon: "minus",      toolType: Gestures.Line,        scripts: {onClick:`{ return setActiveTool(this.toolType, false, _readOnly_);}`, onDoubleClick:`{ return setActiveTool(this.toolType, true, _readOnly_);}`} },
             { title: "Mask",   toolTip: "Mask",                             btnType: ButtonType.ToggleButton, icon: "user-circle",toolType: "inkMask",                        scripts: {onClick:'{ return setInkProperty(this.toolType, value, _readOnly_);}'}, funcs: {hidden:"IsNoviceMode()" } },
             { title: "Labels", toolTip: "Lab els",                          btnType: ButtonType.ToggleButton, icon: "text-width", toolType: "labels",                         scripts: {onClick:'{ return setInkProperty(this.toolType, value, _readOnly_);}'}, },
             { title: "Width",  toolTip: "Stroke width",                     btnType: ButtonType.NumberSliderButton,               toolType: "strokeWidth", ignoreClick: true, scripts: {script: '{ return setInkProperty(this.toolType, value, _readOnly_);}'}, numBtnMin: 1},
@@ -835,13 +771,13 @@ pie title Minerals in my tap water
     
     static setupContextMenuBtn(params:Button, menuDoc:Doc):Doc {
         const menuBtnDoc = DocListCast(menuDoc?.data).find( doc => doc.title === params.title);
-        const subMenu = params.subMenu;
+        const {subMenu} = params;
         if (!subMenu) { // button does not have a sub menu
             return this.setupContextMenuButton(params, menuBtnDoc, menuDoc);
         }  
         // linear view
         const reqdSubMenuOpts = { ...OmitKeys(params, ["scripts", "funcs", "subMenu"]).omit, undoIgnoreFields: new List<string>(['width', "linearView_IsOpen"]),
-            childDontRegisterViews: true, flexGap: 0, _height: 30, ignoreClick: params.scripts?.onClick ? false : true,
+            childDontRegisterViews: true, flexGap: 0, _height: 30, ignoreClick: !params.scripts?.onClick,
             linearView_SubMenu: true, linearView_Expandable: true, embedContainer: menuDoc};
         
         const items = (menuBtnDoc?:Doc) => !menuBtnDoc ? [] : subMenu.map(sub => this.setupContextMenuBtn(sub, menuBtnDoc) );
@@ -873,10 +809,10 @@ pie title Minerals in my tap water
             { opts: { title: "Replicate",icon:"camera",toolTip: "Copy dashboard layout",btnType: ButtonType.ClickButton, expertMode: true},                                                                                              scripts: { onClick:  `snapshotDashboard()`}},
             { opts: { title: "Recordings",            toolTip: "Workspace Recordings", btnType: ButtonType.DropdownList,expertMode: false, ignoreClick: true, width: 100}, funcs: {hidden: `false`, btnList:`getWorkspaceRecordings()`}, scripts: { script:  `{ return replayWorkspace(value, _readOnly_); }`, onDragScript: `{ return startRecordingDrag(value); }`}},
             { opts: { title: "Stop Rec",icon: "stop", toolTip: "Stop recording",       btnType: ButtonType.ClickButton, expertMode: false}, funcs: {hidden: `!isWorkspaceRecording()`},                                                  scripts: { onClick: `stopWorkspaceRecording()`}},
-            { opts: { title: "Play",    icon: "play", toolTip: "Play recording",       btnType: ButtonType.ClickButton, expertMode: false}, funcs: {hidden: `isWorkspaceReplaying() !== "${media_state.Paused}"`},                       scripts: { onClick: `resumeWorkspaceReplaying(getCurrentRecording())`}},
-            { opts: { title: "Pause",   icon: "pause",toolTip: "Pause playback",       btnType: ButtonType.ClickButton, expertMode: false}, funcs: {hidden: `isWorkspaceReplaying() !== "${media_state.Playing}"`},                      scripts: { onClick: `pauseWorkspaceReplaying(getCurrentRecording())`}},
-            { opts: { title: "Stop",    icon: "stop", toolTip: "Stop playback",        btnType: ButtonType.ClickButton, expertMode: false}, funcs: {hidden: `isWorkspaceReplaying() !== "${media_state.Paused}"`},                       scripts: { onClick: `stopWorkspaceReplaying(getCurrentRecording())`}},
-            { opts: { title: "Delete",  icon: "trash",toolTip: "delete selected rec",  btnType: ButtonType.ClickButton, expertMode: false}, funcs: {hidden: `isWorkspaceReplaying() !== "${media_state.Paused}"`},                       scripts: { onClick: `removeWorkspaceReplaying(getCurrentRecording())`}}
+            { opts: { title: "Play",    icon: "play", toolTip: "Play recording",       btnType: ButtonType.ClickButton, expertMode: false}, funcs: {hidden: `isWorkspaceReplaying() !== "${mediaState.Paused}"`},                       scripts: { onClick: `resumeWorkspaceReplaying(getCurrentRecording())`}},
+            { opts: { title: "Pause",   icon: "pause",toolTip: "Pause playback",       btnType: ButtonType.ClickButton, expertMode: false}, funcs: {hidden: `isWorkspaceReplaying() !== "${mediaState.Playing}"`},                      scripts: { onClick: `pauseWorkspaceReplaying(getCurrentRecording())`}},
+            { opts: { title: "Stop",    icon: "stop", toolTip: "Stop playback",        btnType: ButtonType.ClickButton, expertMode: false}, funcs: {hidden: `isWorkspaceReplaying() !== "${mediaState.Paused}"`},                       scripts: { onClick: `stopWorkspaceReplaying(getCurrentRecording())`}},
+            { opts: { title: "Delete",  icon: "trash",toolTip: "delete selected rec",  btnType: ButtonType.ClickButton, expertMode: false}, funcs: {hidden: `isWorkspaceReplaying() !== "${mediaState.Paused}"`},                       scripts: { onClick: `removeWorkspaceReplaying(getCurrentRecording())`}}
         ];
         const btns = btnDescs.map(desc => dockBtn({_width: desc.opts.width??30, _height: 30, defaultDoubleClick: 'ignore', undoIgnoreFields: new List<string>(['opacity']), _dragOnlyWithinContainer: true, ...desc.opts}, desc.scripts, desc.funcs));
         const dockBtnsReqdOpts:DocumentOptions = {
@@ -890,12 +826,14 @@ pie title Minerals in my tap water
         return DocUtils.AssignDocField(doc, field, (opts) => Docs.Create.TreeDocument([], opts), { title: "published docs", backgroundColor: "#aca3a6", isSystem: true });
     }
     
-    /// The database of all links on all documents
+    static newAccount: boolean = false;
+
+    // The database of all links on all documents
     static setupLinkDocs(doc: Doc, linkDatabaseId: string) {
-        if (!(Docs.newAccount ? undefined : DocCast(doc.myLinkDatabase))) {
+        if (!(CurrentUserUtils.newAccount ? undefined : DocCast(doc.myLinkDatabase))) {
             const linkDocs = new Doc(linkDatabaseId, true);
-            linkDocs.title = "LINK DATABASE: " + Doc.CurrentUserEmail;
-            linkDocs.author = Doc.CurrentUserEmail;
+            linkDocs.title = "LINK DATABASE: " + ClientUtils.CurrentUserEmail;
+            linkDocs.author = ClientUtils.CurrentUserEmail;
             linkDocs.isSystem = true;
             linkDocs.data = new List<Doc>([]);
             linkDocs["acl-Guest"] = SharingPermissions.Augment;
@@ -949,16 +887,17 @@ pie title Minerals in my tap water
     /// Updates the UserDoc to have all required fields, docs, etc.  No changes should need to be
     /// written to the server if the code hasn't changed.  However, choices need to be made for each Doc/field
     /// whether to revert to "default" values, or to leave them as the user/system last set them.
-    static updateUserDocument(doc: Doc, sharingDocumentId: string, linkDatabaseId: string) {
+    static updateUserDocument(docIn: Doc, sharingDocumentId: string, linkDatabaseId: string) {
+        const doc = docIn;
         DocUtils.AssignDocField(doc, "globalGroupDatabase", () => Docs.Prototypes.MainGroupDocument(), {});
-        reaction(() => DateCast(DocCast(doc.globalGroupDatabase)["data_modificationDate"]),
+        reaction(() => DateCast(DocCast(doc.globalGroupDatabase).data_modificationDate),
             async () => {
                 const groups = await DocListCastAsync(DocCast(doc.globalGroupDatabase).data);
-                const mygroups = groups?.filter(group => JSON.parse(StrCast(group.members)).includes(Doc.CurrentUserEmail)) || [];
-                SetCachedGroups(["Guest", ...mygroups?.map(g => StrCast(g.title))]);
+                const mygroups = groups?.filter(group => JSON.parse(StrCast(group.members)).includes(ClientUtils.CurrentUserEmail)) || [];
+                SetCachedGroups(["Guest", ...(mygroups?.map(g => StrCast(g.title))??[])]);
             }, { fireImmediately: true });
         doc.isSystem ?? (doc.isSystem = true);
-        doc.title ?? (doc.title = Doc.CurrentUserEmail);
+        doc.title ?? (doc.title = ClientUtils.CurrentUserEmail);
         Doc.noviceMode ?? (Doc.noviceMode = true);
         doc._showLabel ?? (doc._showLabel = true);
         doc.textAlign ?? (doc.textAlign = "left");
@@ -971,7 +910,7 @@ pie title Minerals in my tap water
         doc.activeFillColor ?? (doc.activeFillColor = "");
         doc.activeArrowStart ?? (doc.activeArrowStart = "");
         doc.activeArrowEnd ?? (doc.activeArrowEnd = "");
-        doc.activeDash ?? (doc.activeDash == "0");
+        doc.activeDash ?? (doc.activeDash === "0");
         doc.fontSize ?? (doc.fontSize = "12px");
         doc.fontFamily ?? (doc.fontFamily = "Arial");
         doc.fontColor ?? (doc.fontColor = "black");
@@ -988,15 +927,14 @@ pie title Minerals in my tap water
         this.setupLinkDocs(doc, linkDatabaseId);
         this.setupSharedDocs(doc, sharingDocumentId);  // sets up the right sidebar collection for mobile upload documents and sharing
         this.setupDefaultIconTemplates(doc);  // creates a set of icon templates triggered by the document deoration icon
-        this.setupActiveMobileMenu(doc); // sets up the current mobile menu for Dash Mobile
         this.setupPublished(doc);  // sets up the list doc of all docs that have been published (meaning that they can be auto-linked by typing their title into another text box)
         this.setupContextMenuButtons(doc); // set up the row of buttons at the top of the dashboard that change depending on what is selected
         this.setupTopbarButtons(doc);
         this.setupDockedButtons(doc);  // the bottom bar of font icons
         this.setupLeftSidebarMenu(doc); // the left-side column of buttons that open their contents in a flyout panel on the left
         this.setupDocTemplates(doc); // sets up the template menu of templates
-        //this.setupFieldInfos(doc); // sets up the collection of field info descriptions for each possible DocumentOption
-        DocUtils.AssignDocField(doc, "globalScriptDatabase", (opts) =>  Docs.Prototypes.MainScriptDocument(), {});
+        // sthis.setupFieldInfos(doc); // sets up the collection of field info descriptions for each possible DocumentOption
+        DocUtils.AssignDocField(doc, "globalScriptDatabase", () =>  Docs.Prototypes.MainScriptDocument(), {});
         DocUtils.AssignDocField(doc, "myHeaderBar", (opts) => Docs.Create.MulticolumnDocument([], opts), { title: "My Header Bar", isSystem: true, _chromeHidden:true, layout_maxShown: 10, childLayoutFitWidth:false, childDocumentsActive:false, dropAction: dropActionType.move}); // drop down panel at top of dashboard for stashing documents
         
         Doc.AddDocToList(Doc.MyFilesystem, undefined, Doc.MyDashboards)
@@ -1005,10 +943,11 @@ pie title Minerals in my tap water
 
         Doc.GetProto(DocCast(Doc.UserDoc().emptyWebpage)).data = new WebField("https://www.wikipedia.org")
 
+        // eslint-disable-next-line no-new
         new LinkManager();
 
-        DocServer.CacheNeedsUpdate && setTimeout(DocServer.UPDATE_SERVER_CACHE, 2500);
-        setInterval(DocServer.UPDATE_SERVER_CACHE, 120000);
+        DocServer.CacheNeedsUpdate && setTimeout(UPDATE_SERVER_CACHE, 2500);
+        setInterval(UPDATE_SERVER_CACHE, 120000);
         return doc;
     }
     static setupFieldInfos(doc:Doc, field="fieldInfos") {
@@ -1032,11 +971,11 @@ pie title Minerals in my tap water
 
     @observable public static ServerVersion: string = ';'
     public static async loadCurrentUser() {
-        return rp.get(Utils.prepend("/getCurrentUser")).then(async response => {
+        return rp.get(ClientUtils.prepend("/getCurrentUser")).then(async response => {
             if (response) {
                 const result: { version: string, userDocumentId: string, sharingDocumentId: string, linkDatabaseId: string,  email: string, cacheDocumentIds: string, resolvedPorts: string } = JSON.parse(response);
-                runInAction(() => CurrentUserUtils.ServerVersion = result.version);
-                Doc.CurrentUserEmail = result.email;
+                runInAction(() => { CurrentUserUtils.ServerVersion = result.version; });
+                ClientUtils.CurrentUserEmail = result.email;
                 resolvedPorts = result.resolvedPorts as any;
                 DocServer.init(window.location.protocol, window.location.hostname, resolvedPorts?.socket, result.email);
                 if (result.cacheDocumentIds) 
@@ -1044,13 +983,13 @@ pie title Minerals in my tap water
                     const ids = result.cacheDocumentIds.split(";");
                     const batch = 30000;
                     for (let i = 0; i < ids.length; i += batch) {
+                        // eslint-disable-next-line no-await-in-loop
                         await DocServer.GetRefFields(ids.slice(i, i+batch));
                     }
                 }
                 return result;
-            } else {
-                throw new Error("There should be a user! Why does Dash think there isn't one?");
-            }
+            } 
+            throw new Error("There should be a user! Why does Dash think there isn't one?");
         });
     }
 
@@ -1060,12 +999,12 @@ pie title Minerals in my tap water
         linkDatabaseId: string;
     }) {
         return DocServer.GetRefField(info.userDocumentId).then(async field => {
-            Docs.newAccount = !(field instanceof Doc);
+            CurrentUserUtils.newAccount = !(field instanceof Doc);
             await Docs.Prototypes.initialize();
-            const userDoc = Docs.newAccount ? new Doc(info.userDocumentId, true) : field as Doc;
+            const userDoc = CurrentUserUtils.newAccount ? new Doc(info.userDocumentId, true) : field as Doc;
             this.updateUserDocument(Doc.SetUserDoc(userDoc), info.sharingDocumentId, info.linkDatabaseId);
-            if (Docs.newAccount) {
-                if (Doc.CurrentUserEmail === "guest") {
+            if (CurrentUserUtils.newAccount) {
+                if (ClientUtils.CurrentUserEmail === "guest") {
                     DashboardView.createNewDashboard(undefined, "guest dashboard");
                 } else {
                     userDoc.activePage = "home";
@@ -1082,14 +1021,10 @@ pie title Minerals in my tap water
         input.type = "file";
         input.multiple = true;
         input.accept = ".zip, application/pdf, video/*, image/*, audio/*";
-        input.onchange = async _e => {
+        input.onchange = async () => {
             const file = input.files?.[0];
             if (file?.type === 'application/zip' || file?.type === 'application/x-zip-compressed') {
                 const doc = await Doc.importDocument(file);
-                // NOT USING SOLR, so need to replace this with something else // if (doc instanceof Doc) {
-                //     setTimeout(() => SearchUtil.Search(`{!join from=id to=proto_i}id:link*`, true, {}).then(docs =>
-                //         docs.docs.forEach(d => LinkManager.Instance.addLink(d))), 2000); // need to give solr some time to update so that this query will find any link docs we've added.
-                // }
                 const list = Cast(Doc.MyImports.data, listSpec(Doc), null);
                 doc instanceof Doc && list?.splice(0, 0, doc);
             } else if (input.files && input.files.length !== 0) {
@@ -1109,10 +1044,17 @@ pie title Minerals in my tap water
     }
 }
 
-ScriptingGlobals.add(function MySharedDocs() { return Doc.MySharedDocs; }, "document containing all shared Docs");
-ScriptingGlobals.add(function IsExploreMode() { return SnappingManager.ExploreMode; }, "is Dash in exploration mode");
-ScriptingGlobals.add(function IsNoviceMode() { return Doc.noviceMode; }, "is Dash in novice mode");
+// eslint-disable-next-line prefer-arrow-callback
+ScriptingGlobals.add(function MySharedDocs()    { return Doc.MySharedDocs; }, "document containing all shared Docs");
+// eslint-disable-next-line prefer-arrow-callback
+ScriptingGlobals.add(function IsExploreMode()   { return SnappingManager.ExploreMode; }, "is Dash in exploration mode");
+// eslint-disable-next-line prefer-arrow-callback
+ScriptingGlobals.add(function IsNoviceMode()    { return Doc.noviceMode; }, "is Dash in novice mode");
+// eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function toggleComicMode() { Doc.UserDoc().renderStyle = Doc.UserDoc().renderStyle === "comic" ? undefined : "comic"; }, "switches between comic and normal document rendering");
-ScriptingGlobals.add(function importDocument() { return CurrentUserUtils.importDocument(); }, "imports files from device directly into the import sidebar");
+// eslint-disable-next-line prefer-arrow-callback
+ScriptingGlobals.add(function importDocument()  { return CurrentUserUtils.importDocument(); }, "imports files from device directly into the import sidebar");
+// eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function setInkToolDefaults() { Doc.ActiveTool = InkTool.None; });
-ScriptingGlobals.add(function getSharingDoc() {return Doc.SharingDoc() });
\ No newline at end of file
+// eslint-disable-next-line prefer-arrow-callback
+ScriptingGlobals.add(function getSharingDoc()   { return Doc.SharingDoc() });
\ No newline at end of file
diff --git a/src/client/util/DictationManager.ts b/src/client/util/DictationManager.ts
index 82c63695c..207d3ea0b 100644
--- a/src/client/util/DictationManager.ts
+++ b/src/client/util/DictationManager.ts
@@ -1,15 +1,15 @@
 import * as interpreter from 'words-to-numbers';
 // @ts-ignore  bcz: how are you supposed to include these definitions since dom-speech-recognition isn't a module?
 import type {} from '@types/dom-speech-recognition';
+import { ClientUtils } from '../../ClientUtils';
 import { Doc, Opt } from '../../fields/Doc';
 import { List } from '../../fields/List';
 import { RichTextField } from '../../fields/RichTextField';
 import { listSpec } from '../../fields/Schema';
 import { Cast, CastCtor, DocCast } from '../../fields/Types';
 import { AudioField, ImageField } from '../../fields/URLField';
-import { Utils } from '../../Utils';
-import { Docs } from '../documents/Documents';
 import { DocumentType } from '../documents/DocumentTypes';
+import { Docs } from '../documents/Documents';
 import { DictationOverlay } from '../views/DictationOverlay';
 import { DocumentView, OpenWhere } from '../views/nodes/DocumentView';
 import { SelectionManager } from './SelectionManager';
@@ -103,7 +103,7 @@ export namespace DictationManager {
                 results = await (pendingListen = listenImpl(options));
                 pendingListen = undefined;
                 if (results) {
-                    Utils.CopyText(results);
+                    ClientUtils.CopyText(results);
                     if (overlay) {
                         DictationOverlay.Instance.isListening = false;
                         const execute = options?.tryExecute;
diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts
index 40d28c690..67a61f17e 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -8,10 +8,9 @@ import { Cast, DocCast, NumCast, StrCast } from '../../fields/Types';
 import { AudioField } from '../../fields/URLField';
 import { GetEffectiveAcl } from '../../fields/util';
 import { CollectionViewType } from '../documents/DocumentTypes';
-import { CollectionDockingView } from '../views/collections/CollectionDockingView';
 import { TabDocView } from '../views/collections/TabDocView';
 import { LightboxView } from '../views/LightboxView';
-import { DocumentView, DocumentViewInternal, OpenWhere, OpenWhereMod } from '../views/nodes/DocumentView';
+import { DocumentView, DocumentViewInternal, OpenWhere } from '../views/nodes/DocumentView';
 import { FocusViewOptions } from '../views/nodes/FieldView';
 import { KeyValueBox } from '../views/nodes/KeyValueBox';
 import { LinkAnchorBox } from '../views/nodes/LinkAnchorBox';
@@ -20,12 +19,14 @@ import { ScriptingGlobals } from './ScriptingGlobals';
 import { SelectionManager } from './SelectionManager';
 
 export class DocumentManager {
+    // eslint-disable-next-line no-use-before-define
     private static _instance: DocumentManager;
     public static get Instance(): DocumentManager {
+        // eslint-disable-next-line no-return-assign
         return this._instance || (this._instance = new this());
     }
 
-    //global holds all of the nodes (regardless of which collection they're in)
+    // global holds all of the nodes (regardless of which collection they're in)
     @observable _documentViews = new Set<DocumentView>();
     @observable.shallow public CurrentlyLoading: Doc[] = [];
     @computed public get DocumentViews() {
@@ -38,7 +39,7 @@ export class DocumentManager {
         this._documentViews.delete(dv);
     }
 
-    //private constructor so no other class can create a nodemanager
+    // private constructor so no other class can create a nodemanager
     private constructor() {
         makeObservable(this);
         observe(this.CurrentlyLoading, change => {
@@ -52,6 +53,7 @@ export class DocumentManager {
                 case 'splice':
                     (change as any).removed.forEach((doc: Doc) => DocumentManager.Instance.getAllDocumentViews(doc).forEach(dv => StrCast(dv.Document.layout_fieldKey) === 'layout_icon' && dv.iconify(() => dv.iconify())));
                     break;
+                default:
             }
         });
     }
@@ -101,7 +103,7 @@ export class DocumentManager {
         SelectionManager.DeselectView(view);
     });
 
-    //gets all views
+    // gets all views
     public getDocumentViewsById(id: string) {
         const toReturn: DocumentView[] = [];
         DocumentManager.Instance.DocumentViews.forEach(view => {
@@ -135,25 +137,17 @@ export class DocumentManager {
         );
     }
 
-    public getLightboxDocumentView = (toFind: Doc, originatingDoc: Opt<Doc> = undefined): DocumentView | undefined => {
+    public getLightboxDocumentView = (toFind: Doc): DocumentView | undefined => {
         const views: DocumentView[] = [];
         DocumentManager.Instance.DocumentViews.forEach(view => LightboxView.Contains(view) && Doc.AreProtosEqual(view.Document, toFind) && views.push(view));
-        return views?.find(view => view.ContentDiv?.getBoundingClientRect().width /*&& view._props.focus !== returnFalse) || views?.find(view => view._props.focus !== returnFalse*/) || (views.length ? views[0] : undefined);
+        return views?.find(view => view.ContentDiv?.getBoundingClientRect().width /* && view._props.focus !== returnFalse) || views?.find(view => view._props.focus !== returnFalse */) || (views.length ? views[0] : undefined);
     };
-    public getFirstDocumentView = (toFind: Doc, originatingDoc: Opt<Doc> = undefined): DocumentView | undefined => {
-        if (LightboxView.LightboxDoc) return DocumentManager.Instance.getLightboxDocumentView(toFind, originatingDoc);
-        const views = this.getDocumentViews(toFind); //.filter(view => view.Document !== originatingDoc);
-        return views?.find(view => view.ContentDiv?.getBoundingClientRect().width /*&& view._props.focus !== returnFalse) || views?.find(view => view._props.focus !== returnFalse*/) || (views.length ? views[0] : undefined);
+    public getFirstDocumentView = (toFind: Doc): DocumentView | undefined => {
+        if (LightboxView.LightboxDoc) return DocumentManager.Instance.getLightboxDocumentView(toFind);
+        const views = this.getDocumentViews(toFind); // .filter(view => view.Document !== originatingDoc);
+        return views?.find(view => view.ContentDiv?.getBoundingClientRect().width /* && view._props.focus !== returnFalse) || views?.find(view => view._props.focus !== returnFalse */) || (views.length ? views[0] : undefined);
     };
-    public getDocumentViews(toFindIn: Doc): DocumentView[] {
-        const toFind =
-            // Array.from(DocumentManager.Instance.DocumentViews).find(
-            //     dv =>
-            //         ((dv.Document.data as any)?.url?.href && (dv.Document.data as any)?.url?.href === (toFindIn.data as any)?.url?.href) ||
-            //         ((DocCast(dv.Document.annotationOn)?.data as any)?.url?.href && (DocCast(dv.Document.annotationOn)?.data as any)?.url?.href === (DocCast(toFindIn.annotationOn)?.data as any)?.url?.href)
-            // )?.Document ??
-            toFindIn;
-
+    public getDocumentViews(toFind: Doc): DocumentView[] {
         const toReturn: DocumentView[] = [];
         const docViews = DocumentManager.Instance.DocumentViews.filter(view => !LightboxView.Contains(view));
         const lightViews = DocumentManager.Instance.DocumentViews.filter(view => LightboxView.Contains(view));
@@ -172,7 +166,7 @@ export class DocumentManager {
     static GetContextPath(doc: Opt<Doc>, includeExistingViews?: boolean) {
         if (!doc) return [];
         const srcContext = DocCast(doc.annotationOn, DocCast(doc.embedContainer));
-        var containerDocContext = srcContext ? [srcContext, doc] : [doc];
+        let containerDocContext = srcContext ? [srcContext, doc] : [doc];
         while (
             containerDocContext.length &&
             DocCast(containerDocContext[0]?.embedContainer) &&
@@ -204,10 +198,6 @@ export class DocumentManager {
         DocumentManager._overlayViews?.clear();
     }
     static _overlayViews = new ObservableSet<DocumentView>();
-    static addView = (doc: Doc, finished?: () => void) => {
-        CollectionDockingView.AddSplit(doc, OpenWhereMod.right);
-        finished?.();
-    };
 
     public static LinkCommonAncestor(linkDoc: Doc) {
         const anchor = (which: number) => {
@@ -228,7 +218,7 @@ export class DocumentManager {
     //           focusing on each context
     public showDocumentView = async (targetDocView: DocumentView, options: FocusViewOptions) => {
         const docViewPath = [...(targetDocView.containerViewPath?.() ?? []), targetDocView];
-        let rootContextView = docViewPath.shift();
+        const rootContextView = docViewPath.shift();
         await (rootContextView && this.focusViewsInPath(rootContextView, options, async () => ({ childDocView: docViewPath.shift(), viewSpec: undefined, focused: false })));
         if (options.toggleTarget && (!options.didMove || targetDocView.Document.hidden)) targetDocView.Document.hidden = !targetDocView.Document.hidden;
         else if (options.openLocation?.startsWith(OpenWhere.toggle) && !options.didMove && rootContextView) DocumentViewInternal.addDocTabFunc(rootContextView.Document, options.openLocation);
@@ -242,9 +232,10 @@ export class DocumentManager {
     //           and finally restoring the targetDoc to the viewSpec specified by the last document which may either be the targetDoc, or a viewSpec that describes the targetDoc configuration
     public showDocument = async (
         targetDoc: Doc, // document to display
-        options: FocusViewOptions, // options for how to navigate to target
+        optionsIn: FocusViewOptions, // options for how to navigate to target
         finished?: (changed: boolean) => void // func called after focusing on target with flag indicating whether anything needed to be done.
     ) => {
+        const options = optionsIn;
         Doc.RemoveDocFromList(Doc.MyRecentlyClosed, undefined, targetDoc);
         const docContextPath = DocumentManager.GetContextPath(targetDoc, true);
         if (docContextPath.some(doc => doc.hidden)) options.toggleTarget = false;
@@ -253,13 +244,14 @@ export class DocumentManager {
             options.toggleTarget = false;
             TabDocView.Activate(tabView?._document);
         }
-        let rootContextView =
+        const rootContextView =
             docContextPath.length &&
             (await new Promise<DocumentView>(res => {
                 const viewIndex = docContextPath.findIndex(doc => this.getDocumentView(doc));
                 if (viewIndex !== -1) {
                     viewIndex && docContextPath.splice(0, viewIndex);
-                    return res(this.getDocumentView(docContextPath[0])!);
+                    res(this.getDocumentView(docContextPath[0])!);
+                    return;
                 }
                 options.didMove = true;
                 (!LightboxView.LightboxDoc && docContextPath.some(doc => TabDocView.Activate(doc))) || DocumentViewInternal.addDocTabFunc(docContextPath[0], options.openLocation ?? OpenWhere.addRight);
@@ -270,7 +262,9 @@ export class DocumentManager {
             const target = DocCast(targetDoc.annotationOn, targetDoc);
             const contextView = this.getDocumentView(DocCast(target.embedContainer));
             if (contextView?.ComponentView?.addDocTab?.(target, OpenWhere.lightbox)) {
-                await new Promise<void>(waitres => setTimeout(() => waitres()));
+                await new Promise<void>(waitres => {
+                    setTimeout(() => waitres());
+                });
             }
         }
         docContextPath.shift();
@@ -287,19 +281,25 @@ export class DocumentManager {
     };
 
     focusViewsInPath = async (
-        docView: DocumentView, //
-        options: FocusViewOptions,
+        docViewIn: DocumentView, //
+        optionsIn: FocusViewOptions,
         iterator: (docView: DocumentView) => Promise<{ viewSpec: Opt<Doc>; childDocView: Opt<DocumentView>; focused: boolean }>
     ) => {
         let contextView: DocumentView | undefined; // view containing context that contains target
         let focused = false;
+        let docView = docViewIn;
+        const options = optionsIn;
         while (true) {
             if (docView.Document.layout_fieldKey === 'layout_icon') {
-                await new Promise<void>(res => docView.iconify(res));
+                // eslint-disable-next-line no-await-in-loop
+                await new Promise<any>(res => {
+                    docView.iconify(res);
+                });
                 options.didMove = true;
             }
             const nextFocus = docView._props.focus(docView.Document, options); // focus the view within its container
-            focused = focused || (nextFocus === undefined ? false : true); // keep track of whether focusing on a view needed to actually change anything
+            focused = focused || nextFocus !== undefined; // keep track of whether focusing on a view needed to actually change anything
+            // eslint-disable-next-line no-await-in-loop
             const { childDocView, viewSpec } = await iterator(docView);
             if (!childDocView) return { viewSpec: options.anchorDoc ?? viewSpec ?? docView.Document, docView, contextView, focused };
             contextView = options.anchorDoc?.layout_unrendered && !childDocView.Document.layout_unrendered ? childDocView : docView;
@@ -308,10 +308,11 @@ export class DocumentManager {
     };
 
     @action
-    restoreDocView(viewSpec: Opt<Doc>, docView: DocumentView, options: FocusViewOptions, contextView: Opt<DocumentView>, targetDoc: Doc) {
+    restoreDocView(viewSpec: Opt<Doc>, docViewIn: DocumentView, options: FocusViewOptions, contextView: Opt<DocumentView>, targetDoc: Doc) {
+        const docView = docViewIn;
         if (viewSpec && docView) {
-            //if (docView.ComponentView instanceof FormattedTextBox)
-            //viewSpec !== docView.Document &&
+            // if (docView.ComponentView instanceof FormattedTextBox)
+            // viewSpec !== docView.Document &&
             docView.ComponentView?.focus?.(viewSpec, options);
             PresBox.restoreTargetDocView(docView, viewSpec, options.zoomTime ?? 500);
             Doc.linkFollowHighlight(viewSpec ? [docView.Document, viewSpec] : docView.Document, undefined, options.effect);
@@ -332,7 +333,10 @@ export class DocumentManager {
         }
     }
 }
-export function DocFocusOrOpen(doc: Doc, options: FocusViewOptions = { willZoomCentered: true, zoomScale: 0, openLocation: OpenWhere.toggleRight }, containingDoc?: Doc) {
+// eslint-disable-next-line default-param-last
+export function DocFocusOrOpen(docIn: Doc, optionsIn: FocusViewOptions = { willZoomCentered: true, zoomScale: 0, openLocation: OpenWhere.toggleRight }, containingDoc?: Doc) {
+    let doc = docIn;
+    const options = optionsIn;
     const func = () => {
         const cv = DocumentManager.Instance.getDocumentView(containingDoc);
         const dv = DocumentManager.Instance.getDocumentView(doc, cv);
diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts
index 9627c5df2..62f055f1a 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -1,3 +1,5 @@
+/* eslint-disable import/no-mutable-exports */
+/* eslint-disable no-use-before-define */
 /**
  * The DragManager handles all dragging interactions that occur entirely within Dash (as opposed to external drag operations from the file system, etc)
  *
@@ -13,32 +15,25 @@
  */
 
 import { action, observable, runInAction } from 'mobx';
+import { ClientUtils } from '../../ClientUtils';
+import { emptyFunction } from '../../Utils';
 import { DateField } from '../../fields/DateField';
-import { Doc, Field, Opt, StrListCast } from '../../fields/Doc';
+import { Doc, FieldType, Opt, StrListCast } from '../../fields/Doc';
+import { DocData } from '../../fields/DocSymbols';
 import { List } from '../../fields/List';
 import { PrefetchProxy } from '../../fields/Proxy';
 import { ScriptField } from '../../fields/ScriptField';
 import { ScriptCast } from '../../fields/Types';
-import { emptyFunction, Utils } from '../../Utils';
-import { Docs, DocUtils } from '../documents/Documents';
+import { DocUtils, Docs } from '../documents/Documents';
 import { CollectionFreeFormDocumentView } from '../views/nodes/CollectionFreeFormDocumentView';
 import { DocumentView } from '../views/nodes/DocumentView';
+import { dropActionType } from './DropActionTypes';
 import { ScriptingGlobals } from './ScriptingGlobals';
 import { SelectionManager } from './SelectionManager';
 import { SnappingManager } from './SnappingManager';
 import { UndoManager } from './UndoManager';
-import { DocData } from '../../fields/DocSymbols';
-const { contextMenuZindex } = require('../views/global/globalCssVariables.module.scss'); // prettier-ignore
 
-export enum dropActionType {
-    embed = 'embed', // create a new embedding of the dragged document for the new location
-    copy = 'copy', // copy the dragged document
-    move = 'move', // move the dragged document to the drop location after removing it from where it was
-    add = 'add', // add the dragged document to the drop location without removing it from where it was
-    same = 'same', // only allow drop within same collection (or same hierarchical tree collection)
-    inPlace = 'inSame', // keep document in place (unless overridden by a drag modifier)
-    proto = 'proto',
-} // undefined = move, same = move but doesn't call dropPropertiesToRemove
+const { contextMenuZindex } = require('../views/global/globalCssVariables.module.scss'); // prettier-ignore
 
 /**
  * Initialize drag
@@ -83,6 +78,9 @@ export namespace DragManager {
     let dragLabel: HTMLDivElement;
     export let StartWindowDrag: Opt<(e: { pageX: number; pageY: number }, dragDocs: Doc[], finishDrag?: (aborted: boolean) => void) => boolean>;
     export let CompleteWindowDrag: Opt<(aborted: boolean) => void>;
+    export let AbortDrag: () => void = emptyFunction;
+    export const docsBeingDragged: Doc[] = observable([]);
+    export let DocDragData: DocumentDragData | undefined;
 
     export function Root() {
         const root = document.getElementById('root');
@@ -91,7 +89,6 @@ export namespace DragManager {
         }
         return root;
     }
-    export let AbortDrag: () => void = emptyFunction;
     export type MoveFunction = (document: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (document: Doc | Doc[]) => boolean) => boolean;
     export type RemoveFunction = (document: Doc | Doc[]) => boolean;
 
@@ -106,6 +103,7 @@ export namespace DragManager {
 
     // event called when the drag operation results in a drop action
     export class DropEvent {
+        // eslint-disable-next-line no-useless-constructor
         constructor(
             readonly x: number,
             readonly y: number,
@@ -115,7 +113,9 @@ export namespace DragManager {
             readonly metaKey: boolean,
             readonly ctrlKey: boolean,
             readonly embedKey: boolean
-        ) {}
+        ) {
+            /* empty */
+        }
     }
 
     // event called when the drag operation has completed (aborted or completed a drop) -- this will be after any drop event has been generated
@@ -194,7 +194,7 @@ export namespace DragManager {
         userDropAction?: dropActionType;
     }
 
-    let defaultPreDropFunc = (e: Event, de: DragManager.DropEvent, targetAction: dropActionType) => {
+    const defaultPreDropFunc = (e: Event, de: DragManager.DropEvent, targetAction: dropActionType) => {
         if (de.complete.docDragData) {
             targetAction && (de.complete.docDragData.dropAction = targetAction);
             e.stopPropagation();
@@ -228,7 +228,7 @@ export namespace DragManager {
             return dropDoc;
         };
         const finishDrag = async (e: DragCompleteEvent) => {
-            const docDragData = e.docDragData;
+            const { docDragData } = e;
             setTimeout(() => dragData.draggedViews.forEach(view => view.props.dragEnding?.()));
             onDropCompleted?.(e); // glr: optional additional function to be called - in this case with presentation trails
             if (docDragData && !docDragData.droppedDocuments.length) {
@@ -256,7 +256,9 @@ export namespace DragManager {
                         .forEach((drop: Doc, i: number) => {
                             const dragProps = StrListCast(dragData.draggedDocuments[i].dropPropertiesToRemove);
                             const remProps = (dragData?.dropPropertiesToRemove || []).concat(Array.from(dragProps));
-                            [...remProps, 'dropPropertiesToRemove'].map(prop => (drop[prop] = undefined));
+                            [...remProps, 'dropPropertiesToRemove'].forEach(prop => {
+                                drop[prop] = undefined;
+                            });
                         });
             }
             return e;
@@ -268,15 +270,18 @@ export namespace DragManager {
     }
 
     // drag a button template and drop a new button
-    export function StartButtonDrag(eles: HTMLElement[], script: string, title: string, vars: { [name: string]: Field }, params: string[], initialize: (button: Doc) => void, downX: number, downY: number, options?: DragOptions) {
+    export function StartButtonDrag(eles: HTMLElement[], script: string, title: string, vars: { [name: string]: FieldType }, params: string[], initialize: (button: Doc) => void, downX: number, downY: number, options?: DragOptions) {
         const finishDrag = (e: DragCompleteEvent) => {
             const bd = Docs.Create.ButtonDocument({ toolTip: title, z: 1, _width: 150, _height: 50, title, onClick: ScriptField.MakeScript(script) });
-            params.map(p => Object.keys(vars).indexOf(p) !== -1 && (bd[DocData][p] = new PrefetchProxy(vars[p] as Doc))); // copy all "captured" arguments into document parameterfields
+            params.forEach(p => {
+                Object.keys(vars).indexOf(p) !== -1 && (bd[DocData][p] = new PrefetchProxy(vars[p] as Doc));
+            }); // copy all "captured" arguments into document parameterfields
             initialize?.(bd);
             bd[DocData]['onClick-paramFieldKeys'] = new List<string>(params);
             e.docDragData && (e.docDragData.droppedDocuments = [bd]);
             return e;
         };
+        // eslint-disable-next-line no-param-reassign
         options = options ?? {};
         options.noAutoscroll = true; // these buttons are being dragged on the overlay layer, so scrollin the underlay is not appropriate
         StartDrag(eles, new DragManager.DocumentDragData([]), downX, downY, options, finishDrag);
@@ -298,7 +303,7 @@ export namespace DragManager {
     }
 
     export function snapDragAspect(dragPt: number[], snapAspect: number) {
-        let closest = Utils.SNAP_THRESHOLD;
+        let closest = ClientUtils.SNAP_THRESHOLD;
         let near = dragPt;
         const intersect = (x1: number, y1: number, x2: number, y2: number, x3: number, y3: number, x4: number, y4: number, dragx: number, dragy: number) => {
             if ((x1 === x2 && y1 === y2) || (x3 === x4 && y3 === y4)) return undefined; // Check if none of the lines are of length 0
@@ -307,7 +312,7 @@ export namespace DragManager {
 
             const ua = ((x4 - x3) * (y1 - y3) - (y4 - y3) * (x1 - x3)) / denominator;
             // let ub = ((x2 - x1) * (y1 - y3) - (y2 - y1) * (x1 - x3)) / denominator;
-            //if (ua < 0 || ua > 1 || ub < 0 || ub > 1)  return undefined;  // is the intersection along the segments
+            // if (ua < 0 || ua > 1 || ub < 0 || ub > 1)  return undefined;  // is the intersection along the segments
 
             // Return a object with the x and y coordinates of the intersection
             const x = x1 + ua * (x2 - x1);
@@ -315,14 +320,14 @@ export namespace DragManager {
             const dist = Math.sqrt((dragx - x) * (dragx - x) + (dragy - y) * (dragy - y));
             return { pt: [x, y], dist };
         };
-        SnappingManager.VertSnapLines.forEach((xCoord, i) => {
+        SnappingManager.VertSnapLines.forEach(xCoord => {
             const pt = intersect(dragPt[0], dragPt[1], dragPt[0] + snapAspect, dragPt[1] + 1, xCoord, -1, xCoord, 1, dragPt[0], dragPt[1]);
             if (pt && pt.dist < closest) {
                 closest = pt.dist;
                 near = pt.pt;
             }
         });
-        SnappingManager.HorizSnapLines.forEach((yCoord, i) => {
+        SnappingManager.HorizSnapLines.forEach(yCoord => {
             const pt = intersect(dragPt[0], dragPt[1], dragPt[0] + snapAspect, dragPt[1] + 1, -1, yCoord, 1, yCoord, dragPt[0], dragPt[1]);
             if (pt && pt.dist < closest) {
                 closest = pt.dist;
@@ -333,7 +338,7 @@ export namespace DragManager {
     }
     // snap to the active snap lines - if oneAxis is set (eg, for maintaining aspect ratios), then it only snaps to the nearest horizontal/vertical line
     export function snapDrag(e: PointerEvent, xFromLeft: number, yFromTop: number, xFromRight: number, yFromBottom: number) {
-        const snapThreshold = Utils.SNAP_THRESHOLD;
+        const snapThreshold = ClientUtils.SNAP_THRESHOLD;
         const snapVal = (pts: number[], drag: number, snapLines: number[]) => {
             if (snapLines.length) {
                 const offs = [pts[0], (pts[0] - pts[1]) / 2, -pts[1]]; // offsets from drag pt
@@ -350,14 +355,33 @@ export namespace DragManager {
             y: snapVal([yFromTop, yFromBottom], e.pageY, SnappingManager.HorizSnapLines),
         };
     }
-    export let docsBeingDragged: Doc[] = observable([]);
-    export let CanEmbed = false;
-    export let DocDragData: DocumentDragData | undefined;
-    export function StartDrag(eles: HTMLElement[], dragData: { [id: string]: any }, downX: number, downY: number, options?: DragOptions, finishDrag?: (dropData: DragCompleteEvent) => void, dragUndoName?: string) {
+
+    async function dispatchDrag(target: Element, e: PointerEvent, complete: DragCompleteEvent, pos: { x: number; y: number }, finishDrag?: (e: DragCompleteEvent) => void, options?: DragOptions, endDrag?: () => void) {
+        const dropArgs = {
+            cancelable: true, // allows preventDefault() to be called to cancel the drop
+            bubbles: true,
+            detail: {
+                ...pos,
+                complete,
+                shiftKey: e.shiftKey,
+                altKey: e.altKey,
+                metaKey: e.metaKey,
+                ctrlKey: e.ctrlKey,
+                embedKey: SnappingManager.CanEmbed,
+            },
+        };
+        target.dispatchEvent(new CustomEvent<DropEvent>('dashPreDrop', dropArgs));
+        UndoManager.StartTempBatch(); // run drag/drop in temp batch in case drop is not allowed (so we can undo any intermediate changes)
+        await finishDrag?.(complete);
+        UndoManager.EndTempBatch(target.dispatchEvent(new CustomEvent<DropEvent>('dashOnDrop', dropArgs))); // event return val is true unless the event preventDefault() is called
+        options?.dragComplete?.(complete);
+        endDrag?.();
+    }
+    export function StartDrag(elesIn: HTMLElement[], dragData: { [id: string]: any }, downX: number, downY: number, options?: DragOptions, finishDrag?: (dropData: DragCompleteEvent) => void, dragUndoName?: string) {
         if (dragData.dropAction === 'none' || SnappingManager.ExploreMode) return;
         DocDragData = dragData as DocumentDragData;
         const batch = UndoManager.StartBatch(dragUndoName ?? 'document drag');
-        eles = eles.filter(e => e);
+        const eles = elesIn.filter(e => e);
         SnappingManager.SetCanEmbed(dragData.canEmbed || false);
         if (!dragDiv) {
             dragDiv = document.createElement('div');
@@ -375,9 +399,9 @@ export namespace DragManager {
         }
         Object.assign(dragDiv.style, { width: '', height: '', overflow: '' });
         dragDiv.hidden = false;
-        const scalings: number[] = [],
-            xs: number[] = [],
-            ys: number[] = [];
+        const scalings: number[] = [];
+        const xs: number[] = [];
+        const ys: number[] = [];
 
         const elesCont = {
             left: Number.MAX_SAFE_INTEGER,
@@ -385,7 +409,7 @@ export namespace DragManager {
             top: Number.MAX_SAFE_INTEGER,
             bottom: Number.MIN_SAFE_INTEGER,
         };
-        let rot: number[] = [];
+        const rot: number[] = [];
         const docsToDrag = dragData instanceof DocumentDragData ? dragData.draggedDocuments : dragData instanceof AnchorAnnoDragData ? [dragData.dragDocument] : [];
         const dragElements = eles.map(ele => {
             // bcz: very hacky -- if dragged element is a freeForm view with a rotation, then extract the rotation in order to apply it to the dragged element
@@ -471,7 +495,9 @@ export namespace DragManager {
                     .filter(pb => pb.width && pb.height)
                     .map((pb, i) => pb.getContext('2d')!.drawImage(pdfBoxSrc[i], 0, 0));
             }
-            [dragElement, ...Array.from(dragElement.getElementsByTagName('*'))].forEach(ele => (ele as any).style && ((ele as any).style.pointerEvents = 'none'));
+            [dragElement, ...Array.from(dragElement.getElementsByTagName('*'))].forEach(ele => {
+                (ele as any).style && ((ele as any).style.pointerEvents = 'none');
+            });
 
             dragDiv.appendChild(dragElement);
             if (dragElement !== ele) {
@@ -493,7 +519,11 @@ export namespace DragManager {
         const hideDragShowOriginalElements = (hide: boolean) => {
             dragLabel.style.display = hide && !SnappingManager.CanEmbed ? '' : 'none';
             !hide && dragElements.map(dragElement => dragElement.parentNode === dragDiv && dragDiv.removeChild(dragElement));
-            setTimeout(() => eles.forEach(ele => (ele.hidden = hide)));
+            setTimeout(() =>
+                eles.forEach(ele => {
+                    ele.hidden = hide;
+                })
+            );
         };
         options?.hideSource && hideDragShowOriginalElements(true);
 
@@ -505,22 +535,7 @@ export namespace DragManager {
         const yFromBottom = elesCont.bottom - downY;
         let scrollAwaiter: Opt<NodeJS.Timeout>;
 
-        AbortDrag = () => {
-            options?.dragComplete?.(new DragCompleteEvent(true, dragData));
-            cleanupDrag(true);
-        };
-
-        const cleanupDrag = action((undo: boolean) => {
-            (dragData as DocumentDragData).draggedViews?.forEach(view => view.props.dragEnding?.());
-            hideDragShowOriginalElements(false);
-            document.removeEventListener('pointermove', moveHandler, true);
-            document.removeEventListener('pointerup', upHandler, true);
-            SnappingManager.SetIsDragging(false);
-            if (batch.end() && undo) UndoManager.Undo();
-            docsBeingDragged.length = 0;
-            SnappingManager.SetCanEmbed(false);
-        });
-        var startWindowDragTimer: any;
+        let startWindowDragTimer: any;
         const moveHandler = (e: PointerEvent) => {
             e.preventDefault(); // required or dragging text menu link item ends up dragging the link button as native drag/drop
             if (dragData instanceof DocumentDragData) {
@@ -580,10 +595,10 @@ export namespace DragManager {
                                 defaultPrevented: true,
                                 eventPhase: e.eventPhase,
                                 isTrusted: true,
-                                preventDefault: () => ('not implemented for this event' ? false : false),
-                                isDefaultPrevented: () => ('not implemented for this event' ? false : false),
-                                stopPropagation: () => ('not implemented for this event' ? false : false),
-                                isPropagationStopped: () => ('not implemented for this event' ? false : false),
+                                preventDefault: () => 'not implemented for this event' && false,
+                                isDefaultPrevented: () => 'not implemented for this event' && false,
+                                stopPropagation: () => 'not implemented for this event' && false,
+                                isPropagationStopped: () => 'not implemented for this event' && false,
                                 persist: emptyFunction,
                                 timeStamp: e.timeStamp,
                                 type: 'dashDragMovePause',
@@ -602,7 +617,9 @@ export namespace DragManager {
             const moveVec = { x: x - lastPt.x, y: y - lastPt.y };
             lastPt = { x, y };
 
-            dragElements.map((dragElement, i) => (dragElement.style.transform = `translate(${(xs[i] += moveVec.x)}px, ${(ys[i] += moveVec.y)}px) rotate(${rot[i]}deg)  scale(${scalings[i]})`));
+            dragElements.forEach((dragElement, i) => {
+                dragElement.style.transform = `translate(${(xs[i] += moveVec.x)}px, ${(ys[i] += moveVec.y)}px) rotate(${rot[i]}deg)  scale(${scalings[i]})`;
+            });
             dragLabel.style.transform = `translate(${xs[0]}px, ${ys[0] - 20}px)`;
         };
         const upHandler = (e: PointerEvent) => {
@@ -610,36 +627,32 @@ export namespace DragManager {
             startWindowDragTimer = undefined;
             dispatchDrag(document.elementFromPoint(e.x, e.y) || document.body, e, new DragCompleteEvent(false, dragData), snapDrag(e, xFromLeft, yFromTop, xFromRight, yFromBottom), finishDrag, options, () => cleanupDrag(false));
         };
+        const cleanupDrag = action((undo: boolean) => {
+            (dragData as DocumentDragData).draggedViews?.forEach(view => view.props.dragEnding?.());
+            hideDragShowOriginalElements(false);
+            document.removeEventListener('pointermove', moveHandler, true);
+            document.removeEventListener('pointerup', upHandler, true);
+            SnappingManager.SetIsDragging(false);
+            if (batch.end() && undo) UndoManager.Undo();
+            docsBeingDragged.length = 0;
+            SnappingManager.SetCanEmbed(false);
+        });
+        AbortDrag = () => {
+            options?.dragComplete?.(new DragCompleteEvent(true, dragData));
+            cleanupDrag(true);
+        };
         document.addEventListener('pointermove', moveHandler, true);
         document.addEventListener('pointerup', upHandler, true);
     }
-
-    async function dispatchDrag(target: Element, e: PointerEvent, complete: DragCompleteEvent, pos: { x: number; y: number }, finishDrag?: (e: DragCompleteEvent) => void, options?: DragOptions, endDrag?: () => void) {
-        const dropArgs = {
-            cancelable: true, // allows preventDefault() to be called to cancel the drop
-            bubbles: true,
-            detail: {
-                ...pos,
-                complete,
-                shiftKey: e.shiftKey,
-                altKey: e.altKey,
-                metaKey: e.metaKey,
-                ctrlKey: e.ctrlKey,
-                embedKey: SnappingManager.CanEmbed,
-            },
-        };
-        target.dispatchEvent(new CustomEvent<DropEvent>('dashPreDrop', dropArgs));
-        UndoManager.StartTempBatch(); // run drag/drop in temp batch in case drop is not allowed (so we can undo any intermediate changes)
-        await finishDrag?.(complete);
-        UndoManager.EndTempBatch(target.dispatchEvent(new CustomEvent<DropEvent>('dashOnDrop', dropArgs))); // event return val is true unless the event preventDefault() is called
-        options?.dragComplete?.(complete);
-        endDrag?.();
-    }
 }
 
+// eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function toggleRaiseOnDrag(readOnly?: boolean) {
     if (readOnly) {
         return SelectionManager.Views.some(dv => dv.Document.keepZWhenDragged);
     }
-    SelectionManager.Views.map(dv => (dv.Document.keepZWhenDragged = !dv.Document.keepZWhenDragged));
+    SelectionManager.Views.forEach(dv => {
+        dv.Document.keepZWhenDragged = !dv.Document.keepZWhenDragged;
+    });
+    return undefined;
 });
diff --git a/src/client/util/DropActionTypes.ts b/src/client/util/DropActionTypes.ts
new file mode 100644
index 000000000..45b294d97
--- /dev/null
+++ b/src/client/util/DropActionTypes.ts
@@ -0,0 +1,9 @@
+export enum dropActionType {
+    embed = 'embed', // create a new embedding of the dragged document for the new location
+    copy = 'copy', // copy the dragged document
+    move = 'move', // move the dragged document to the drop location after removing it from where it was
+    add = 'add', // add the dragged document to the drop location without removing it from where it was
+    same = 'same', // only allow drop within same collection (or same hierarchical tree collection)
+    inPlace = 'inSame', // keep document in place (unless overridden by a drag modifier)
+    proto = 'proto',
+} // undefined = move, same = move but doesn't call dropPropertiesToRemove
diff --git a/src/client/util/GroupManager.tsx b/src/client/util/GroupManager.tsx
index f4f879208..6b20b885b 100644
--- a/src/client/util/GroupManager.tsx
+++ b/src/client/util/GroupManager.tsx
@@ -5,19 +5,20 @@ import { observer } from 'mobx-react';
 import * as React from 'react';
 import Select from 'react-select';
 import * as RequestPromise from 'request-promise';
+import { ClientUtils } from '../../ClientUtils';
+import { Utils } from '../../Utils';
 import { DateField } from '../../fields/DateField';
 import { Doc, DocListCast, Opt } from '../../fields/Doc';
 import { Id } from '../../fields/FieldSymbols';
 import { listSpec } from '../../fields/Schema';
 import { Cast, StrCast } from '../../fields/Types';
-import { Utils } from '../../Utils';
 import { MainViewModal } from '../views/MainViewModal';
+import { ObservableReactComponent } from '../views/ObservableReactComponent';
 import { TaskCompletionBox } from '../views/nodes/TaskCompletedBox';
 import './GroupManager.scss';
 import { GroupMemberView } from './GroupMemberView';
 import { SettingsManager } from './SettingsManager';
 import { SharingManager, User } from './SharingManager';
-import { ObservableReactComponent } from '../views/ObservableReactComponent';
 
 /**
  * Interface for options for the react-select component
@@ -55,7 +56,7 @@ export class GroupManager extends ObservableReactComponent<{}> {
      */
     populateUsers = async () => {
         if (Doc.UserDoc()[Id] !== Utils.GuestID()) {
-            const userList = await RequestPromise.get(Utils.prepend('/getUsers'));
+            const userList = await RequestPromise.get(ClientUtils.prepend('/getUsers'));
             const raw = JSON.parse(userList) as User[];
             raw.map(action(user => !this.users.some(umail => umail === user.email) && this.users.push(user.email)));
         }
@@ -135,7 +136,7 @@ export class GroupManager extends ObservableReactComponent<{}> {
     hasEditAccess(groupDoc: Doc): boolean {
         if (!groupDoc) return false;
         const accessList: string[] = JSON.parse(StrCast(groupDoc.owners));
-        return accessList.includes(Doc.CurrentUserEmail) || this.adminGroupMembers?.includes(Doc.CurrentUserEmail);
+        return accessList.includes(ClientUtils.CurrentUserEmail) || this.adminGroupMembers?.includes(ClientUtils.CurrentUserEmail);
     }
 
     /**
@@ -147,7 +148,7 @@ export class GroupManager extends ObservableReactComponent<{}> {
         const name = groupName.toLowerCase() === 'admin' ? 'Admin' : groupName;
         const groupDoc = new Doc('GROUP:' + name, true);
         groupDoc.title = name;
-        groupDoc.owners = JSON.stringify([Doc.CurrentUserEmail]);
+        groupDoc.owners = JSON.stringify([ClientUtils.CurrentUserEmail]);
         groupDoc.members = JSON.stringify(memberEmails);
         this.addGroup(groupDoc);
     }
@@ -176,11 +177,11 @@ export class GroupManager extends ObservableReactComponent<{}> {
                 Doc.RemoveDocFromList(this.GroupManagerDoc, 'data', group);
                 SharingManager.Instance.removeGroup(group);
                 const members = JSON.parse(StrCast(group.members));
-                if (members.includes(Doc.CurrentUserEmail)) {
+                if (members.includes(ClientUtils.CurrentUserEmail)) {
                     const index = DocListCast(this.GroupManagerDoc.data).findIndex(grp => grp === group);
                     index !== -1 && Cast(this.GroupManagerDoc.data, listSpec(Doc), [])?.splice(index, 1);
                 }
-                this.GroupManagerDoc['data_modificationDate'] = new DateField();
+                this.GroupManagerDoc.data_modificationDate = new DateField();
                 if (group === this.currentGroup) {
                     this.currentGroup = undefined;
                 }
@@ -260,7 +261,10 @@ export class GroupManager extends ObservableReactComponent<{}> {
             alert('Please select a unique group name');
             return;
         }
-        this.createGroupDoc(value, this.selectedUsers?.map(user => user.value));
+        this.createGroupDoc(
+            value,
+            this.selectedUsers?.map(user => user.value)
+        );
         this.selectedUsers = null;
         this.inputRef.current!.value = '';
         this.buttonColour = '#979797';
diff --git a/src/client/util/History.ts b/src/client/util/History.ts
index 2f1a336cc..b500a5af9 100644
--- a/src/client/util/History.ts
+++ b/src/client/util/History.ts
@@ -1,6 +1,6 @@
 import * as qs from 'query-string';
 import { Doc } from '../../fields/Doc';
-import { OmitKeys, Utils } from '../../Utils';
+import { OmitKeys, ClientUtils } from '../../ClientUtils';
 import { DocServer } from '../DocServer';
 import { DashboardView } from '../views/DashboardView';
 
@@ -136,7 +136,7 @@ export namespace HistoryUtil {
 
     function addStringifier(type: string, keys: string[], customStringifier?: (state: ParsedUrl, current: string) => string) {
         stringifiers[type] = state => {
-            let path = Utils.prepend(`/${type}`);
+            let path = ClientUtils.prepend(`/${type}`);
             if (customStringifier) {
                 path = customStringifier(state, path);
             }
diff --git a/src/client/util/HypothesisUtils.ts b/src/client/util/HypothesisUtils.ts
index c5f307f44..d1600468a 100644
--- a/src/client/util/HypothesisUtils.ts
+++ b/src/client/util/HypothesisUtils.ts
@@ -1,5 +1,5 @@
 import { action, runInAction } from 'mobx';
-import { simulateMouseClick } from '../../Utils';
+import { simulateMouseClick } from '../../ClientUtils';
 import { Doc, Opt } from '../../fields/Doc';
 import { Cast, StrCast } from '../../fields/Types';
 import { WebField } from '../../fields/URLField';
@@ -33,7 +33,7 @@ export namespace Hypothesis {
         // await SearchUtil.Search('web', true).then(
         //     action(async (res: SearchUtil.DocSearchResult) => {
         //         const docs = res.docs;
-        //         const filteredDocs = docs.filter(doc => doc.author === Doc.CurrentUserEmail && doc.type === DocumentType.WEB && doc.data);
+        //         const filteredDocs = docs.filter(doc => doc.author === ClientUtils.CurrentUserEmail && doc.type === DocumentType.WEB && doc.data);
         //         filteredDocs.forEach(doc => {
         //             uri === Cast(doc.data, WebField)?.url.href && results.push(doc); // TODO check visited sites history?
         //         });
diff --git a/src/client/util/Import & Export/ImageUtils.ts b/src/client/util/Import & Export/ImageUtils.ts
index d99828956..dfad9755c 100644
--- a/src/client/util/Import & Export/ImageUtils.ts	
+++ b/src/client/util/Import & Export/ImageUtils.ts	
@@ -1,10 +1,10 @@
+import { ClientUtils } from '../../../ClientUtils';
 import { Doc } from '../../../fields/Doc';
+import { DocData } from '../../../fields/DocSymbols';
+import { Id } from '../../../fields/FieldSymbols';
+import { Cast, NumCast, StrCast } from '../../../fields/Types';
 import { ImageField } from '../../../fields/URLField';
-import { Cast, StrCast, NumCast } from '../../../fields/Types';
 import { Networking } from '../../Network';
-import { Id } from '../../../fields/FieldSymbols';
-import { Utils } from '../../../Utils';
-import { DocData } from '../../../fields/DocSymbols';
 
 export namespace ImageUtils {
     export type imgInfo = {
@@ -35,7 +35,7 @@ export namespace ImageUtils {
 
     export const ExportHierarchyToFileSystem = async (collection: Doc): Promise<void> => {
         const a = document.createElement('a');
-        a.href = Utils.prepend(`/imageHierarchyExport/${collection[Id]}`);
+        a.href = ClientUtils.prepend(`/imageHierarchyExport/${collection[Id]}`);
         a.download = `Dash Export [${StrCast(collection.title)}].zip`;
         a.click();
     };
diff --git a/src/client/util/InteractionUtils.tsx b/src/client/util/InteractionUtils.tsx
index a2f5826fe..a07550e09 100644
--- a/src/client/util/InteractionUtils.tsx
+++ b/src/client/util/InteractionUtils.tsx
@@ -1,6 +1,6 @@
 import * as React from 'react';
-import { GestureUtils } from '../../pen-gestures/GestureUtils';
 import { Utils } from '../../Utils';
+import { Gestures } from '../../pen-gestures/GestureTypes';
 import './InteractionUtils.scss';
 
 export namespace InteractionUtils {
@@ -9,81 +9,86 @@ export namespace InteractionUtils {
     export const PENTYPE = 'pen';
     export const ERASERTYPE = 'eraser';
 
-    const POINTER_PEN_BUTTON = -1;
-    const REACT_POINTER_PEN_BUTTON = 0;
     const ERASER_BUTTON = 5;
 
-    export class MultiTouchEvent<T extends React.TouchEvent | TouchEvent> {
-        constructor(
-            readonly fingers: number,
-            readonly targetTouches: T extends React.TouchEvent ? React.Touch[] : Touch[],
-            readonly touches: T extends React.TouchEvent ? React.Touch[] : Touch[],
-            readonly changedTouches: T extends React.TouchEvent ? React.Touch[] : Touch[],
-            readonly touchEvent: T extends React.TouchEvent ? React.TouchEvent : TouchEvent
-        ) {}
-    }
-
-    export interface MultiTouchEventDisposer {
-        (): void;
-    }
-
-    /**
-     *
-     * @param element - element to turn into a touch target
-     * @param startFunc - event handler, typically Touchable.onTouchStart (classes that inherit touchable can pass in this.onTouchStart)
-     */
-    export function MakeMultiTouchTarget(element: HTMLElement, startFunc: (e: Event, me: MultiTouchEvent<React.TouchEvent>) => void): MultiTouchEventDisposer {
-        const onMultiTouchStartHandler = (e: Event) => startFunc(e, (e as CustomEvent<MultiTouchEvent<React.TouchEvent>>).detail);
-        // const onMultiTouchMoveHandler = moveFunc ? (e: Event) => moveFunc(e, (e as CustomEvent<MultiTouchEvent<TouchEvent>>).detail) : undefined;
-        // const onMultiTouchEndHandler = endFunc ? (e: Event) => endFunc(e, (e as CustomEvent<MultiTouchEvent<TouchEvent>>).detail) : undefined;
-        element.addEventListener('dashOnTouchStart', onMultiTouchStartHandler);
-        // if (onMultiTouchMoveHandler) {
-        //     element.addEventListener("dashOnTouchMove", onMultiTouchMoveHandler);
-        // }
-        // if (onMultiTouchEndHandler) {
-        //     element.addEventListener("dashOnTouchEnd", onMultiTouchEndHandler);
-        // }
-        return () => {
-            element.removeEventListener('dashOnTouchStart', onMultiTouchStartHandler);
-            // if (onMultiTouchMoveHandler) {
-            //     element.removeEventListener("dashOnTouchMove", onMultiTouchMoveHandler);
-            // }
-            // if (onMultiTouchEndHandler) {
-            //     element.removeEventListener("dashOnTouchend", onMultiTouchEndHandler);
-            // }
-        };
-    }
-
-    /**
-     * Turns an element onto a target for touch hold handling.
-     * @param element - element to add events to
-     * @param func - function to add to the event
-     */
-    export function MakeHoldTouchTarget(element: HTMLElement, func: (e: Event, me: MultiTouchEvent<React.TouchEvent>) => void): MultiTouchEventDisposer {
-        const handler = (e: Event) => func(e, (e as CustomEvent<MultiTouchEvent<React.TouchEvent>>).detail);
-        element.addEventListener('dashOnTouchHoldStart', handler);
-        return () => {
-            element.removeEventListener('dashOnTouchHoldStart', handler);
-        };
-    }
-
-    export function GetMyTargetTouches(mte: InteractionUtils.MultiTouchEvent<React.TouchEvent | TouchEvent>, prevPoints: Map<number, React.Touch>, ignorePen: boolean): React.Touch[] {
-        const myTouches = new Array<React.Touch>();
-        for (const pt of mte.touches) {
-            if (!ignorePen || ((pt as any).radiusX > 1 && (pt as any).radiusY > 1)) {
-                for (const tPt of mte.targetTouches) {
-                    if (tPt?.screenX === pt?.screenX && tPt?.screenY === pt?.screenY) {
-                        if (pt && prevPoints.has(pt.identifier)) {
-                            myTouches.push(pt);
-                        }
-                    }
+    export function makePolygon(shape: string, points: { X: number; Y: number }[]) {
+        // if arrow/line/circle, the two end points should be the starting and the ending point
+        let left = points[0].X;
+        let top = points[0].Y;
+        let right = points[1].X;
+        let bottom = points[1].Y;
+        if (points.length > 1 && points[points.length - 1].X === points[0].X && points[points.length - 1].Y + 1 === points[0].Y) {
+            // pointer is up (first and last points are the same)
+            if (![Gestures.Arrow, Gestures.Line, Gestures.Circle].includes(shape as any as Gestures)) {
+                // otherwise take max and min
+                const xs = points.map(p => p.X);
+                const ys = points.map(p => p.Y);
+                right = Math.max(...xs);
+                left = Math.min(...xs);
+                bottom = Math.max(...ys);
+                top = Math.min(...ys);
+            }
+        } else {
+            // if in the middle of drawing
+            // take first and last points
+            right = points[points.length - 1].X;
+            left = points[0].X;
+            bottom = points[points.length - 1].Y;
+            top = points[0].Y;
+            if (shape !== Gestures.Arrow && shape !== Gestures.Line && shape !== Gestures.Circle) {
+                // switch left/right and top/bottom if needed
+                if (left > right) {
+                    const temp = right;
+                    right = left;
+                    left = temp;
+                }
+                if (top > bottom) {
+                    const temp = top;
+                    top = bottom;
+                    bottom = temp;
                 }
             }
         }
-        // if (mte.touches.length !== myTouches.length) {
-        //     throw Error("opo")
-        // }
-        return myTouches;
+        const polyPts = [];
+        switch (shape) {
+            case Gestures.Rectangle:
+                polyPts.push({ X: left, Y: top });
+                polyPts.push({ X: right, Y: top });
+                polyPts.push({ X: right, Y: bottom });
+                polyPts.push({ X: left, Y: bottom });
+                polyPts.push({ X: left, Y: top });
+                break;
+            case Gestures.Triangle:
+                polyPts.push({ X: left, Y: bottom });
+                polyPts.push({ X: right, Y: bottom });
+                polyPts.push({ X: (right + left) / 2, Y: top });
+                polyPts.push({ X: left, Y: bottom });
+                break;
+            case Gestures.Circle:
+                {
+                    const centerX = (Math.max(left, right) + Math.min(left, right)) / 2;
+                    const centerY = (Math.max(top, bottom) + Math.min(top, bottom)) / 2;
+                    const radius = Math.max(centerX - Math.min(left, right), centerY - Math.min(top, bottom));
+                    for (let x = centerX - radius; x < centerX + radius; x++) {
+                        const y = Math.sqrt(radius ** 2 - (x - centerX) ** 2) + centerY;
+                        polyPts.push({ X: x, Y: y });
+                    }
+                    for (let x = centerX + radius; x > centerX - radius; x--) {
+                        const y = Math.sqrt(radius ** 2 - (x - centerX) ** 2) + centerY;
+                        const newY = centerY - (y - centerY);
+                        polyPts.push({ X: x, Y: newY });
+                    }
+                    polyPts.push({ X: centerX - radius, Y: Math.sqrt(radius ** 2 - (-radius) ** 2) + centerY });
+                }
+                break;
+
+            case Gestures.Line:
+            default:
+                polyPts.push({ X: left, Y: top });
+                polyPts.push({ X: right, Y: bottom });
+                break;
+        }
+        return polyPts;
     }
 
     export function CreatePolyline(
@@ -101,20 +106,20 @@ export namespace InteractionUtils {
         arrowEnd: string,
         markerScale: number,
         dash: string | undefined,
-        scalex: number,
-        scaley: number,
+        scalexIn: number,
+        scaleyIn: number,
         shape: string,
         pevents: string,
         opacity: number,
         nodefs: boolean,
         downHdlr?: (e: React.PointerEvent) => void,
-        mask?: boolean,
-        dropshadow?: string
+        mask?: boolean
+        // dropshadow?: string
     ) {
         const pts = shape ? makePolygon(shape, points) : points;
 
-        if (isNaN(scalex)) scalex = 1;
-        if (isNaN(scaley)) scaley = 1;
+        const scalex = isNaN(scalexIn) ? 1 : scalexIn;
+        const scaley = isNaN(scaleyIn) ? 1 : scaleyIn;
 
         const toScr = (p: { X: number; Y: number }) => ` ${!p ? 0 : (p.X - left - width / 2) * scalex + width / 2}, ${!p ? 0 : (p.Y - top - width / 2) * scaley + width / 2} `;
         const strpts = bezier
@@ -137,7 +142,7 @@ export namespace InteractionUtils {
                     <defs>
                         {!mask ? null : (
                             <filter id={`mask${defGuid}`} x="-1" y="-1" width="500%" height="500%">
-                                <feGaussianBlur result="blurOut" in="offOut" stdDeviation="5"></feGaussianBlur>
+                                <feGaussianBlur result="blurOut" in="offOut" stdDeviation="5" />
                             </filter>
                         )}
                         {arrowStart !== 'dot' && arrowEnd !== 'dot' ? null : (
@@ -172,7 +177,7 @@ export namespace InteractionUtils {
                 <Tag
                     d={bezier ? strpts + (arrowStart || arrowEnd ? ' ' : '') : undefined}
                     points={bezier ? undefined : strpts}
-                    //filter={!dropshadow ? undefined : `drop-shadow(-1px -1px 0px ${dropshadow}) `}
+                    // filter={!dropshadow ? undefined : `drop-shadow(-1px -1px 0px ${dropshadow}) `}
                     style={{
                         // filter: drawHalo ? "url(#inkSelectionHalo)" : undefined,
                         fill: fill && fill !== 'transparent' ? fill : 'none',
@@ -193,83 +198,6 @@ export namespace InteractionUtils {
         );
     }
 
-    export function makePolygon(shape: string, points: { X: number; Y: number }[]) {
-        if (points.length > 1 && points[points.length - 1].X === points[0].X && points[points.length - 1].Y + 1 === points[0].Y) {
-            //pointer is up (first and last points are the same)
-            if (shape === GestureUtils.Gestures.Arrow || shape === GestureUtils.Gestures.Line || shape === GestureUtils.Gestures.Circle) {
-                //if arrow or line, the two end points should be the starting and the ending point
-                var left = points[0].X;
-                var top = points[0].Y;
-                var right = points[1].X;
-                var bottom = points[1].Y;
-            } else {
-                //otherwise take max and min
-                const xs = points.map(p => p.X);
-                const ys = points.map(p => p.Y);
-                right = Math.max(...xs);
-                left = Math.min(...xs);
-                bottom = Math.max(...ys);
-                top = Math.min(...ys);
-            }
-        } else {
-            //if in the middle of drawing
-            //take first and last points
-            right = points[points.length - 1].X;
-            left = points[0].X;
-            bottom = points[points.length - 1].Y;
-            top = points[0].Y;
-            if (shape !== GestureUtils.Gestures.Arrow && shape !== GestureUtils.Gestures.Line && shape !== GestureUtils.Gestures.Circle) {
-                //switch left/right and top/bottom if needed
-                if (left > right) {
-                    const temp = right;
-                    right = left;
-                    left = temp;
-                }
-                if (top > bottom) {
-                    const temp = top;
-                    top = bottom;
-                    bottom = temp;
-                }
-            }
-        }
-        points = [];
-        switch (shape) {
-            case GestureUtils.Gestures.Rectangle:
-                points.push({ X: left, Y: top });
-                points.push({ X: right, Y: top });
-                points.push({ X: right, Y: bottom });
-                points.push({ X: left, Y: bottom });
-                points.push({ X: left, Y: top });
-                break;
-            case GestureUtils.Gestures.Triangle:
-                points.push({ X: left, Y: bottom });
-                points.push({ X: right, Y: bottom });
-                points.push({ X: (right + left) / 2, Y: top });
-                points.push({ X: left, Y: bottom });
-                break;
-            case GestureUtils.Gestures.Circle:
-                const centerX = (Math.max(left, right) + Math.min(left, right)) / 2;
-                const centerY = (Math.max(top, bottom) + Math.min(top, bottom)) / 2;
-                const radius = Math.max(centerX - Math.min(left, right), centerY - Math.min(top, bottom));
-                for (var x = centerX - radius; x < centerX + radius; x++) {
-                    const y = Math.sqrt(Math.pow(radius, 2) - Math.pow(x - centerX, 2)) + centerY;
-                    points.push({ X: x, Y: y });
-                }
-                for (var x = centerX + radius; x > centerX - radius; x--) {
-                    const y = Math.sqrt(Math.pow(radius, 2) - Math.pow(x - centerX, 2)) + centerY;
-                    const newY = centerY - (y - centerY);
-                    points.push({ X: x, Y: newY });
-                }
-                points.push({ X: centerX - radius, Y: Math.sqrt(Math.pow(radius, 2) - Math.pow(-radius, 2)) + centerY });
-                break;
-
-            case GestureUtils.Gestures.Line:
-                points.push({ X: left, Y: top });
-                points.push({ X: right, Y: bottom });
-                break;
-        }
-        return points;
-    }
     /**
      * Returns whether or not the pointer event passed in is of the type passed in
      * @param e - pointer event. this event could be from a mouse, a pen, or a finger
@@ -279,10 +207,11 @@ export namespace InteractionUtils {
         // prettier-ignore
         switch (type) {
             // pen and eraser are both pointer type 'pen', but pen is button 0 and eraser is button 5. -syip2
-            case PENTYPE: return e.pointerType === PENTYPE && (e.button === -1 || e.button === 0);
+            case PENTYPE:    return e.pointerType === PENTYPE && (e.button === -1 || e.button === 0);
             case ERASERTYPE: return e.pointerType === PENTYPE && e.button === (e instanceof PointerEvent ? ERASER_BUTTON : ERASER_BUTTON);
-            case TOUCHTYPE: return e.pointerType === TOUCHTYPE;
-        }
+            case TOUCHTYPE:  return e.pointerType === TOUCHTYPE;
+            default: 
+        } // prettier-ignore
         return e.pointerType === type;
     }
 
@@ -292,7 +221,7 @@ export namespace InteractionUtils {
      * @param pt2
      */
     export function TwoPointEuclidist(pt1: React.Touch, pt2: React.Touch): number {
-        return Math.sqrt(Math.pow(pt1.clientX - pt2.clientX, 2) + Math.pow(pt1.clientY - pt2.clientY, 2));
+        return Math.sqrt((pt1.clientX - pt2.clientX) ** 2 + (pt1.clientY - pt2.clientY) ** 2);
     }
 
     /**
@@ -349,72 +278,4 @@ export namespace InteractionUtils {
         }
         return 0;
     }
-
-    export function IsDragging(oldTouches: Map<number, React.Touch>, newTouches: React.Touch[], leniency: number): boolean {
-        for (const touch of newTouches) {
-            if (touch) {
-                const oldTouch = oldTouches.get(touch.identifier);
-                if (oldTouch) {
-                    if (TwoPointEuclidist(touch, oldTouch) >= leniency) {
-                        return true;
-                    }
-                }
-            }
-        }
-        return false;
-    }
-
-    // These might not be very useful anymore, but I'll leave them here for now -syip2
-    {
-        /**
-         * Returns the type of Touch Interaction from a list of points.
-         * Also returns any data that is associated with a Touch Interaction
-         * @param pts - List of points
-         */
-        // export function InterpretPointers(pts: React.Touch[]): { type: Opt<TouchInteraction>, data?: any } {
-        //     const leniency = 200;
-        //     switch (pts.length) {
-        //         case 1:
-        //             return { type: OneFinger };
-        //         case 2:
-        //             return { type: TwoSeperateFingers };
-        //         case 3:
-        //             let pt1 = pts[0];
-        //             let pt2 = pts[1];
-        //             let pt3 = pts[2];
-        //             if (pt1 && pt2 && pt3) {
-        //                 let dist12 = TwoPointEuclidist(pt1, pt2);
-        //                 let dist23 = TwoPointEuclidist(pt2, pt3);
-        //                 let dist13 = TwoPointEuclidist(pt1, pt3);
-        //                 let dist12close = dist12 < leniency;
-        //                 let dist23close = dist23 < leniency;
-        //                 let dist13close = dist13 < leniency;
-        //                 let xor2313 = dist23close ? !dist13close : dist13close;
-        //                 let xor = dist12close ? !xor2313 : xor2313;
-        //                 // three input xor because javascript doesn't have logical xor's
-        //                 if (xor) {
-        //                     let points: number[] = [];
-        //                     let min = Math.min(dist12, dist23, dist13);
-        //                     switch (min) {
-        //                         case dist12:
-        //                             points = [0, 1, 2];
-        //                             break;
-        //                         case dist23:
-        //                             points = [1, 2, 0];
-        //                             break;
-        //                         case dist13:
-        //                             points = [0, 2, 1];
-        //                             break;
-        //                     }
-        //                     return { type: TwoToOneFingers, data: points };
-        //                 }
-        //                 else {
-        //                     return { type: ThreeSeperateFingers, data: null };
-        //                 }
-        //             }
-        //         default:
-        //             return { type: undefined };
-        //     }
-        // }
-    }
 }
diff --git a/src/client/util/LinkFollower.ts b/src/client/util/LinkFollower.ts
index 6c0bf3242..ba715aa1f 100644
--- a/src/client/util/LinkFollower.ts
+++ b/src/client/util/LinkFollower.ts
@@ -1,5 +1,5 @@
 import { action, runInAction } from 'mobx';
-import { Doc, DocListCast, Field, FieldResult, Opt } from '../../fields/Doc';
+import { Doc, DocListCast, FieldType, FieldResult, Opt } from '../../fields/Doc';
 import { ScriptField } from '../../fields/ScriptField';
 import { BoolCast, Cast, DocCast, NumCast, ScriptCast, StrCast } from '../../fields/Types';
 import { DocumentType } from '../documents/DocumentTypes';
@@ -138,6 +138,6 @@ ScriptingGlobals.add(function followLink(doc: Doc, altKey: boolean) {
 export function FollowLinkScript() {
     return ScriptField.MakeScript('return followLink(this,altKey)', { altKey: 'boolean' });
 }
-export function IsFollowLinkScript(field: FieldResult<Field>) {
+export function IsFollowLinkScript(field: FieldResult<FieldType>) {
     return ScriptCast(field)?.script.originalScript.includes('return followLink(');
 }
diff --git a/src/client/util/LinkManager.ts b/src/client/util/LinkManager.ts
index 8972bf705..82cd791cc 100644
--- a/src/client/util/LinkManager.ts
+++ b/src/client/util/LinkManager.ts
@@ -1,12 +1,16 @@
-import { action, makeObservable, observable, observe, runInAction } from 'mobx';
+import { action, makeObservable, observable, observe } from 'mobx';
 import { computedFn } from 'mobx-utils';
-import { Doc, DocListCast, DocListCastAsync, Field, Opt } from '../../fields/Doc';
+import * as rp from 'request-promise';
+import { ClientUtils } from '../../ClientUtils';
+import { Doc, DocListCast, DocListCastAsync, FieldType, Opt } from '../../fields/Doc';
 import { DirectLinks, DocData } from '../../fields/DocSymbols';
 import { FieldLoader } from '../../fields/FieldLoader';
+import { Id } from '../../fields/FieldSymbols';
 import { List } from '../../fields/List';
 import { ProxyField } from '../../fields/Proxy';
 import { Cast, DocCast, PromiseValue, StrCast } from '../../fields/Types';
 import { DocServer } from '../DocServer';
+import { DocumentType } from '../documents/DocumentTypes';
 import { ScriptingGlobals } from './ScriptingGlobals';
 /*
  * link doc:
@@ -45,6 +49,7 @@ export class LinkManager {
     constructor() {
         makeObservable(this);
         LinkManager._instance = this;
+        Doc.AddLink = this.addLink;
         this.createlink_relationshipLists();
         // since this is an action, not a reaction, we get only one shot to add this link to the Anchor docs
         // Thus make sure all promised values are resolved from link -> link.proto -> link.link_anchor_[1,2] -> link.link_anchor_[1,2].proto
@@ -84,7 +89,7 @@ export class LinkManager {
             );
 
         const watchUserLinkDB = (userLinkDBDoc: Doc) => {
-            const toRealField = (field: Field) => (field instanceof ProxyField ? field.value : field); // see List.ts.  data structure is not a simple list of Docs, but a list of ProxyField/Fields
+            const toRealField = (field: FieldType) => (field instanceof ProxyField ? field.value : field); // see List.ts.  data structure is not a simple list of Docs, but a list of ProxyField/Fields
             if (userLinkDBDoc.data) {
                 observe(
                     userLinkDBDoc.data,
@@ -148,7 +153,7 @@ export class LinkManager {
         Doc.AddDocToList(Doc.UserDoc(), 'links', linkDoc);
         if (!checkExists || !DocListCast(Doc.LinkDBDoc().data).includes(linkDoc)) {
             Doc.AddDocToList(Doc.LinkDBDoc(), 'data', linkDoc);
-            setTimeout(DocServer.UPDATE_SERVER_CACHE, 100);
+            setTimeout(UPDATE_SERVER_CACHE, 100);
         }
     }
     public deleteLink(linkDoc: Doc) {
@@ -240,6 +245,42 @@ export class LinkManager {
     }
 }
 
+let cacheDocumentIds = ''; // ; separate string of all documents ids in the user's working set (cached on the server)
+export function UPDATE_SERVER_CACHE() {
+    const prototypes = Object.values(DocumentType)
+        .filter(type => type !== DocumentType.NONE)
+        .map(type => DocServer._cache[type + 'Proto'])
+        .filter(doc => doc instanceof Doc)
+        .map(doc => doc as Doc);
+    const references = new Set<Doc>(prototypes);
+    Doc.FindReferences(Doc.UserDoc(), references, undefined);
+    DocListCast(DocCast(Doc.UserDoc().myLinkDatabase).data).forEach(link => {
+        if (!references.has(DocCast(link.link_anchor_1)) && !references.has(DocCast(link.link_anchor_2))) {
+            Doc.RemoveDocFromList(DocCast(Doc.UserDoc().myLinkDatabase), 'data', link);
+            Doc.AddDocToList(Doc.MyRecentlyClosed, undefined, link);
+        }
+    });
+    LinkManager.Instance.userLinkDBs.forEach(linkDb => Doc.FindReferences(linkDb, references, undefined));
+    const filtered = Array.from(references);
+
+    const newCacheUpdate = filtered.map(doc => doc[Id]).join(';');
+    if (newCacheUpdate === cacheDocumentIds) return;
+    cacheDocumentIds = newCacheUpdate;
+
+    // print out cached docs
+    Doc.MyDockedBtns.linearView_IsOpen && console.log('Set cached docs = ');
+    const isFiltered = filtered.filter(doc => !Doc.IsSystem(doc));
+    const strings = isFiltered.map(doc => StrCast(doc.title) + ' ' + (Doc.IsDataProto(doc) ? '(data)' : '(embedding)'));
+    Doc.MyDockedBtns.linearView_IsOpen && strings.sort().forEach((str, i) => console.log(i.toString() + ' ' + str));
+
+    rp.post(ClientUtils.prepend('/setCacheDocumentIds'), {
+        body: {
+            cacheDocumentIds,
+        },
+        json: true,
+    });
+}
+
 ScriptingGlobals.add(
     function links(doc: any) {
         return new List(LinkManager.Links(doc));
diff --git a/src/client/util/Scripting.ts b/src/client/util/Scripting.ts
index 422e708bc..de5e8b92e 100644
--- a/src/client/util/Scripting.ts
+++ b/src/client/util/Scripting.ts
@@ -4,12 +4,14 @@
 // // @ts-ignore
 // import * as typescriptes5 from '!!raw-loader!../../../node_modules/typescript/lib/lib.es5.d.ts'
 // @ts-ignore
-import * as typescriptlib from '!!raw-loader!./type_decls.d';
+// eslint-disable-next-line node/no-unpublished-import
 import * as ts from 'typescript';
-import { Doc, Field } from '../../fields/Doc';
+import * as typescriptlib from '!!raw-loader!./type_decls.d';
+import { Doc, FieldType } from '../../fields/Doc';
 import { RefField } from '../../fields/RefField';
 import { ScriptField } from '../../fields/ScriptField';
-import { scriptingGlobals, ScriptingGlobals } from './ScriptingGlobals';
+import { ScriptingGlobals, scriptingGlobals } from './ScriptingGlobals';
+
 export { ts };
 
 export interface ScriptSuccess {
@@ -30,6 +32,7 @@ export type ScriptParam = { [name: string]: string };
 export interface CompiledScript {
     readonly compiled: true;
     readonly originalScript: string;
+    // eslint-disable-next-line no-use-before-define
     readonly options: Readonly<ScriptOptions>;
     run(args?: { [name: string]: any }, onError?: (res: any) => void, errorVal?: any): ScriptResult;
 }
@@ -47,6 +50,7 @@ export function isCompileError(toBeDetermined: CompileResult): toBeDetermined is
     return false;
 }
 
+// eslint-disable-next-line no-use-before-define
 function Run(script: string | undefined, customParams: string[], diagnostics: any[], originalScript: string, options: ScriptOptions): CompileResult {
     const errors = diagnostics.filter(diag => diag.category === ts.DiagnosticCategory.Error);
     if ((options.typecheck !== false && errors.length) || !script) {
@@ -60,6 +64,7 @@ function Run(script: string | undefined, customParams: string[], diagnostics: an
     // let params: any[] = [Docs, ...fieldTypes];
     const compiledFunction = (() => {
         try {
+            // eslint-disable-next-line no-new-func
             return new Function(...paramNames, `return ${script}`);
         } catch (e) {
             console.log(e);
@@ -68,20 +73,17 @@ function Run(script: string | undefined, customParams: string[], diagnostics: an
     })();
     if (!compiledFunction) return { compiled: false, errors };
     const { capturedVariables = {} } = options;
+    // eslint-disable-next-line default-param-last
     const run = (args: { [name: string]: any } = {}, onError?: (e: any) => void, errorVal?: any): ScriptResult => {
         const argsArray: any[] = [];
+        // eslint-disable-next-line no-restricted-syntax
         for (const name of customParams) {
-            if (name === 'this') {
-                continue;
-            }
-            if (name in args) {
-                argsArray.push(args[name]);
-            } else {
-                argsArray.push(capturedVariables[name]);
+            if (name !== 'this') {
+                argsArray.push(name in args ? args[name] : capturedVariables[name]);
             }
         }
         const thisParam = args.this || capturedVariables.this;
-        let batch: { end(): void } | undefined = undefined;
+        let batch: { end(): void } | undefined;
         try {
             if (!options.editable) {
                 batch = Doc.MakeReadOnly();
@@ -109,7 +111,7 @@ class ScriptingCompilerHost {
     files: File[] = [];
 
     // getSourceFile(fileName: string, languageVersion: ts.ScriptTarget, onError?: ((message: string) => void) | undefined, shouldCreateNewSourceFile?: boolean | undefined): ts.SourceFile | undefined {
-    getSourceFile(fileName: string, languageVersion: any, onError?: ((message: string) => void) | undefined, shouldCreateNewSourceFile?: boolean | undefined): any | undefined {
+    getSourceFile(fileName: string, languageVersion: any /* , onError?: ((message: string) => void) | undefined, shouldCreateNewSourceFile?: boolean | undefined */): any | undefined {
         const contents = this.readFile(fileName);
         if (contents !== undefined) {
             return ts.createSourceFile(fileName, contents, languageVersion, true);
@@ -118,7 +120,7 @@ class ScriptingCompilerHost {
     }
 
     // getDefaultLibFileName(options: ts.CompilerOptions): string {
-    getDefaultLibFileName(options: any): string {
+    getDefaultLibFileName(/* options: any */): string {
         return 'node_modules/typescript/lib/lib.d.ts'; // No idea what this means...
     }
     writeFile(fileName: string, content: string) {
@@ -157,7 +159,7 @@ export type Traverser = (node: ts.Node, indentation: string) => boolean | void;
 export type TraverserParam = Traverser | { onEnter: Traverser; onLeave: Traverser };
 export type Transformer = {
     transformer: ts.TransformerFactory<ts.Node>;
-    getVars?: () => { [name: string]: Field };
+    getVars?: () => { [name: string]: FieldType };
 };
 export interface ScriptOptions {
     requiredType?: string; // does function required a typed return value
@@ -210,12 +212,13 @@ export function CompileScript(script: string, options: ScriptOptions = {}): Comp
         const newCaptures = options.transformer.getVars?.();
         if (Object.keys(newCaptures ?? {}).length) {
             // tslint:disable-next-line: prefer-object-spread
-            //options.capturedVariables = Object.assign(capturedVariables, newCaptures!) as any;
+            // options.capturedVariables = Object.assign(capturedVariables, newCaptures!) as any;
 
-            const transformed = result.transformed;
+            const { transformed } = result;
             const printer = ts.createPrinter({
                 newLine: ts.NewLineKind.LineFeed,
             });
+            // eslint-disable-next-line no-param-reassign
             script = printer.printFile(transformed[0].getSourceFile());
         }
         result.dispose();
@@ -224,19 +227,23 @@ export function CompileScript(script: string, options: ScriptOptions = {}): Comp
     if ('this' in params || 'this' in capturedVariables) {
         paramNames.push('this');
     }
+    // eslint-disable-next-line no-restricted-syntax
     for (const key in params) {
-        if (key === 'this') continue;
-        paramNames.push(key);
+        if (key !== 'this') {
+            paramNames.push(key);
+        }
     }
     const paramList = paramNames.map(key => {
         const val = params[key];
         return `${key}: ${val}`;
     });
+    // eslint-disable-next-line no-restricted-syntax
     for (const key in capturedVariables) {
-        if (key === 'this') continue;
-        const val = capturedVariables[key];
-        paramNames.push(key);
-        paramList.push(`${key}: ${typeof val === 'object' ? Object.getPrototypeOf(val).constructor.name : typeof val}`);
+        if (key !== 'this') {
+            const val = capturedVariables[key];
+            paramNames.push(key);
+            paramList.push(`${key}: ${typeof val === 'object' ? Object.getPrototypeOf(val).constructor.name : typeof val}`);
+        }
     }
     const paramString = paramList.join(', ');
     const body = addReturn && !script.startsWith('{ return') ? `return ${script};` : script;
@@ -261,6 +268,7 @@ export function CompileScript(script: string, options: ScriptOptions = {}): Comp
 }
 
 ScriptingGlobals.add(CompileScript);
+// eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function runScript(doc: Doc, script: ScriptField) {
     return script?.script.run({ this: doc }).result;
 });
diff --git a/src/client/util/ScriptingGlobals.ts b/src/client/util/ScriptingGlobals.ts
index f151acd81..bc159ed65 100644
--- a/src/client/util/ScriptingGlobals.ts
+++ b/src/client/util/ScriptingGlobals.ts
@@ -1,8 +1,18 @@
+import ts from 'typescript';
 
-import * as ts from "typescript";
 export { ts };
 
+const _scriptingGlobals: { [name: string]: any } = {};
+const _scriptingDescriptions: { [name: string]: any } = {};
+const _scriptingParams: { [name: string]: any } = {};
+// eslint-disable-next-line import/no-mutable-exports
+export let scriptingGlobals: { [name: string]: any } = _scriptingGlobals;
 export namespace ScriptingGlobals {
+    export function getGlobals()      { return Object.keys(_scriptingGlobals); } // prettier-ignore
+    export function getGlobalObj()    { return _scriptingGlobals; } // prettier-ignore
+    export function getDescriptions() { return _scriptingDescriptions; } // prettier-ignore
+    export function getParameters()   { return _scriptingParams; } // prettier-ignore
+
     export function add(global: { name: string }): void;
     export function add(name: string, global: any): void;
     export function add(global: { name: string }, decription?: string, params?: string): void;
@@ -11,7 +21,7 @@ export namespace ScriptingGlobals {
         let obj: any;
 
         if (second !== undefined) {
-            if (typeof first === "string") {
+            if (typeof first === 'string') {
                 n = first;
                 obj = second;
             } else {
@@ -22,18 +32,20 @@ export namespace ScriptingGlobals {
                     _scriptingParams[n] = third;
                 }
             }
-        } else if (first && typeof first.name === "string") {
+        } else if (first && typeof first.name === 'string') {
             n = first.name;
             obj = first;
         } else {
-            throw new Error("Must either register an object with a name, or give a name and an object");
+            throw new Error('Must either register an object with a name, or give a name and an object');
         }
-        if (n === undefined || n === "undefined") {
+        if (n === undefined || n === 'undefined') {
             return false;
-        } else if (_scriptingGlobals.hasOwnProperty(n)) {
+        }
+        if (_scriptingGlobals.hasOwnProperty(n)) {
             throw new Error(`Global with name ${n} is already registered, choose another name`);
         }
         _scriptingGlobals[n] = obj;
+        return true;
     }
     export function makeMutableGlobalsCopy(globals?: { [name: string]: any }) {
         return { ..._scriptingGlobals, ...(globals || {}) };
@@ -57,25 +69,16 @@ export namespace ScriptingGlobals {
         return false;
     }
 
-    export function resetScriptingGlobals() { scriptingGlobals = _scriptingGlobals; }
+    export function resetScriptingGlobals() {
+        scriptingGlobals = _scriptingGlobals;
+    }
 
     // const types = Object.keys(ts.SyntaxKind).map(kind => ts.SyntaxKind[kind]);
-    export function printNodeType(node: any, indentation = "") { console.log(indentation + ts.SyntaxKind[node.kind]); }
-
-    export function getGlobals() { return Object.keys(_scriptingGlobals); }
-
-    export function getGlobalObj() { return _scriptingGlobals; }
-
-    export function getDescriptions() { return _scriptingDescriptions; }
-
-    export function getParameters() { return _scriptingParams; }
+    export function printNodeType(node: any, indentation = '') {
+        console.log(indentation + ts.SyntaxKind[node.kind]);
+    }
 }
 
-export function scriptingGlobal(constructor: { new(...args: any[]): any }) {
+export function scriptingGlobal(constructor: { new (...args: any[]): any }) {
     ScriptingGlobals.add(constructor);
 }
-
-const _scriptingGlobals: { [name: string]: any } = {};
-export let scriptingGlobals: { [name: string]: any } = _scriptingGlobals;
-const _scriptingDescriptions: { [name: string]: any } = {};
-const _scriptingParams: { [name: string]: any } = {};
\ No newline at end of file
diff --git a/src/client/util/SearchUtil.ts b/src/client/util/SearchUtil.ts
index fff2737b6..65b9a977d 100644
--- a/src/client/util/SearchUtil.ts
+++ b/src/client/util/SearchUtil.ts
@@ -1,5 +1,5 @@
 import { ObservableMap } from 'mobx';
-import { Doc, DocListCast, Field, Opt } from '../../fields/Doc';
+import { Doc, DocListCast, Field, FieldType, Opt } from '../../fields/Doc';
 import { Id } from '../../fields/FieldSymbols';
 import { StrCast } from '../../fields/Types';
 import { DocumentType } from '../documents/DocumentTypes';
@@ -30,7 +30,7 @@ export namespace SearchUtil {
                     (onlyKeys ?? SearchUtil.documentKeys(doc)).forEach(
                         key =>
                             (val => (exact ? val === query.toLowerCase() : val.includes(query.toLowerCase())))(
-                            matchKeyNames ? key : Field.toString(doc[key] as Field)) 
+                            matchKeyNames ? key : Field.toString(doc[key] as FieldType)) 
                         && hlights.add(key)
                     ); // prettier-ignore
                     blockedKeys.forEach(key => hlights.delete(key));
@@ -71,7 +71,7 @@ export namespace SearchUtil {
             docs.filter(d => d && !visited.includes(d)).forEach(d => {
                 visited.push(d);
                 const fieldKey = Doc.LayoutFieldKey(d);
-                const annos = !Field.toString(Doc.LayoutField(d) as Field).includes('CollectionView');
+                const annos = !Field.toString(Doc.LayoutField(d) as FieldType).includes('CollectionView');
                 const data = d[annos ? fieldKey + '_annotations' : fieldKey];
                 data && newarray.push(...DocListCast(data));
                 const sidebar = d[fieldKey + '_sidebar'];
diff --git a/src/client/util/SerializationHelper.ts b/src/client/util/SerializationHelper.ts
index 8daa69890..fa1a911f7 100644
--- a/src/client/util/SerializationHelper.ts
+++ b/src/client/util/SerializationHelper.ts
@@ -1,5 +1,5 @@
 import { PropSchema, serialize, deserialize, custom, setDefaultModelSchema, getDefaultModelSchema } from 'serializr';
-import { Field } from '../../fields/Doc';
+// import { Field } from '../../fields/Doc';
 
 let serializing = 0;
 export function afterDocDeserialize(cb: (err: any, val: any) => void, err: any, newValue: any) {
@@ -7,12 +7,16 @@ export function afterDocDeserialize(cb: (err: any, val: any) => void, err: any,
     cb(err, newValue);
     serializing--;
 }
+
+const serializationTypes: { [name: string]: { ctor: { new (): any }; afterDeserialize?: (obj: any) => void | Promise<any> } } = {};
+const reverseMap: { [ctor: string]: string } = {};
+
 export namespace SerializationHelper {
     export function IsSerializing() {
         return serializing > 0;
     }
 
-    export function Serialize(obj: Field): any {
+    export function Serialize(obj: any /* Field */): any {
         if (obj === undefined || obj === null) {
             return null;
         }
@@ -24,7 +28,7 @@ export namespace SerializationHelper {
         serializing++;
         if (!(obj.constructor.name in reverseMap)) {
             serializing--;
-            throw Error('Error: ' + `type '${obj.constructor.name}' not registered. Make sure you register it using a @Deserializable decorator`);
+            throw Error(`Error: type '${obj.constructor.name}' not registered. Make sure you register it using a @Deserializable decorator`);
         }
 
         const json = serialize(obj);
@@ -52,25 +56,24 @@ export namespace SerializationHelper {
         }
 
         const type = serializationTypes[obj.__type];
-        const value = await new Promise(res => deserialize(type.ctor, obj, (err, result) => res(result)));
+        const value = await new Promise(res => {
+            deserialize(type.ctor, obj, (err, result) => res(result));
+        });
         type.afterDeserialize?.(value);
 
         return value;
     }
 }
 
-const serializationTypes: { [name: string]: { ctor: { new (): any }; afterDeserialize?: (obj: any) => void | Promise<any> } } = {};
-const reverseMap: { [ctor: string]: string } = {};
-
 export function Deserializable(className: string, afterDeserialize?: (obj: any) => void | Promise<any>, constructorArgs?: [string]): (constructor: { new (...args: any[]): any }) => void {
-    function addToMap(className: string, ctor: { new (...args: any[]): any }) {
-        const schema = getDefaultModelSchema(ctor) as any;
-        if (schema.targetClass !== ctor || constructorArgs) {
-            setDefaultModelSchema(ctor, { ...schema, factory: (context: any) => new ctor(...(constructorArgs ?? [])?.map(arg => context.json[arg])) });
+    function addToMap(className: string, Ctor: { new (...args: any[]): any }) {
+        const schema = getDefaultModelSchema(Ctor) as any;
+        if (schema.targetClass !== Ctor || constructorArgs) {
+            setDefaultModelSchema(Ctor, { ...schema, factory: (context: any) => new Ctor(...(constructorArgs ?? []).map(arg => context.json[arg])) });
         }
         if (!(className in serializationTypes)) {
-            serializationTypes[className] = { ctor, afterDeserialize };
-            reverseMap[ctor.name] = className;
+            serializationTypes[className] = { ctor: Ctor, afterDeserialize };
+            reverseMap[Ctor.name] = className;
         } else {
             throw new Error(`Name ${className} has already been registered as deserializable`);
         }
diff --git a/src/client/util/SettingsManager.tsx b/src/client/util/SettingsManager.tsx
index 8594a1c92..e2971895a 100644
--- a/src/client/util/SettingsManager.tsx
+++ b/src/client/util/SettingsManager.tsx
@@ -1,11 +1,11 @@
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { Button, ColorPicker, Dropdown, DropdownType, EditableText, Group, NumberDropdown, Size, Toggle, ToggleType, Type } from 'browndash-components';
-import { action, computed, makeObservable, observable, runInAction } from 'mobx';
+import { action, computed, makeObservable, observable, reaction, runInAction } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
 import { BsGoogle } from 'react-icons/bs';
 import { FaFillDrip, FaPalette } from 'react-icons/fa';
-import { Utils, addStyleSheet, addStyleSheetRule } from '../../Utils';
+import { ClientUtils, addStyleSheet, addStyleSheetRule } from '../../ClientUtils';
 import { Doc, Opt } from '../../fields/Doc';
 import { DashVersion } from '../../fields/DocSymbols';
 import { BoolCast, Cast, NumCast, StrCast } from '../../fields/Types';
@@ -18,6 +18,7 @@ import { FontIconBox } from '../views/nodes/FontIconBox/FontIconBox';
 import { GroupManager } from './GroupManager';
 import './SettingsManager.scss';
 import { undoBatch } from './UndoManager';
+import { SnappingManager } from './SnappingManager';
 
 export enum ColorScheme {
     Dark = 'Dark',
@@ -34,6 +35,7 @@ export enum freeformScrollMode {
 
 @observer
 export class SettingsManager extends React.Component<{}> {
+    // eslint-disable-next-line no-use-before-define
     public static Instance: SettingsManager;
     static _settingsStyle = addStyleSheet();
     @observable public isOpen = false;
@@ -60,6 +62,16 @@ export class SettingsManager extends React.Component<{}> {
             }
             // undefined means ColorScheme.Light until all CSS is updated with values for each color scheme (e.g., see MainView.scss, DocumentDecorations.scss)
         });
+        reaction(
+            () => [SettingsManager.userBackgroundColor, SettingsManager.userColor, SettingsManager.userVariantColor],
+            ([back, user, variant]) => {
+                SnappingManager.userBackgroundColor = back;
+                SnappingManager.userVariantColor = variant;
+                SnappingManager.userColor = user;
+            },
+            { fireImmediately: true }
+        );
+        SnappingManager.SettingsStyle = SettingsManager._settingsStyle;
     }
     matchSystem = () => {
         if (Doc.UserDoc().userThemeSystem) {
@@ -116,7 +128,7 @@ export class SettingsManager extends React.Component<{}> {
         if (this.playgroundMode) {
             DocServer.Control.makeReadOnly();
             addStyleSheetRule(SettingsManager._settingsStyle, 'topbar-inner-container', { background: 'red !important' });
-        } else Doc.CurrentUserEmail !== 'guest' && DocServer.Control.makeEditable();
+        } else ClientUtils.CurrentUserEmail !== 'guest' && DocServer.Control.makeEditable();
     });
 
     @undoBatch
@@ -176,7 +188,7 @@ export class SettingsManager extends React.Component<{}> {
                 {userTheme === ColorScheme.Custom && (
                     <Group formLabel="Custom Theme">
                         <ColorPicker
-                            tooltip={'User Color'} //
+                            tooltip="User Color" //
                             color={SettingsManager.userColor}
                             type={Type.SEC}
                             icon={<FaFillDrip />}
@@ -185,7 +197,7 @@ export class SettingsManager extends React.Component<{}> {
                             setFinalColor={this.switchUserColor}
                         />
                         <ColorPicker
-                            tooltip={'User Background Color'}
+                            tooltip="User Background Color"
                             color={SettingsManager.userColor}
                             type={Type.SEC}
                             icon={<FaPalette />}
@@ -194,7 +206,7 @@ export class SettingsManager extends React.Component<{}> {
                             setFinalColor={this.switchUserBackgroundColor}
                         />
                         <ColorPicker
-                            tooltip={'User Variant Color'}
+                            tooltip="User Variant Color"
                             color={SettingsManager.userColor}
                             type={Type.SEC}
                             icon={<FaPalette />}
@@ -212,8 +224,8 @@ export class SettingsManager extends React.Component<{}> {
         return (
             <div className="prefs-content">
                 <Toggle
-                    formLabel={'Show document header'}
-                    formLabelPlacement={'right'}
+                    formLabel="Show document header"
+                    formLabelPlacement="right"
                     toggleType={ToggleType.SWITCH}
                     onClick={e => (Doc.UserDoc().layout_showTitle = Doc.UserDoc().layout_showTitle ? undefined : 'author_date')}
                     toggleStatus={Doc.UserDoc().layout_showTitle !== undefined}
@@ -221,8 +233,8 @@ export class SettingsManager extends React.Component<{}> {
                     color={SettingsManager.userColor}
                 />
                 <Toggle
-                    formLabel={'Show Full Toolbar'}
-                    formLabelPlacement={'right'}
+                    formLabel="Show Full Toolbar"
+                    formLabelPlacement="right"
                     toggleType={ToggleType.SWITCH}
                     onClick={e => (Doc.UserDoc()['documentLinksButton-fullMenu'] = !Doc.UserDoc()['documentLinksButton-fullMenu'])}
                     toggleStatus={BoolCast(Doc.UserDoc()['documentLinksButton-fullMenu'])}
@@ -230,8 +242,8 @@ export class SettingsManager extends React.Component<{}> {
                     color={SettingsManager.userColor}
                 />
                 <Toggle
-                    formLabel={'Show Button Labels'}
-                    formLabelPlacement={'right'}
+                    formLabel="Show Button Labels"
+                    formLabelPlacement="right"
                     toggleType={ToggleType.SWITCH}
                     onClick={e => (FontIconBox.ShowIconLabels = !FontIconBox.ShowIconLabels)}
                     toggleStatus={FontIconBox.ShowIconLabels}
@@ -239,8 +251,8 @@ export class SettingsManager extends React.Component<{}> {
                     color={SettingsManager.userColor}
                 />
                 <Toggle
-                    formLabel={'Recognize Ink Gestures'}
-                    formLabelPlacement={'right'}
+                    formLabel="Recognize Ink Gestures"
+                    formLabelPlacement="right"
                     toggleType={ToggleType.SWITCH}
                     onClick={e => (GestureOverlay.RecognizeGestures = !GestureOverlay.RecognizeGestures)}
                     toggleStatus={GestureOverlay.RecognizeGestures}
@@ -248,8 +260,8 @@ export class SettingsManager extends React.Component<{}> {
                     color={SettingsManager.userColor}
                 />
                 <Toggle
-                    formLabel={'Hide Labels In Ink Shapes'}
-                    formLabelPlacement={'right'}
+                    formLabel="Hide Labels In Ink Shapes"
+                    formLabelPlacement="right"
                     toggleType={ToggleType.SWITCH}
                     onClick={e => (Doc.UserDoc().activeInkHideTextLabels = !Doc.UserDoc().activeInkHideTextLabels)}
                     toggleStatus={BoolCast(Doc.UserDoc().activeInkHideTextLabels)}
@@ -257,8 +269,8 @@ export class SettingsManager extends React.Component<{}> {
                     color={SettingsManager.userColor}
                 />
                 <Toggle
-                    formLabel={'Open Ink Docs in Lightbox'}
-                    formLabelPlacement={'right'}
+                    formLabel="Open Ink Docs in Lightbox"
+                    formLabelPlacement="right"
                     toggleType={ToggleType.SWITCH}
                     onClick={e => (Doc.UserDoc().openInkInLightbox = !Doc.UserDoc().openInkInLightbox)}
                     toggleStatus={BoolCast(Doc.UserDoc().openInkInLightbox)}
@@ -266,8 +278,8 @@ export class SettingsManager extends React.Component<{}> {
                     color={SettingsManager.userColor}
                 />
                 <Toggle
-                    formLabel={'Show Link Lines'}
-                    formLabelPlacement={'right'}
+                    formLabel="Show Link Lines"
+                    formLabelPlacement="right"
                     toggleType={ToggleType.SWITCH}
                     onClick={e => (Doc.UserDoc().showLinkLines = !Doc.UserDoc().showLinkLines)}
                     toggleStatus={BoolCast(Doc.UserDoc().showLinkLines)}
@@ -278,12 +290,12 @@ export class SettingsManager extends React.Component<{}> {
                     <NumberDropdown
                         number={NumCast(Doc.UserDoc().headerHeight, 30)}
                         color={SettingsManager.userColor}
-                        numberDropdownType={'slider'}
+                        numberDropdownType="slider"
                         min={6}
                         max={60}
                         step={2}
                         type={Type.TERT}
-                        unit={'px'}
+                        unit="px"
                         setNumber={val => console.log('GOT: ' + (Doc.UserDoc().headerHeight = val))}
                     />
                 </Group>
@@ -316,7 +328,7 @@ export class SettingsManager extends React.Component<{}> {
                     <div className="tab-column-title">Text</div>
                     <div className="tab-column-content">
                         {/* <NumberInput/> */}
-                        <Group formLabel={'Default Font'}>
+                        <Group formLabel="Default Font">
                             <NumberDropdown
                                 color={SettingsManager.userColor}
                                 numberDropdownType="slider"
@@ -325,7 +337,7 @@ export class SettingsManager extends React.Component<{}> {
                                 step={2}
                                 type={Type.PRIM}
                                 number={NumCast(Doc.UserDoc().fontSize, Number(StrCast(Doc.UserDoc().fontSize).replace('px', '')))}
-                                unit={'px'}
+                                unit="px"
                                 setNumber={val => (Doc.UserDoc().fontSize = val + 'px')}
                             />
                             <Dropdown
@@ -373,12 +385,12 @@ export class SettingsManager extends React.Component<{}> {
     @computed get passwordContent() {
         return (
             <div className="password-content">
-                <EditableText placeholder="Current password" type={Type.SEC} color={SettingsManager.userColor} val={''} setVal={val => this.changeVal(val as string, 'curr')} fillWidth password />
-                <EditableText placeholder="New password" type={Type.SEC} color={SettingsManager.userColor} val={''} setVal={val => this.changeVal(val as string, 'new')} fillWidth password />
-                <EditableText placeholder="Confirm new password" type={Type.SEC} color={SettingsManager.userColor} val={''} setVal={val => this.changeVal(val as string, 'conf')} fillWidth password />
+                <EditableText placeholder="Current password" type={Type.SEC} color={SettingsManager.userColor} val="" setVal={val => this.changeVal(val as string, 'curr')} fillWidth password />
+                <EditableText placeholder="New password" type={Type.SEC} color={SettingsManager.userColor} val="" setVal={val => this.changeVal(val as string, 'new')} fillWidth password />
+                <EditableText placeholder="Confirm new password" type={Type.SEC} color={SettingsManager.userColor} val="" setVal={val => this.changeVal(val as string, 'conf')} fillWidth password />
                 {!this.passwordResultText ? null : <div className={`${this.passwordResultText.startsWith('Error') ? 'error' : 'success'}-text`}>{this.passwordResultText}</div>}
-                <Button type={Type.SEC} text={'Forgot Password'} color={SettingsManager.userColor} />
-                <Button type={Type.TERT} text={'Submit'} onClick={this.changePassword} color={SettingsManager.userColor} />
+                <Button type={Type.SEC} text="Forgot Password" color={SettingsManager.userColor} />
+                <Button type={Type.TERT} text="Submit" onClick={this.changePassword} color={SettingsManager.userColor} />
             </div>
         );
     }
@@ -386,7 +398,7 @@ export class SettingsManager extends React.Component<{}> {
     @computed get accountOthersContent() {
         return (
             <div className="account-others-content">
-                <Button type={Type.TERT} text={'Connect to Google'} iconPlacement="left" icon={<BsGoogle />} onClick={() => this.googleAuthorize()} />
+                <Button type={Type.TERT} text="Connect to Google" iconPlacement="left" icon={<BsGoogle />} onClick={() => this.googleAuthorize()} />
             </div>
         );
     }
@@ -417,7 +429,7 @@ export class SettingsManager extends React.Component<{}> {
                     <div className="tab-column-title">Modes</div>
                     <div className="tab-column-content">
                         <Dropdown
-                            formLabel={'Mode'}
+                            formLabel="Mode"
                             closeOnSelect={true}
                             items={[
                                 {
@@ -442,7 +454,7 @@ export class SettingsManager extends React.Component<{}> {
                             color={SettingsManager.userColor}
                             fillWidth
                         />
-                        <Toggle formLabel={'Playground Mode'} toggleType={ToggleType.SWITCH} toggleStatus={this.playgroundMode} onClick={this.playgroundModeToggle} color={SettingsManager.userColor} />
+                        <Toggle formLabel="Playground Mode" toggleType={ToggleType.SWITCH} toggleStatus={this.playgroundMode} onClick={this.playgroundModeToggle} color={SettingsManager.userColor} />
                     </div>
                     <div className="tab-column-title" style={{ marginTop: 20, marginBottom: 10 }}>
                         Freeform Navigation
@@ -475,10 +487,10 @@ export class SettingsManager extends React.Component<{}> {
                 <div className="tab-column">
                     <div className="tab-column-title">Permissions</div>
                     <div className="tab-column-content">
-                        <Button text={'Manage Groups'} type={Type.TERT} onClick={() => GroupManager.Instance?.open()} color={SettingsManager.userColor} />
+                        <Button text="Manage Groups" type={Type.TERT} onClick={() => GroupManager.Instance?.open()} color={SettingsManager.userColor} />
                         <Toggle
                             toggleType={ToggleType.SWITCH}
-                            formLabel={'Default access private'}
+                            formLabel="Default access private"
                             color={SettingsManager.userColor}
                             toggleStatus={BoolCast(Doc.defaultAclPrivate)}
                             onClick={action(() => (Doc.defaultAclPrivate = !Doc.defaultAclPrivate))}
@@ -525,14 +537,14 @@ export class SettingsManager extends React.Component<{}> {
                     <div className="settings-user">
                         <div style={{ color: SettingsManager.userBackgroundColor }}>{DashVersion}</div>
                         <div className="settings-username" style={{ color: SettingsManager.userBackgroundColor }}>
-                            {Doc.CurrentUserEmail}
+                            {ClientUtils.CurrentUserEmail}
                         </div>
-                        <Button text={Doc.GuestDashboard ? 'Exit' : 'Log Out'} type={Type.TERT} color={SettingsManager.userVariantColor} onClick={() => window.location.assign(Utils.prepend('/logout'))} />
+                        <Button text={Doc.GuestDashboard ? 'Exit' : 'Log Out'} type={Type.TERT} color={SettingsManager.userVariantColor} onClick={() => window.location.assign(ClientUtils.prepend('/logout'))} />
                     </div>
                 </div>
 
                 <div className="close-button">
-                    <Button icon={<FontAwesomeIcon icon={'times'} size={'lg'} />} onClick={this.close} color={SettingsManager.userColor} />
+                    <Button icon={<FontAwesomeIcon icon="times" size="lg" />} onClick={this.close} color={SettingsManager.userColor} />
                 </div>
 
                 <div className="settings-content" style={{ color: SettingsManager.userColor, background: SettingsManager.userBackgroundColor }}>
diff --git a/src/client/util/SharingManager.tsx b/src/client/util/SharingManager.tsx
index fddf735e3..03f7e9b71 100644
--- a/src/client/util/SharingManager.tsx
+++ b/src/client/util/SharingManager.tsx
@@ -6,13 +6,14 @@ import { observer } from 'mobx-react';
 import * as React from 'react';
 import Select from 'react-select';
 import * as RequestPromise from 'request-promise';
+import { ClientUtils } from '../../ClientUtils';
+import { Utils } from '../../Utils';
 import { Doc, DocListCast, DocListCastAsync, HierarchyMapping, ReverseHierarchyMap } from '../../fields/Doc';
 import { AclAdmin, AclPrivate, DocAcl, DocData } from '../../fields/DocSymbols';
 import { FieldLoader } from '../../fields/FieldLoader';
 import { Id } from '../../fields/FieldSymbols';
 import { StrCast } from '../../fields/Types';
-import { distributeAcls, GetEffectiveAcl, normalizeEmail, SharingPermissions, TraceMobx } from '../../fields/util';
-import { Utils } from '../../Utils';
+import { GetEffectiveAcl, SharingPermissions, TraceMobx, distributeAcls, normalizeEmail } from '../../fields/util';
 import { DocServer } from '../DocServer';
 import { DictationOverlay } from '../views/DictationOverlay';
 import { MainViewModal } from '../views/MainViewModal';
@@ -84,7 +85,6 @@ export class SharingManager extends React.Component<{}> {
     @observable private upgradeNested: boolean = false; // whether child docs in a collection/dashboard should be changed to be less private - initially selected so default is upgrade all
     @observable private layoutDocAcls: boolean = false; // whether the layout doc or data doc's acls are to be used
     @observable private myDocAcls: boolean = false; // whether the My Docs checkbox is selected or not
-    @observable private _buttonDown = false;
 
     // private get linkVisible() {
     //     return this.targetDoc ? this.targetDoc["acl-" + PublicKey] !== SharingPermissions.None : false;
@@ -136,9 +136,11 @@ export class SharingManager extends React.Component<{}> {
     populateUsers = async () => {
         if (!this.populating && Doc.UserDoc()[Id] !== Utils.GuestID()) {
             this.populating = true;
-            const userList = await RequestPromise.get(Utils.prepend('/getUsers'));
-            const raw = (JSON.parse(userList) as User[]).filter(user => user.email !== 'guest' && user.email !== Doc.CurrentUserEmail);
-            runInAction(() => (FieldLoader.ServerLoadStatus.message = 'users'));
+            const userList = await RequestPromise.get(ClientUtils.prepend('/getUsers'));
+            const raw = (JSON.parse(userList) as User[]).filter(user => user.email !== 'guest' && user.email !== ClientUtils.CurrentUserEmail);
+            runInAction(() => {
+                FieldLoader.ServerLoadStatus.message = 'users';
+            });
             const docs = await DocServer.GetRefFields(raw.reduce((list, user) => [...list, user.sharingDocumentId, user.linkDatabaseId], [] as string[]));
             raw.map(
                 action((newUser: User) => {
@@ -191,7 +193,8 @@ export class SharingManager extends React.Component<{}> {
                 this.users
                     .filter(({ user: { email } }) => JSON.parse(StrCast(group.members)).includes(email))
                     .forEach(({ user, sharingDoc }) => {
-                        if (permission !== SharingPermissions.None) Doc.AddDocToList(sharingDoc, doc.dockingConfig ? dashStorage : storage, doc); // add the doc to the sharingDoc if it hasn't already been added
+                        if (permission !== SharingPermissions.None)
+                            Doc.AddDocToList(sharingDoc, doc.dockingConfig ? dashStorage : storage, doc); // add the doc to the sharingDoc if it hasn't already been added
                         else GetEffectiveAcl(doc, user.email) === AclPrivate && Doc.RemoveDocFromList(sharingDoc, ((doc.createdFrom as Doc) || doc).dockingConfig ? dashStorage : storage, (doc.createdFrom as Doc) || doc); // remove the doc from the list if it already exists
                     });
             }
@@ -231,7 +234,7 @@ export class SharingManager extends React.Component<{}> {
     shareFromPropertiesSidebar = undoable((shareWith: string, permission: SharingPermissions, docs: Doc[], layout: boolean) => {
         if (layout) this.layoutDocAcls = true;
         if (shareWith !== 'Guest') {
-            const user = this.users.find(({ user: { email } }) => email === (shareWith === 'Me' ? Doc.CurrentUserEmail : shareWith));
+            const user = this.users.find(({ user: { email } }) => email === (shareWith === 'Me' ? ClientUtils.CurrentUserEmail : shareWith));
             docs.forEach(doc => {
                 if (user) this.setInternalSharing(user, permission, doc);
                 else this.setInternalGroupSharing(GroupManager.Instance.getGroup(shareWith)!, permission, doc, undefined, true);
@@ -300,7 +303,7 @@ export class SharingManager extends React.Component<{}> {
      * Copies the Public sharing url to the user's clipboard.
      */
     private copyURL = (e: any) => {
-        Utils.CopyText(Utils.shareUrl(this.targetDoc![Id]));
+        ClientUtils.CopyText(ClientUtils.shareUrl(this.targetDoc![Id]));
     };
 
     /**
@@ -496,19 +499,19 @@ export class SharingManager extends React.Component<{}> {
         const sameAuthor = docs.every(doc => doc?.author === docs[0]?.author);
 
         // the owner of the doc and the current user are placed at the top of the user list.
-        const userKey = `acl-${normalizeEmail(Doc.CurrentUserEmail)}`;
+        const userKey = `acl-${normalizeEmail(ClientUtils.CurrentUserEmail)}`;
         const curUserPermission = StrCast(targetDoc[userKey]);
         // const curUserPermission = HierarchyMapping.get(effectiveAcls[0])!.name
         userListContents.unshift(
             sameAuthor ? (
                 <div key={'owner'} className={'container'}>
-                    <span className="padding">{targetDoc?.author === Doc.CurrentUserEmail ? 'Me' : StrCast(targetDoc?.author)}</span>
+                    <span className="padding">{targetDoc?.author === ClientUtils.CurrentUserEmail ? 'Me' : StrCast(targetDoc?.author)}</span>
                     <div className="edit-actions">
                         <div className={'permissions-dropdown'}>Owner</div>
                     </div>
                 </div>
             ) : null,
-            sameAuthor && targetDoc?.author !== Doc.CurrentUserEmail ? (
+            sameAuthor && targetDoc?.author !== ClientUtils.CurrentUserEmail ? (
                 <div key={'me'} className={'container'}>
                     <span className={'padding'}>Me</span>
                     <div className="edit-actions">
diff --git a/src/client/util/SnappingManager.ts b/src/client/util/SnappingManager.ts
index 359140732..eb47bbe88 100644
--- a/src/client/util/SnappingManager.ts
+++ b/src/client/util/SnappingManager.ts
@@ -1,7 +1,7 @@
-import { observable, action, runInAction, reaction, makeObservable } from 'mobx';
-import { Doc, Opt } from '../../fields/Doc';
+import { observable, action, runInAction, makeObservable } from 'mobx';
 
 export class SnappingManager {
+    // eslint-disable-next-line no-use-before-define
     private static _manager: SnappingManager;
     private static get Instance() {
         return SnappingManager._manager ?? new SnappingManager();
@@ -12,7 +12,7 @@ export class SnappingManager {
     @observable _metaKey = false;
     @observable _isLinkFollowing = false;
     @observable _isDragging: boolean = false;
-    @observable _isResizing: Doc | undefined = undefined;
+    @observable _isResizing: string | undefined = undefined; // the string is the Id of the document being resized
     @observable _canEmbed: boolean = false;
     @observable _horizSnapLines: number[] = [];
     @observable _vertSnapLines: number[] = [];
@@ -23,7 +23,9 @@ export class SnappingManager {
         makeObservable(this);
     }
 
-    @action public static clearSnapLines = () => (this.Instance._vertSnapLines.length = this.Instance._horizSnapLines.length = 0);
+    @action public static clearSnapLines = () => {
+        this.Instance._vertSnapLines.length = this.Instance._horizSnapLines.length = 0;
+    };
     @action public static addSnapLines = (horizLines: number[], vertLines: number[]) => {
         this.Instance._horizSnapLines.push(...horizLines);
         this.Instance._vertSnapLines.push(...vertLines);
@@ -39,12 +41,17 @@ export class SnappingManager {
     public static get IsResizing()     { return this.Instance._isResizing; } // prettier-ignore
     public static get CanEmbed()       { return this.Instance._canEmbed; } // prettier-ignore
     public static get ExploreMode()    { return this.Instance._exploreMode; } // prettier-ignore
-    public static SetShiftKey       = (down: boolean) => runInAction(() => (this.Instance._shiftKey = down)); // prettier-ignore
-    public static SetCtrlKey        = (down: boolean) => runInAction(() => (this.Instance._ctrlKey = down)); // prettier-ignore
-    public static SetMetaKey        = (down: boolean) => runInAction(() => (this.Instance._metaKey = down)); // prettier-ignore
-    public static SetIsLinkFollowing= (follow:boolean)=> runInAction(() => (this.Instance._isLinkFollowing = follow)); // prettier-ignore
-    public static SetIsDragging     = (drag: boolean) => runInAction(() => (this.Instance._isDragging = drag)); // prettier-ignore
-    public static SetIsResizing     = (doc: Opt<Doc>) => runInAction(() => (this.Instance._isResizing = doc)); // prettier-ignore
-    public static SetCanEmbed       = (embed:boolean) => runInAction(() => (this.Instance._canEmbed = embed)); // prettier-ignore
-    public static SetExploreMode    = (state:boolean) => runInAction(() => (this.Instance._exploreMode = state)); // prettier-ignore
+    public static SetShiftKey       = (down: boolean) => runInAction(() => {this.Instance._shiftKey = down}); // prettier-ignore
+    public static SetCtrlKey        = (down: boolean) => runInAction(() => {this.Instance._ctrlKey = down}); // prettier-ignore
+    public static SetMetaKey        = (down: boolean) => runInAction(() => {this.Instance._metaKey = down}); // prettier-ignore
+    public static SetIsLinkFollowing= (follow:boolean)=> runInAction(() => {this.Instance._isLinkFollowing = follow}); // prettier-ignore
+    public static SetIsDragging     = (drag: boolean) => runInAction(() => {this.Instance._isDragging = drag}); // prettier-ignore
+    public static SetIsResizing     = (docid?:string) => runInAction(() => {this.Instance._isResizing = docid}); // prettier-ignore
+    public static SetCanEmbed       = (embed:boolean) => runInAction(() => {this.Instance._canEmbed = embed}); // prettier-ignore
+    public static SetExploreMode    = (state:boolean) => runInAction(() => {this.Instance._exploreMode = state}); // prettier-ignore
+
+    public static userColor: string | undefined;
+    public static userVariantColor: string | undefined;
+    public static userBackgroundColor: string | undefined;
+    public static SettingsStyle: any;
 }
diff --git a/src/client/util/UndoManager.ts b/src/client/util/UndoManager.ts
index 857ca852f..4e941508d 100644
--- a/src/client/util/UndoManager.ts
+++ b/src/client/util/UndoManager.ts
@@ -1,7 +1,8 @@
-import { observable, action, runInAction } from 'mobx';
-import { Doc, Field } from '../../fields/Doc';
-import { RichTextField } from '../../fields/RichTextField';
+import { action, observable, runInAction } from 'mobx';
 import { Without } from '../../Utils';
+import { RichTextField } from '../../fields/RichTextField';
+
+export let printToConsole = false; // Doc.MyDockedBtns.linearView_IsOpen
 
 function getBatchName(target: any, key: string | symbol): string {
     const keyName = key.toString();
@@ -97,7 +98,7 @@ export namespace UndoManager {
 
     export function AddEvent(event: UndoEvent, value?: any): void {
         if (currentBatch && batchCounter.get() && !undoing) {
-            Doc.MyDockedBtns.linearView_IsOpen &&
+            printToConsole &&
                 console.log(
                     '                         '.slice(0, batchCounter.get()) +
                         'UndoEvent : ' +
@@ -172,7 +173,7 @@ export namespace UndoManager {
     }
 
     export function StartBatch(batchName: string): Batch {
-        Doc.MyDockedBtns.linearView_IsOpen && console.log('                          '.slice(0, batchCounter.get()) + 'Start ' + batchCounter + ' ' + batchName);
+        printToConsole && console.log('                          '.slice(0, batchCounter.get()) + 'Start ' + batchCounter + ' ' + batchName);
         runInAction(() => batchCounter.set(batchCounter.get() + 1));
         if (currentBatch === undefined) {
             currentBatch = [];
@@ -182,7 +183,7 @@ export namespace UndoManager {
 
     const EndBatch = action((batchName: string, cancel: boolean = false) => {
         runInAction(() => batchCounter.set(batchCounter.get() - 1));
-        Doc.MyDockedBtns.linearView_IsOpen && console.log('                          '.slice(0, batchCounter.get()) + 'End ' + batchName + ' (' + currentBatch?.length + ')');
+        printToConsole && 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/util/bezierFit.ts b/src/client/util/bezierFit.ts
index 8fc6de6f9..6bbf55e5a 100644
--- a/src/client/util/bezierFit.ts
+++ b/src/client/util/bezierFit.ts
@@ -1,4 +1,7 @@
-import { Point } from "../../pen-gestures/ndollar";
+/* eslint-disable prefer-destructuring */
+/* eslint-disable no-param-reassign */
+/* eslint-disable camelcase */
+import { Point } from '../../pen-gestures/ndollar';
 
 class SmartRect {
     minx: number = 0;
@@ -6,20 +9,43 @@ class SmartRect {
     maxx: number = 0;
     maxy: number = 0;
 
-    constructor(mix: number = 0, miy: number = 0, max: number = 0, may: number = 0) { this.minx = mix; this.miny = miy; this.maxx = max; this.maxy = may; }
+    constructor(mix: number = 0, miy: number = 0, max: number = 0, may: number = 0) {
+        this.minx = mix;
+        this.miny = miy;
+        this.maxx = max;
+        this.maxy = may;
+    }
 
-    public get Center() { return new Point((this.maxx + this.minx) / 2.0, (this.maxy + this.miny) / 2.0); }
-    public get TopLeft() { return new Point(this.minx, this.miny); }
-    public get TopRight() { return new Point(this.maxx, this.miny); }
-    public get BotLeft() { return new Point(this.minx, this.maxy); }
-    public get BotRight() { return new Point(this.maxx, this.maxy); }
-    public get Width() { return this.maxx - this.minx; }
-    public get Height() { return this.maxy - this.miny; }
-    public static Intersect(a: SmartRect, b: SmartRect) { return a.Intersect(b); }
-    public Intersect(b: SmartRect) { return !((this.minx > b.maxx) || (this.miny > b.maxy) || (b.minx > this.maxx) || (b.miny > this.maxy)); }
+    public get Center() {
+        return new Point((this.maxx + this.minx) / 2.0, (this.maxy + this.miny) / 2.0);
+    }
+    public get TopLeft() {
+        return new Point(this.minx, this.miny);
+    }
+    public get TopRight() {
+        return new Point(this.maxx, this.miny);
+    }
+    public get BotLeft() {
+        return new Point(this.minx, this.maxy);
+    }
+    public get BotRight() {
+        return new Point(this.maxx, this.maxy);
+    }
+    public get Width() {
+        return this.maxx - this.minx;
+    }
+    public get Height() {
+        return this.maxy - this.miny;
+    }
+    public static Intersect(a: SmartRect, b: SmartRect) {
+        return a.Intersect(b);
+    }
+    public Intersect(b: SmartRect) {
+        return !(this.minx > b.maxx || this.miny > b.maxy || b.minx > this.maxx || b.miny > this.maxy);
+    }
 
     public ContainsPercentage(other: SmartRect, axis: Point) {
-        var ret = 0;
+        let ret = 0;
         const minx = Math.max(other.TopLeft.X * axis.X + other.TopLeft.Y * axis.Y, this.TopLeft.X * axis.X + this.TopLeft.Y * axis.Y);
         const maxx = Math.max(other.BotRight.X * axis.X + other.BotRight.Y * axis.Y, this.BotRight.X * axis.X + this.BotRight.Y * axis.Y);
         ret = maxx > minx ? (maxx - minx) / (axis === new Point(1, 0) ? other.Width : other.Height) : 0;
@@ -36,7 +62,7 @@ class SmartRect {
             if (r.minx > r.maxx) [r.minx, r.maxx] = [r.maxx, r.minx];
             if (r.miny > r.maxy) [r.miny, r.maxy] = [r.maxy, r.miny];
 
-            for (const pt of p) {
+            p.forEach(pt => {
                 if (pt.X < r.minx) {
                     r.minx = pt.X;
                 } else if (pt.X > r.maxx) {
@@ -47,7 +73,7 @@ class SmartRect {
                 } else if (pt.Y > r.maxy) {
                     r.maxy = pt.Y;
                 }
-            }
+            });
         }
         return r;
     }
@@ -64,18 +90,18 @@ export function Normalize(p: Point) {
 function ReparameterizeBezier(d: Point[], first: number, last: number, u: number[], bezCurve: Point[]) {
     const uPrime = new Array<number>(last - first + 1); //  New parameter values
 
-    for (var i = first; i <= last; i++) {
+    for (let i = first; i <= last; i++) {
         uPrime[i - first] = NewtonRaphsonRootFind(bezCurve, d[i], u[i - first]);
     }
     return uPrime;
 }
 function ComputeMaxError(d: Point[], first: number, last: number, bezCurve: Point[], u: number[]) {
-    var maxError = 0; //  Maximum error
-    var splitPoint2D = (last - first + 1) / 2;
-    for (var i = first + 1; i < last; i++) {
-        const P = [0, 0];  //  point on curve
+    let maxError = 0; //  Maximum error
+    let splitPoint2D = (last - first + 1) / 2;
+    for (let i = first + 1; i < last; i++) {
+        const P = [0, 0]; //  point on curve
         EvalBezierFast(bezCurve, u[i - first], P);
-        const dx = P[0] - d[i].X;//  offset from point to curve
+        const dx = P[0] - d[i].X; //  offset from point to curve
         const dy = P[1] - d[i].Y;
         const dist = Math.sqrt(dx * dx + dy * dy); //  Current error
         if (dist >= maxError) {
@@ -88,11 +114,11 @@ function ComputeMaxError(d: Point[], first: number, last: number, bezCurve: Poin
     return { maxError, splitPoint2D };
 }
 function ChordLengthParameterize(d: Point[], first: number, last: number) {
-    const u = new Array<number>(last - first + 1);//  Parameterization
+    const u = new Array<number>(last - first + 1); //  Parameterization
 
-    var prev = 0.0;
+    let prev = 0.0;
     u[0] = prev;
-    for (var i = first + 1; i <= last; i++) {
+    for (let i = first + 1; i <= last; i++) {
         const lastd = d[i - 1];
         const curd = d[i];
         const dx = lastd.X - curd.X;
@@ -101,27 +127,38 @@ function ChordLengthParameterize(d: Point[], first: number, last: number) {
     }
 
     const ulastfirst = u[last - first];
-    for (var i = first + 1; i <= last; i++) {
+    for (let i = first + 1; i <= last; i++) {
         u[i - first] /= ulastfirst;
     }
 
     return u;
 }
 /*
-*  B0, B1, B2, B3 :
-*  Bezier multipliers
-*/
-function B0(u: number) { const tmp = 1.0 - u; return tmp * tmp * tmp; }
-function B1(u: number) { const tmp = 1.0 - u; return 3 * u * tmp * tmp; }
-function B2(u: number) { const tmp = 1.0 - u; return 3 * u * u * tmp; }
-function B3(u: number) { return u * u * u; }
+ *  B0, B1, B2, B3 :
+ *  Bezier multipliers
+ */
+function B0(u: number) {
+    const tmp = 1.0 - u;
+    return tmp * tmp * tmp;
+}
+function B1(u: number) {
+    const tmp = 1.0 - u;
+    return 3 * u * tmp * tmp;
+}
+function B2(u: number) {
+    const tmp = 1.0 - u;
+    return 3 * u * u * tmp;
+}
+function B3(u: number) {
+    return u * u * u;
+}
 function bounds(p: Point[]) {
     const r = new SmartRect(p[0].X, p[0].Y, p[3].X, p[3].Y); // These are the most likely to be extremal
 
-    if (r.minx > r.maxx) (r.minx, r.maxx);
+    if (r.minx > r.maxx) [r.minx, r.maxx] = [r.maxx, r.minx];
     if (r.miny > r.maxy) [r.miny, r.maxy] = [r.maxy, r.miny]; // swap min & max
 
-    for (var i = 1; i < 3; i++) {
+    for (let i = 1; i < 3; i++) {
         if (p[i].X < r.minx) r.minx = p[i].X;
         else if (p[i].X > r.maxx) r.maxx = p[i].X;
 
@@ -131,37 +168,36 @@ function bounds(p: Point[]) {
     return r;
 }
 
-
 function splitCubic(p: Point[], t: number, left: Point[], right: Point[]) {
     const sz = 4;
     const Vtemp = new Array<Array<Point>>(4);
-    for (var i = 0; i < 4; i++) Vtemp[i] = new Array<Point>(4);
+    for (let i = 0; i < 4; i++) Vtemp[i] = new Array<Point>(4);
 
     /* Copy control points */
     // std::copy(p.begin(), p.end(), Vtemp[0]);
-    for (var i = 0; i < sz; i++) {
+    for (let i = 0; i < sz; i++) {
         Vtemp[0][i].X = p[i].X;
         Vtemp[0][i].Y = p[i].Y;
     }
 
     /* Triangle computation */
-    for (var i = 1; i < sz; i++) {
-        for (var j = 0; j < sz - i; j++) {
+    for (let i = 1; i < sz; i++) {
+        for (let j = 0; j < sz - i; j++) {
             const a = Vtemp[i - 1][j];
             const b = Vtemp[i - 1][j + 1];
             Vtemp[i][j].X = b.X * t + a.X * (1 - t);
-            Vtemp[i][j].Y = b.Y * t + a.Y * (1 - t);  // Vtemp[i][j] = Point2D::Lerp(Vtemp[i - 1][j], Vtemp[i - 1][j + 1], t);
+            Vtemp[i][j].Y = b.Y * t + a.Y * (1 - t); // Vtemp[i][j] = Point2D::Lerp(Vtemp[i - 1][j], Vtemp[i - 1][j + 1], t);
         }
     }
 
     if (left) {
-        for (var j = 0; j < sz; j++) {
+        for (let j = 0; j < sz; j++) {
             left[j].X = Vtemp[j][0].X;
             left[j].Y = Vtemp[j][0].Y;
         }
     }
     if (right) {
-        for (var j = 0; j < sz; j++) {
+        for (let j = 0; j < sz; j++) {
             right[j].X = Vtemp[sz - 1 - j][j].X;
             right[j].Y = Vtemp[sz - 1 - j][j].Y;
         }
@@ -169,51 +205,53 @@ function splitCubic(p: Point[], t: number, left: Point[], right: Point[]) {
 }
 
 /*
-* Recursively intersect two curves keeping track of their real parameters
-* and depths of intersection.
-* The results are returned in a 2-D array of doubles indicating the parameters
-* for which intersections are found.  The parameters are in the order the
-* intersections were found, which is probably not in sorted order.
-* When an intersection is found, the parameter value for each of the two
-* is stored in the index elements array, and the index is incremented.
-*
-* If either of the curves has subdivisions left before it is straight
-* (depth > 0)
-* that curve (possibly both) is (are) subdivided at its (their) midpoint(s).
-* the depth(s) is (are) decremented, and the parameter value(s) corresponding
-* to the midpoints(s) is (are) computed.
-* Then each of the subcurves of one curve is intersected with each of the
-* subcurves of the other curve, first by testing the bounding boxes for
-* interference.  If there is any bounding box interference, the corresponding
-* subcurves are recursively intersected.
-*
-* If neither curve has subdivisions left, the line segments from the first
-* to last control point of each segment are intersected.  (Actually the
-* only the parameter value corresponding to the intersection point is found).
-*
-* The apriori flatness test is probably more efficient than testing at each
-* level of recursion, although a test after three or four levels would
-* probably be worthwhile, since many curves become flat faster than their
-* asymptotic rate for the first few levels of recursion.
-*
-* The bounding box test fails much more frequently than it succeeds, providing
-* substantial pruning of the search space.
-*
-* Each (sub)curve is subdivided only once, hence it is not possible that for
-* one final line intersection test the subdivision was at one level, while
-* for another final line intersection test the subdivision (of the same curve)
-* was at another.  Since the line segments share endpoints, the intersection
-* is robust: a near-tangential intersection will yield zero or two
-* intersections.
-*/
+ * Recursively intersect two curves keeping track of their real parameters
+ * and depths of intersection.
+ * The results are returned in a 2-D array of doubles indicating the parameters
+ * for which intersections are found.  The parameters are in the order the
+ * intersections were found, which is probably not in sorted order.
+ * When an intersection is found, the parameter value for each of the two
+ * is stored in the index elements array, and the index is incremented.
+ *
+ * If either of the curves has subdivisions left before it is straight
+ * (depth > 0)
+ * that curve (possibly both) is (are) subdivided at its (their) midpoint(s).
+ * the depth(s) is (are) decremented, and the parameter value(s) corresponding
+ * to the midpoints(s) is (are) computed.
+ * Then each of the subcurves of one curve is intersected with each of the
+ * subcurves of the other curve, first by testing the bounding boxes for
+ * interference.  If there is any bounding box interference, the corresponding
+ * subcurves are recursively intersected.
+ *
+ * If neither curve has subdivisions left, the line segments from the first
+ * to last control point of each segment are intersected.  (Actually the
+ * only the parameter value corresponding to the intersection point is found).
+ *
+ * The apriori flatness test is probably more efficient than testing at each
+ * level of recursion, although a test after three or four levels would
+ * probably be worthwhile, since many curves become flat faster than their
+ * asymptotic rate for the first few levels of recursion.
+ *
+ * The bounding box test fails much more frequently than it succeeds, providing
+ * substantial pruning of the search space.
+ *
+ * Each (sub)curve is subdivided only once, hence it is not possible that for
+ * one final line intersection test the subdivision was at one level, while
+ * for another final line intersection test the subdivision (of the same curve)
+ * was at another.  Since the line segments share endpoints, the intersection
+ * is robust: a near-tangential intersection will yield zero or two
+ * intersections.
+ */
 function recursively_intersect(a: Point[], t0: number, t1: number, deptha: number, b: Point[], u0: number, u1: number, depthb: number, parameters: number[][]) {
     if (deptha > 0) {
-        const a1 = new Array<Point>(4), a2 = new Array<Point>(4);
+        const a1 = new Array<Point>(4);
+        const a2 = new Array<Point>(4);
         splitCubic(a, 0.5, a1, a2);
         const tmid = (t0 + t1) * 0.5;
         deptha--;
         if (depthb > 0) {
-            const b1 = new Array<Point>(4), b2 = new Array<Point>(4);
+            const b1 = new Array<Point>(4);
+            const b2 = new Array<Point>(4);
             splitCubic(b, 0.5, b1, b2);
             const umid = (u0 + u1) * 0.5;
             depthb--;
@@ -229,8 +267,7 @@ function recursively_intersect(a: Point[], t0: number, t1: number, deptha: numbe
             if (SmartRect.Intersect(bounds(a2), bounds(b2))) {
                 recursively_intersect(a2, tmid, t1, deptha, b2, umid, u1, depthb, parameters);
             }
-        }
-        else {
+        } else {
             if (SmartRect.Intersect(bounds(a1), bounds(b))) {
                 recursively_intersect(a1, t0, tmid, deptha, b, u0, u1, depthb, parameters);
             }
@@ -238,50 +275,45 @@ function recursively_intersect(a: Point[], t0: number, t1: number, deptha: numbe
                 recursively_intersect(a2, tmid, t1, deptha, b, u0, u1, depthb, parameters);
             }
         }
-    }
+    } else if (depthb > 0) {
+        const b1 = new Array<Point>(4);
+        const b2 = new Array<Point>(4);
+        splitCubic(b, 0.5, b1, b2);
+        const umid = (u0 + u1) * 0.5;
+        depthb--;
+        if (SmartRect.Intersect(bounds(a), bounds(b1))) {
+            recursively_intersect(a, t0, t1, deptha, b1, u0, umid, depthb, parameters);
+        }
+        if (SmartRect.Intersect(bounds(a), bounds(b2))) {
+            recursively_intersect(a, t0, t1, deptha, b2, umid, u1, depthb, parameters);
+        }
+    } // Both segments are fully subdivided; now do line segments
     else {
-        if (depthb > 0) {
-            const b1 = new Array<Point>(4), b2 = new Array<Point>(4);
-            splitCubic(b, 0.5, b1, b2);
-            const umid = (u0 + u1) * 0.5;
-            depthb--;
-            if (SmartRect.Intersect(bounds(a), bounds(b1))) {
-                recursively_intersect(a, t0, t1, deptha, b1, u0, umid, depthb, parameters);
-            }
-            if (SmartRect.Intersect(bounds(a), bounds(b2))) {
-                recursively_intersect(a, t0, t1, deptha, b2, umid, u1, depthb, parameters);
-            }
+        const xlk = a[3].X - a[0].X;
+        const ylk = a[3].Y - a[0].Y;
+        const xnm = b[3].X - b[0].X;
+        const ynm = b[3].Y - b[0].Y;
+        const xmk = b[0].X - a[0].X;
+        const ymk = b[0].Y - a[0].Y;
+        const det = xnm * ylk - ynm * xlk;
+        if (1.0 + det === 1.0) {
+            return;
         }
-        else // Both segments are fully subdivided; now do line segments
-        {
-            const xlk = a[3].X - a[0].X;
-            const ylk = a[3].Y - a[0].Y;
-            const xnm = b[3].X - b[0].X;
-            const ynm = b[3].Y - b[0].Y;
-            const xmk = b[0].X - a[0].X;
-            const ymk = b[0].Y - a[0].Y;
-            const det = xnm * ylk - ynm * xlk;
-            if (1.0 + det === 1.0) {
-                return;
-            }
-            else {
-                const detinv = 1.0 / det;
-                const s = (xnm * ymk - ynm * xmk) * detinv;
-                const t = (xlk * ymk - ylk * xmk) * detinv;
-                if ((s < 0.0) || (s > 1.0) || (t < 0.0) || (t > 1.0) || Number.isNaN(s) || Number.isNaN(t)) {
-                    return;
-                }
-                parameters.push([t0 + s * (t1 - t0), u0 + t * (u1 - u0)]);
-            }
+        const detinv = 1.0 / det;
+        const s = (xnm * ymk - ynm * xmk) * detinv;
+        const t = (xlk * ymk - ylk * xmk) * detinv;
+        if (s < 0.0 || s > 1.0 || t < 0.0 || t > 1.0 || isNaN(s) || isNaN(t)) {
+            return;
         }
+        parameters.push([t0 + s * (t1 - t0), u0 + t * (u1 - u0)]);
     }
 }
 
 /*
-*  EvalBezier :
-*      Evaluate a Bezier curve at a particular parameter value
-*
-*/
+ *  EvalBezier :
+ *      Evaluate a Bezier curve at a particular parameter value
+ *
+ */
 const MAX_DEGREE = 5;
 function EvalBezier(V: Point[], degree: number, t: number, result: number[]) {
     if (degree + 1 > MAX_DEGREE) {
@@ -293,35 +325,36 @@ function EvalBezier(V: Point[], degree: number, t: number, result: number[]) {
     const Vtemp = [new Point(0, 0), new Point(0, 0), new Point(0, 0), new Point(0, 0)]; // Local copy of control points
 
     /* Copy array   */
-    for (var i = 0; i <= degree; i++) {
+    for (let i = 0; i <= degree; i++) {
         Vtemp[i].X = V[i].X;
         Vtemp[i].Y = V[i].Y;
     }
 
     /* Triangle computation */
-    for (var i = 1; i <= degree; i++) {
-        for (var j = 0; j <= degree - i; j++) {
+    for (let i = 1; i <= degree; i++) {
+        for (let j = 0; j <= degree - i; j++) {
             Vtemp[j].X = (1.0 - t) * Vtemp[j].X + t * Vtemp[j + 1].X;
             Vtemp[j].Y = (1.0 - t) * Vtemp[j].Y + t * Vtemp[j + 1].Y;
         }
     }
 
     result[0] = Vtemp[0].X;
-    result[1] = Vtemp[0].Y;// Point on curve at parameter t
+    result[1] = Vtemp[0].Y; // Point on curve at parameter t
 }
 
 function EvalBezierFast(p: Point[], t: number, result: number[]) {
     const n = 3;
     const u = 1.0 - t;
-    var bc = 1, tn = 1;
-    var tmpX = p[0].X * u;
-    var tmpY = p[0].Y * u;
-    tn = tn * t;
-    bc = bc * (n - 1 + 1) / 1;
+    let bc = 1;
+    let tn = 1;
+    let tmpX = p[0].X * u;
+    let tmpY = p[0].Y * u;
+    tn *= t;
+    bc = (bc * (n - 1 + 1)) / 1;
     tmpX = (tmpX + tn * bc * p[1].X) * u;
     tmpY = (tmpY + tn * bc * p[1].Y) * u;
-    tn = tn * t;
-    bc = bc * (n - 2 + 1) / 2;
+    tn *= t;
+    bc = (bc * (n - 2 + 1)) / 2;
     tmpX = (tmpX + tn * bc * p[2].X) * u;
     tmpY = (tmpY + tn * bc * p[2].Y) * u;
 
@@ -329,9 +362,9 @@ function EvalBezierFast(p: Point[], t: number, result: number[]) {
     result[1] = tmpY + tn * t * p[3].Y;
 }
 /*
-* ComputeLeftTangent, ComputeRightTangent, ComputeCenterTangent :
-*Approximate unit tangents at endpoints and "center" of digitized curve
-*/
+ * ComputeLeftTangent, ComputeRightTangent, ComputeCenterTangent :
+ *Approximate unit tangents at endpoints and "center" of digitized curve
+ */
 function ComputeLeftTangent(d: Point[], end: number) {
     const use = 1;
     const tHat1 = new Point(d[end + use].X - d[end].X, d[end + use].Y - d[end].Y);
@@ -348,19 +381,19 @@ function ComputeCenterTangent(d: Point[], center: number) {
     }
     const V1 = ComputeLeftTangent(d, center); // d[center] - d[center-1];
     const V2 = ComputeRightTangent(d, center); // d[center] - d[center + 1];
-    var tHatCenter = new Point((-V1.X + V2.X) / 2.0, (-V1.Y + V2.Y) / 2.0);
+    let tHatCenter = new Point((-V1.X + V2.X) / 2.0, (-V1.Y + V2.Y) / 2.0);
     if (tHatCenter === new Point(0, 0)) {
-        tHatCenter = new Point(-V1.Y, -V1.X);// V1.Perp();
+        tHatCenter = new Point(-V1.Y, -V1.X); // V1.Perp();
     }
     return Normalize(tHatCenter);
 }
 function GenerateBezier(d: Point[], first: number, last: number, uPrime: number[], tHat1: Point, tHat2: Point, result: Point[] /* must be prealloacted to size 4 */) {
     const nPts = last - first + 1; // Number of pts in sub-curve
-    const Ax = new Array<number>(nPts * 2);// Precomputed rhs for eqn  //std::vector<Vector2D> A(nPts * 2);
-    const Ay = new Array<number>(nPts * 2);// Precomputed rhs for eqn  //std::vector<Vector2D> A(nPts * 2);
+    const Ax = new Array<number>(nPts * 2); // Precomputed rhs for eqn  //std::vector<Vector2D> A(nPts * 2);
+    const Ay = new Array<number>(nPts * 2); // Precomputed rhs for eqn  //std::vector<Vector2D> A(nPts * 2);
 
     /* Compute the A's  */
-    for (var i = 0; i < nPts; i++) {
+    for (let i = 0; i < nPts; i++) {
         const uprime = uPrime[i];
         const b1 = B1(uprime);
         const b2 = B2(uprime);
@@ -371,39 +404,42 @@ function GenerateBezier(d: Point[], first: number, last: number, uPrime: number[
     }
 
     /* Create the C and X matrices  */
-    const C = [[0, 0], [0, 0]];
+    const C = [
+        [0, 0],
+        [0, 0],
+    ];
     const df = d[first];
     const dl = d[last];
 
-    const X = [0, 0];    // Matrix X
-    for (var i = 0; i < nPts; i++) {
-        C[0][0] += Ax[i] * Ax[i] + Ay[i] * Ay[i];     //A[i+0*nPts].Dot(A[i+0*nPts]);
-        C[0][1] += Ax[i] * Ax[i + nPts] + Ay[i] * Ay[i + nPts];//A[i+0*nPts].Dot(A[i+1*nPts]);
+    const X = [0, 0]; // Matrix X
+    for (let i = 0; i < nPts; i++) {
+        C[0][0] += Ax[i] * Ax[i] + Ay[i] * Ay[i]; // A[i+0*nPts].Dot(A[i+0*nPts]);
+        C[0][1] += Ax[i] * Ax[i + nPts] + Ay[i] * Ay[i + nPts]; // A[i+0*nPts].Dot(A[i+1*nPts]);
         C[1][0] = C[0][1];
-        C[1][1] += Ax[i + nPts] * Ax[i + nPts] + Ay[i + nPts] * Ay[i + nPts];// A[i+1*nPts].Dot(A[i+1*nPts]);
+        C[1][1] += Ax[i + nPts] * Ax[i + nPts] + Ay[i + nPts] * Ay[i + nPts]; // A[i+1*nPts].Dot(A[i+1*nPts]);
         const uprime = uPrime[i];
         const b0plb1 = B0(uprime) + B1(uprime);
         const b2plb3 = B2(uprime) + B3(uprime);
         const df1 = d[first + i];
-        const tmpX = df1.X - (df.X * b0plb1 + (dl.X * b2plb3));
-        const tmpY = df1.Y - (df.Y * b0plb1 + (dl.Y * b2plb3));
+        const tmpX = df1.X - (df.X * b0plb1 + dl.X * b2plb3);
+        const tmpY = df1.Y - (df.Y * b0plb1 + dl.Y * b2plb3);
 
         X[0] += Ax[i] * tmpX + Ay[i] * tmpY; // A[i+0*nPts].Dot(tmp)
-        X[1] += Ax[i + nPts] * tmpX + Ay[i + nPts] * tmpY; //A[i+1*nPts].Dot(tmp)
+        X[1] += Ax[i + nPts] * tmpX + Ay[i + nPts] * tmpY; // A[i+1*nPts].Dot(tmp)
     }
 
     /* Compute the determinants of C and X  */
-    const det_C0_C1 = (C[0][0] * C[1][1] - C[1][0] * C[0][1]) || (C[0][0] * C[1][1]) * 10e-12;
+    const det_C0_C1 = C[0][0] * C[1][1] - C[1][0] * C[0][1] || C[0][0] * C[1][1] * 10e-12;
     const det_C0_X = C[0][0] * X[1] - C[0][1] * X[0];
     const det_X_C1 = X[0] * C[1][1] - X[1] * C[0][1];
 
     /* Finally, derive alpha values */
-    var alpha_l = (det_C0_C1 === 0) ? 0.0 : det_X_C1 / det_C0_C1;
-    var alpha_r = (det_C0_C1 === 0) ? 0.0 : det_C0_X / det_C0_C1;
+    let alpha_l = det_C0_C1 === 0 ? 0.0 : det_X_C1 / det_C0_C1;
+    let alpha_r = det_C0_C1 === 0 ? 0.0 : det_C0_X / det_C0_C1;
 
     /* If alpha negative, use the Wu/Barsky heuristic (see text) */
     /* (if alpha is 0, you get coincident control points that lead to
-    * divide by zero in any subsequent NewtonRaphsonRootFind() call. */
+     * divide by zero in any subsequent NewtonRaphsonRootFind() call. */
     const segLength = Math.sqrt((df.X - dl.X) * (df.X - dl.X) + (df.Y - dl.Y) * (df.Y - dl.Y));
     const epsilon = 1.0e-6 * segLength;
     if (alpha_l < epsilon || alpha_r < epsilon) {
@@ -415,10 +451,10 @@ function GenerateBezier(d: Point[], first: number, last: number, uPrime: number[
     /*  positioned exactly at the first and last data points */
     /*  Control points 1 and 2 are positioned an alpha distance out */
     /*  on the tangent vectors, left and right, respectively */
-    result[0] = df;// RETURN bezier curve ctl pts
+    result[0] = df; // RETURN bezier curve ctl pts
     result[3] = dl;
-    result[1] = new Point(df.X + (tHat1.X * alpha_l), df.Y + (tHat1.Y * alpha_l));
-    result[2] = new Point(dl.X + (tHat2.X * alpha_r), dl.Y + (tHat2.Y * alpha_r));
+    result[1] = new Point(df.X + tHat1.X * alpha_l, df.Y + tHat1.Y * alpha_l);
+    result[2] = new Point(dl.X + tHat2.X * alpha_r, dl.Y + tHat2.Y * alpha_r);
 }
 
 /*
@@ -426,21 +462,24 @@ function GenerateBezier(d: Point[], first: number, last: number, uPrime: number[
  *  Use Newton-Raphson iteration to find better root.
  */
 function NewtonRaphsonRootFind(Q: Point[], P: Point, u: number) {
-    const Q1 = [new Point(0, 0), new Point(0, 0), new Point(0, 0)], Q2 = [new Point(0, 0), new Point(0, 0)]; //  Q' and Q''
-    const Q_u = [0, 0], Q1_u = [0, 0], Q2_u = [0, 0]; //u evaluated at Q, Q', & Q''
+    const Q1 = [new Point(0, 0), new Point(0, 0), new Point(0, 0)];
+    const Q2 = [new Point(0, 0), new Point(0, 0)]; //  Q' and Q''
+    const Q_u = [0, 0];
+    const Q1_u = [0, 0];
+    const Q2_u = [0, 0]; // u evaluated at Q, Q', & Q''
 
     /* Compute Q(u) */
-    var uPrime: number; //  Improved u
+    let uPrime: number; //  Improved u
     EvalBezierFast(Q, u, Q_u);
 
     /* Generate control vertices for Q' */
-    for (var i = 0; i <= 2; i++) {
+    for (let i = 0; i <= 2; i++) {
         Q1[i].X = (Q[i + 1].X - Q[i].X) * 3.0;
         Q1[i].Y = (Q[i + 1].Y - Q[i].Y) * 3.0;
     }
 
     /* Generate control vertices for Q'' */
-    for (var i = 0; i <= 1; i++) {
+    for (let i = 0; i <= 1; i++) {
         Q2[i].X = (Q1[i + 1].X - Q1[i].X) * 2.0;
         Q2[i].Y = (Q1[i + 1].Y - Q1[i].Y) * 2.0;
     }
@@ -450,20 +489,20 @@ function NewtonRaphsonRootFind(Q: Point[], P: Point, u: number) {
     EvalBezier(Q2, 1, u, Q2_u);
 
     /* Compute f(u)/f'(u) */
-    const numerator = (Q_u[0] - P.X) * (Q1_u[0]) + (Q_u[1] - P.Y) * (Q1_u[1]);
-    const denominator = (Q1_u[0]) * (Q1_u[0]) + (Q1_u[1]) * (Q1_u[1]) + (Q_u[0] - P.X) * (Q2_u[0]) + (Q_u[1] - P.Y) * (Q2_u[1]);
+    const numerator = (Q_u[0] - P.X) * Q1_u[0] + (Q_u[1] - P.Y) * Q1_u[1];
+    const denominator = Q1_u[0] * Q1_u[0] + Q1_u[1] * Q1_u[1] + (Q_u[0] - P.X) * Q2_u[0] + (Q_u[1] - P.Y) * Q2_u[1];
     if (denominator === 0.0) {
         uPrime = u;
-    } else uPrime = u - (numerator / denominator);/* u = u - f(u)/f'(u) */
+    } else uPrime = u - numerator / denominator; /* u = u - f(u)/f'(u) */
 
     return uPrime;
 }
 function FitCubic(d: Point[], first: number, last: number, tHat1: Point, tHat2: Point, error: number, result: Point[]) {
-    const bezCurve = new Array<Point>(4);          //  Control points of fitted Bezier curve
-    const maxIterations = 4;      //  Max times to try iterating
+    const bezCurve = new Array<Point>(4); //  Control points of fitted Bezier curve
+    const maxIterations = 4; //  Max times to try iterating
 
     const iterationError = error * error; //  Error below which you try iterating
-    const nPts = last - first + 1;        //  Number of points in subset
+    const nPts = last - first + 1; //  Number of points in subset
 
     /*  Use heuristic if region only has two points in it */
     if (nPts === 2) {
@@ -471,8 +510,8 @@ function FitCubic(d: Point[], first: number, last: number, tHat1: Point, tHat2:
 
         bezCurve[0] = d[first];
         bezCurve[3] = d[last];
-        bezCurve[1] = new Point(bezCurve[0].X + (tHat1.X * dist), bezCurve[0].Y + (tHat1.Y * dist));
-        bezCurve[2] = new Point(bezCurve[3].X + (tHat2.X * dist), bezCurve[3].Y + (tHat2.Y * dist));
+        bezCurve[1] = new Point(bezCurve[0].X + tHat1.X * dist, bezCurve[0].Y + tHat1.Y * dist);
+        bezCurve[2] = new Point(bezCurve[3].X + tHat2.X * dist, bezCurve[3].Y + tHat2.Y * dist);
 
         result.push(bezCurve[1]);
         result.push(bezCurve[2]);
@@ -481,11 +520,11 @@ function FitCubic(d: Point[], first: number, last: number, tHat1: Point, tHat2:
     }
 
     /*  Parameterize points, and attempt to fit curve */
-    var u = ChordLengthParameterize(d, first, last);
+    let u = ChordLengthParameterize(d, first, last);
     GenerateBezier(d, first, last, u, tHat1, tHat2, bezCurve);
 
     /*  Find max deviation of points to fitted curve */
-    const { maxError, splitPoint2D } = ComputeMaxError(d, first, last, bezCurve, u);  //  Maximum fitting error
+    const { maxError, splitPoint2D } = ComputeMaxError(d, first, last, bezCurve, u); //  Maximum fitting error
     if (maxError < Math.abs(error)) {
         result.push(bezCurve[1]);
         result.push(bezCurve[2]);
@@ -496,8 +535,8 @@ function FitCubic(d: Point[], first: number, last: number, tHat1: Point, tHat2:
     /*  If error not too large, try some reparameterization  */
     /*  and iteration */
     if (maxError < iterationError) {
-        for (var i = 0; i < maxIterations; i++) {
-            const uPrime = ReparameterizeBezier(d, first, last, u, bezCurve);   //  Improved parameter values
+        for (let i = 0; i < maxIterations; i++) {
+            const uPrime = ReparameterizeBezier(d, first, last, u, bezCurve); //  Improved parameter values
             GenerateBezier(d, first, last, uPrime, tHat1, tHat2, bezCurve);
             const { maxError } = ComputeMaxError(d, first, last, bezCurve, uPrime);
             if (maxError < error) {
@@ -527,13 +566,13 @@ export function FitOneCurve(d: Point[], tHat1?: Point, tHat2?: Point) {
     tHat1 = tHat1 ?? Normalize(ComputeLeftTangent(d, 0));
     tHat2 = tHat2 ?? Normalize(ComputeRightTangent(d, d.length - 1));
     tHat2 = new Point(-tHat2.X, -tHat2.Y);
-    var u = ChordLengthParameterize(d, 0, d.length - 1);
+    let u = ChordLengthParameterize(d, 0, d.length - 1);
     const bezCurveCtrls = [new Point(0, 0), new Point(0, 0), new Point(0, 0), new Point(0, 0)];
-    GenerateBezier(d, 0, d.length - 1, u, tHat1, tHat2, bezCurveCtrls);    /*  Find max deviation of points to fitted curve */
-    var finalCtrls = bezCurveCtrls.slice();
-    var { maxError: error } = ComputeMaxError(d, 0, d.length - 1, bezCurveCtrls, u);
-    for (var i = 0; i < 10; i++) {
-        const uPrime = ReparameterizeBezier(d, 0, d.length - 1, u, bezCurveCtrls);   //  Improved parameter values
+    GenerateBezier(d, 0, d.length - 1, u, tHat1, tHat2, bezCurveCtrls); /*  Find max deviation of points to fitted curve */
+    let finalCtrls = bezCurveCtrls.slice();
+    let { maxError: error } = ComputeMaxError(d, 0, d.length - 1, bezCurveCtrls, u);
+    for (let i = 0; i < 10; i++) {
+        const uPrime = ReparameterizeBezier(d, 0, d.length - 1, u, bezCurveCtrls); //  Improved parameter values
         GenerateBezier(d, 0, d.length - 1, uPrime, tHat1, tHat2, bezCurveCtrls);
         const { maxError } = ComputeMaxError(d, 0, d.length - 1, bezCurveCtrls, uPrime);
         if (maxError < error) {
@@ -1428,4 +1467,4 @@ spliceR[0] += v;
 }
 #endif
 
-*/
\ No newline at end of file
+*/
diff --git a/src/client/util/reportManager/ReportManager.tsx b/src/client/util/reportManager/ReportManager.tsx
index 0c49aeed4..d6fd339a9 100644
--- a/src/client/util/reportManager/ReportManager.tsx
+++ b/src/client/util/reportManager/ReportManager.tsx
@@ -1,23 +1,24 @@
-import * as React from 'react';
-import * as uuid from 'uuid';
-import '.././SettingsManager.scss';
-import './ReportManager.scss';
-import ReactLoading from 'react-loading';
+import { Octokit } from '@octokit/core';
+import { Button, Dropdown, DropdownType, IconButton, Type } from 'browndash-components';
 import { action, makeObservable, observable } from 'mobx';
-import { BsX, BsArrowsAngleExpand, BsArrowsAngleContract } from 'react-icons/bs';
+import { observer } from 'mobx-react';
+import * as React from 'react';
+import { BsArrowsAngleContract, BsArrowsAngleExpand, BsX } from 'react-icons/bs';
 import { CgClose } from 'react-icons/cg';
 import { HiOutlineArrowLeft } from 'react-icons/hi';
-import { Issue } from './reportManagerSchema';
-import { observer } from 'mobx-react';
+import { MdRefresh } from 'react-icons/md';
+import ReactLoading from 'react-loading';
+import * as uuid from 'uuid';
+import { ClientUtils } from '../../../ClientUtils';
 import { Doc } from '../../../fields/Doc';
-import { MainViewModal } from '../../views/MainViewModal';
-import { Octokit } from '@octokit/core';
-import { Button, Dropdown, DropdownType, IconButton, Type } from 'browndash-components';
-import { BugType, FileData, Priority, ReportForm, ViewState, bugDropdownItems, darkColors, emptyReportForm, formatTitle, getAllIssues, isDarkMode, lightColors, passesTagFilter, priorityDropdownItems, uploadFilesToServer } from './reportManagerUtils';
-import { Filter, FormInput, FormTextArea, IssueCard, IssueView, Tag } from './ReportManagerComponents';
 import { StrCast } from '../../../fields/Types';
-import { MdRefresh } from 'react-icons/md';
+import { MainViewModal } from '../../views/MainViewModal';
+import '.././SettingsManager.scss';
 import { SettingsManager } from '../SettingsManager';
+import './ReportManager.scss';
+import { Filter, FormInput, FormTextArea, IssueCard, IssueView } from './ReportManagerComponents';
+import { Issue } from './reportManagerSchema';
+import { BugType, FileData, Priority, ReportForm, ViewState, bugDropdownItems, darkColors, emptyReportForm, formatTitle, getAllIssues, isDarkMode, lightColors, passesTagFilter, priorityDropdownItems, uploadFilesToServer } from './reportManagerUtils';
 
 /**
  * Class for reporting and viewing Github issues within the app.
@@ -133,7 +134,7 @@ export class ReportManager extends React.Component<{}> {
         const req = await this.octokit.request('POST /repos/{owner}/{repo}/issues', {
             owner: 'brown-dash',
             repo: 'Dash-Web',
-            title: formatTitle(this.formData.title, Doc.CurrentUserEmail),
+            title: formatTitle(this.formData.title, ClientUtils.CurrentUserEmail),
             body: `${this.formData.description} ${formattedLinks.length > 0 ? `\n\nFiles:\n${formattedLinks.join('\n')}` : ''}`,
             labels: ['from-dash-app', this.formData.type, this.formData.priority],
         });
diff --git a/src/client/views/ContextMenu.tsx b/src/client/views/ContextMenu.tsx
index ca877b93e..18b433a77 100644
--- a/src/client/views/ContextMenu.tsx
+++ b/src/client/views/ContextMenu.tsx
@@ -1,16 +1,16 @@
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { action, computed, IReactionDisposer, makeObservable, observable, reaction } from 'mobx';
+import { action, computed, IReactionDisposer, makeObservable, observable } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
-import { StrCast } from '../../fields/Types';
-import { SettingsManager } from '../util/SettingsManager';
+import { DivHeight, DivWidth } from '../../ClientUtils';
+import { SnappingManager } from '../util/SnappingManager';
 import './ContextMenu.scss';
 import { ContextMenuItem, ContextMenuProps, OriginalMenuProps } from './ContextMenuItem';
 import { ObservableReactComponent } from './ObservableReactComponent';
-import { DivHeight, DivWidth } from '../../Utils';
 
 @observer
 export class ContextMenu extends ObservableReactComponent<{}> {
+    // eslint-disable-next-line no-use-before-define
     static Instance: ContextMenu;
 
     private _ignoreUp = false;
@@ -124,8 +124,8 @@ export class ContextMenu extends ObservableReactComponent<{}> {
 
     @action
     displayMenu = (x: number, y: number, initSearch = '', showSearch = false, onDisplay?: () => void) => {
-        //maxX and maxY will change if the UI/font size changes, but will work for any amount
-        //of items added to the menu
+        // maxX and maxY will change if the UI/font size changes, but will work for any amount
+        // of items added to the menu
 
         this._showSearch = showSearch;
         this._pageX = x;
@@ -192,7 +192,7 @@ export class ContextMenu extends ObservableReactComponent<{}> {
                     key={index + value.join(' -> ')}
                     className="contextMenu-group"
                     style={{
-                        background: StrCast(SettingsManager.userVariantColor),
+                        background: SnappingManager.userVariantColor,
                     }}>
                     <div className="contextMenu-description">{value.join(' -> ')}</div>
                 </div>
@@ -224,11 +224,11 @@ export class ContextMenu extends ObservableReactComponent<{}> {
                     display: this._display ? '' : 'none',
                     left: this.pageX,
                     ...(this._yRelativeToTop ? { top: Math.max(0, this.pageY) } : { bottom: this.pageY }),
-                    background: SettingsManager.userBackgroundColor,
-                    color: SettingsManager.userColor,
+                    background: SnappingManager.userBackgroundColor,
+                    color: SnappingManager.userColor,
                 }}>
                 {!this.itemsNeedSearch ? null : (
-                    <span className={'search-icon'}>
+                    <span className="search-icon">
                         <span className="icon-background">
                             <FontAwesomeIcon icon="search" size="lg" />
                         </span>
@@ -267,7 +267,7 @@ export class ContextMenu extends ObservableReactComponent<{}> {
             if (item) {
                 item.event({ x: this.pageX, y: this.pageY });
             } else {
-                //if (this._searchString.startsWith(this._defaultPrefix)) {
+                // if (this._searchString.startsWith(this._defaultPrefix)) {
                 this._defaultItem?.(this._searchString.substring(this._defaultPrefix.length));
             }
             this.closeMenu();
@@ -281,12 +281,10 @@ export class ContextMenu extends ObservableReactComponent<{}> {
         this._searchString = e.target.value;
         if (!this._searchString) {
             this._selectedIndex = -1;
+        } else if (this._selectedIndex === -1) {
+            this._selectedIndex = 0;
         } else {
-            if (this._selectedIndex === -1) {
-                this._selectedIndex = 0;
-            } else {
-                this._selectedIndex = Math.min(this.flatItems.length - 1, this._selectedIndex);
-            }
+            this._selectedIndex = Math.min(this.flatItems.length - 1, this._selectedIndex);
         }
     };
 }
diff --git a/src/client/views/ContextMenuItem.tsx b/src/client/views/ContextMenuItem.tsx
index d15ab749c..5760872fb 100644
--- a/src/client/views/ContextMenuItem.tsx
+++ b/src/client/views/ContextMenuItem.tsx
@@ -1,17 +1,17 @@
-import * as React from 'react';
-import { observable, action, runInAction, makeObservable } from 'mobx';
-import { observer } from 'mobx-react';
 import { IconProp } from '@fortawesome/fontawesome-svg-core';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { action, makeObservable, observable, runInAction } from 'mobx';
+import { observer } from 'mobx-react';
+import * as React from 'react';
+import { SnappingManager } from '../util/SnappingManager';
 import { UndoManager } from '../util/UndoManager';
-import { SettingsManager } from '../util/SettingsManager';
 import { ObservableReactComponent } from './ObservableReactComponent';
 
 export interface OriginalMenuProps {
     description: string;
     event: (stuff?: any) => void;
     undoable?: boolean;
-    icon: IconProp | JSX.Element; //maybe should be optional (icon?)
+    icon: IconProp | JSX.Element; // maybe should be optional (icon?)
     closeMenu?: () => void;
 }
 
@@ -20,7 +20,7 @@ export interface SubmenuProps {
     subitems: ContextMenuProps[];
     noexpand?: boolean;
     addDivider?: boolean;
-    icon: IconProp; //maybe should be optional (icon?)
+    icon: IconProp; // maybe should be optional (icon?)
     closeMenu?: () => void;
 }
 
@@ -67,7 +67,9 @@ export class ContextMenuItem extends ObservableReactComponent<ContextMenuProps &
         this._overPosY = e.clientY;
         this._overPosX = e.clientX;
         this.currentTimeout = setTimeout(
-            action(() => (this.overItem = true)),
+            action(() => {
+                this.overItem = true;
+            }),
             ContextMenuItem.timeout
         );
     };
@@ -97,14 +99,15 @@ export class ContextMenuItem extends ObservableReactComponent<ContextMenuProps &
                     {this._props.icon ? <span className="contextMenu-item-icon-background">{this.isJSXElement(this._props.icon) ? this._props.icon : <FontAwesomeIcon icon={this._props.icon} size="sm" />}</span> : null}
                     <div className="contextMenu-description">{this._props.description.replace(':', '')}</div>
                     <div
-                        className={`contextMenu-item-background`}
+                        className="contextMenu-item-background"
                         style={{
-                            background: SettingsManager.userColor,
+                            background: SnappingManager.userColor,
                         }}
                     />
                 </div>
             );
-        } else if ('subitems' in this._props) {
+        }
+        if ('subitems' in this._props) {
             const where = !this.overItem ? '' : this._overPosY < window.innerHeight / 3 ? 'flex-start' : this._overPosY > (window.innerHeight * 2) / 3 ? 'flex-end' : 'center';
             const marginTop = !this.overItem ? '' : this._overPosY < window.innerHeight / 3 ? '20px' : this._overPosY > (window.innerHeight * 2) / 3 ? '-20px' : '';
 
@@ -115,7 +118,7 @@ export class ContextMenuItem extends ObservableReactComponent<ContextMenuProps &
                     style={{
                         marginLeft: window.innerWidth - this._overPosX - 50 > 0 ? '90%' : '20%',
                         marginTop,
-                        background: SettingsManager.userBackgroundColor,
+                        background: SnappingManager.userBackgroundColor,
                     }}>
                     {this._items.map(prop => (
                         <ContextMenuItem {...prop} key={prop.description} closeMenu={this._props.closeMenu} />
@@ -149,7 +152,7 @@ export class ContextMenuItem extends ObservableReactComponent<ContextMenuProps &
                     <div
                         className={`contextMenu-item-background`}
                         style={{
-                            background: SettingsManager.userColor,
+                            background: SnappingManager.userColor,
                         }}
                     />
                     {submenu}
diff --git a/src/client/views/DashboardView.tsx b/src/client/views/DashboardView.tsx
index a4f598d1a..1d286c987 100644
--- a/src/client/views/DashboardView.tsx
+++ b/src/client/views/DashboardView.tsx
@@ -4,6 +4,7 @@ import { action, computed, makeObservable, observable } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
 import { FaPlus } from 'react-icons/fa';
+import { ClientUtils } from '../../ClientUtils';
 import { Doc, DocListCast } from '../../fields/Doc';
 import { AclPrivate, DocAcl, DocData } from '../../fields/DocSymbols';
 import { Id } from '../../fields/FieldSymbols';
@@ -14,21 +15,19 @@ import { ScriptField } from '../../fields/ScriptField';
 import { Cast, ImageCast, StrCast } from '../../fields/Types';
 import { normalizeEmail } from '../../fields/util';
 import { DocServer } from '../DocServer';
-import { Docs, DocumentOptions, DocUtils } from '../documents/Documents';
+import { DocUtils, Docs, DocumentOptions } from '../documents/Documents';
+import { dropActionType } from '../util/DropActionTypes';
 import { HistoryUtil } from '../util/History';
 import { ScriptingGlobals } from '../util/ScriptingGlobals';
-import { SettingsManager } from '../util/SettingsManager';
 import { SharingManager } from '../util/SharingManager';
-import { undoable, undoBatch, UndoManager } from '../util/UndoManager';
-import { CollectionDockingView } from './collections/CollectionDockingView';
-import { CollectionView } from './collections/CollectionView';
+import { SnappingManager } from '../util/SnappingManager';
+import { undoBatch, undoable } from '../util/UndoManager';
 import { ContextMenu } from './ContextMenu';
 import './DashboardView.scss';
-import { Colors } from './global/globalEnums';
 import { MainViewModal } from './MainViewModal';
-import { ButtonType } from './nodes/FontIconBox/FontIconBox';
 import { ObservableReactComponent } from './ObservableReactComponent';
-import { dropActionType } from '../util/DragManager';
+import { Colors } from './global/globalEnums';
+import { ButtonType } from './nodes/FontIconBox/FontIconBox';
 
 enum DashboardGroup {
     MyDashboards,
@@ -65,7 +64,7 @@ export class DashboardView extends ObservableReactComponent<{}> {
 
     getDashboards = (whichGroup: DashboardGroup) => {
         if (whichGroup === DashboardGroup.MyDashboards) {
-            return DocListCast(Doc.MyDashboards.data).filter(dashboard => dashboard[DocData].author === Doc.CurrentUserEmail);
+            return DocListCast(Doc.MyDashboards.data).filter(dashboard => dashboard[DocData].author === ClientUtils.CurrentUserEmail);
         }
         return DocListCast(Doc.MySharedDocs.data_dashboards).filter(doc => doc.dockingConfig);
     };
@@ -84,11 +83,11 @@ export class DashboardView extends ObservableReactComponent<{}> {
             <div
                 className="new-dashboard"
                 style={{
-                    background: SettingsManager.userBackgroundColor,
-                    color: SettingsManager.userColor,
+                    background: SnappingManager.userBackgroundColor,
+                    color: SnappingManager.userColor,
                 }}>
                 <div className="header">Create New Dashboard</div>
-                <EditableText formLabel="Title" placeholder={this.newDashboardName} type={Type.SEC} color={SettingsManager.userColor} setVal={val => this.setNewDashboardName(val as string)} fillWidth />
+                <EditableText formLabel="Title" placeholder={this.newDashboardName} type={Type.SEC} color={SnappingManager.userColor} setVal={val => this.setNewDashboardName(val as string)} fillWidth />
                 <ColorPicker
                     formLabel="Background" //
                     colorPickerType="github"
@@ -98,8 +97,8 @@ export class DashboardView extends ObservableReactComponent<{}> {
                     setSelectedColor={this.setNewDashboardColor}
                 />
                 <div className="button-bar">
-                    <Button text="Cancel" color={SettingsManager.userColor} onClick={this.abortCreateNewDashboard} />
-                    <Button text="Create" color={SettingsManager.userVariantColor} type={Type.TERT} onClick={() => this.createNewDashboard(this.newDashboardName, this.newDashboardColor)} />
+                    <Button text="Cancel" color={SnappingManager.userColor} onClick={this.abortCreateNewDashboard} />
+                    <Button text="Create" color={SnappingManager.userVariantColor} type={Type.TERT} onClick={() => this.createNewDashboard(this.newDashboardName, this.newDashboardColor)} />
                 </div>
             </div>
         );
@@ -133,8 +132,8 @@ export class DashboardView extends ObservableReactComponent<{}> {
     };
 
     render() {
-        const color = SettingsManager.userColor;
-        const variant = SettingsManager.userVariantColor;
+        const color = SnappingManager.userColor;
+        const variant = SnappingManager.userVariantColor;
         return (
             <>
                 <div className="dashboard-view">
@@ -156,7 +155,7 @@ export class DashboardView extends ObservableReactComponent<{}> {
                             : this.getDashboards(this.selectedDashboardGroup).map(dashboard => {
                                   const href = ImageCast(dashboard.thumb)?.url?.href;
                                   const shared = Object.keys(dashboard[DocAcl])
-                                      .filter(key => key !== `acl-${normalizeEmail(Doc.CurrentUserEmail)}` && !['acl-Me', 'acl-Guest'].includes(key))
+                                      .filter(key => key !== `acl-${normalizeEmail(ClientUtils.CurrentUserEmail)}` && !['acl-Me', 'acl-Guest'].includes(key))
                                       .some(key => dashboard[DocAcl][key] !== AclPrivate);
                                   return (
                                       <div
@@ -187,7 +186,7 @@ export class DashboardView extends ObservableReactComponent<{}> {
                                           <div
                                               className="background"
                                               style={{
-                                                  background: SettingsManager.userColor,
+                                                  background: SnappingManager.userColor,
                                                   filter: 'opacity(0.2)',
                                               }}
                                           />
@@ -201,7 +200,7 @@ export class DashboardView extends ObservableReactComponent<{}> {
                                 <div
                                     className="background"
                                     style={{
-                                        background: SettingsManager.userColor,
+                                        background: SnappingManager.userColor,
                                         filter: 'opacity(0.2)',
                                     }}
                                 />
@@ -217,9 +216,6 @@ export class DashboardView extends ObservableReactComponent<{}> {
     public static closeActiveDashboard() {
         Doc.ActiveDashboard = undefined;
     }
-    public static snapshotDashboard() {
-        return CollectionDockingView.TakeSnapshot(Doc.ActiveDashboard);
-    }
 
     public static openSharedDashboard = (dashboard: Doc) => {
         Doc.AddDocToList(Doc.MySharedDocs, 'viewed', dashboard);
@@ -250,16 +246,16 @@ export class DashboardView extends ObservableReactComponent<{}> {
                 });
             if (state.readonly === true || state.readonly === null) {
                 DocServer.Control.makeReadOnly();
-            } else if (state.safe) {
-                if (!state.nro) {
-                    DocServer.Control.makeReadOnly();
-                }
-                CollectionView.SetSafeMode(true);
+                // } else if (state.safe) {
+                //     if (!state.nro) {
+                //         DocServer.Control.makeReadOnly();
+                //     }
+                //     CollectionView.SetSafeMode(true);
             } else if (state.nro || state.nro === null || state.readonly === false) {
             } else if (doc.readOnly) {
                 DocServer.Control.makeReadOnly();
             } else {
-                Doc.CurrentUserEmail !== 'guest' && DocServer.Control.makeEditable();
+                ClientUtils.CurrentUserEmail !== 'guest' && DocServer.Control.makeEditable();
             }
         }
 
@@ -473,23 +469,23 @@ export class DashboardView extends ObservableReactComponent<{}> {
     }
 }
 
+// eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function createNewDashboard() {
     return DashboardView.createNewDashboard();
 }, 'creates a new dashboard when called');
+// eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function shareDashboard(dashboard: Doc) {
     SharingManager.Instance.open(undefined, dashboard);
 }, 'opens sharing dialog for Dashboard');
+// eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function removeDashboard(dashboard: Doc) {
     DashboardView.removeDashboard(dashboard);
 }, 'Remove Dashboard from Dashboards');
+// eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function resetDashboard(dashboard: Doc) {
     DashboardView.resetDashboard(dashboard);
 }, 'move all dashboard tabs to single stack');
+// eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function addToDashboards(dashboard: Doc) {
     DashboardView.openDashboard(Doc.MakeEmbedding(dashboard));
 }, 'adds Dashboard to set of Dashboards');
-ScriptingGlobals.add(async function snapshotDashboard() {
-    const batch = UndoManager.StartBatch('snapshot');
-    await DashboardView.snapshotDashboard();
-    batch.end();
-}, 'creates a snapshot copy of a dashboard');
diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx
index de4df1830..348b18129 100644
--- a/src/client/views/DocComponent.tsx
+++ b/src/client/views/DocComponent.tsx
@@ -1,22 +1,52 @@
 import { action, computed, makeObservable, observable } from 'mobx';
 import * as React from 'react';
-import { returnFalse } from '../../Utils';
+import { returnFalse } from '../../ClientUtils';
 import { DateField } from '../../fields/DateField';
-import { Doc, DocListCast, Field, Opt } from '../../fields/Doc';
-import { AclAdmin, AclAugment, AclEdit, AclPrivate, AclReadonly, DocData } from '../../fields/DocSymbols';
+import { Doc, DocListCast, FieldType, Opt } from '../../fields/Doc';
+import { AclAdmin, AclAugment, AclEdit, AclPrivate, AclReadonly, DocData, DocViews } from '../../fields/DocSymbols';
 import { List } from '../../fields/List';
 import { RefField } from '../../fields/RefField';
 import { GetEffectiveAcl, inheritParentAcls } from '../../fields/util';
 import { DocumentType } from '../documents/DocumentTypes';
-import { DocUtils } from '../documents/Documents';
-import { DocumentManager } from '../util/DocumentManager';
 import { DragManager } from '../util/DragManager';
 import { ObservableReactComponent } from './ObservableReactComponent';
-import { CollectionFreeFormView } from './collections/collectionFreeForm';
 import { DocumentView, OpenWhere } from './nodes/DocumentView';
 import { FieldViewProps, FocusViewOptions } from './nodes/FieldView';
-import { PinProps } from './nodes/trails';
+// import { DocUtils } from '../documents/Documents';
 
+export interface pinDataTypes {
+    scrollable?: boolean;
+    dataviz?: number[];
+    pannable?: boolean;
+    type_collection?: boolean;
+    inkable?: boolean;
+    filters?: boolean;
+    pivot?: boolean;
+    temporal?: boolean;
+    clippable?: boolean;
+    datarange?: boolean;
+    dataview?: boolean;
+    poslayoutview?: boolean;
+    dataannos?: boolean;
+    map?: boolean;
+}
+
+export interface MarqueeViewBounds {
+    left: number;
+    top: number;
+    width: number;
+    height: number;
+}
+export interface PinProps {
+    audioRange?: boolean;
+    activeFrame?: number;
+    currentFrame?: number;
+    hidePresBox?: boolean;
+    pinViewport?: MarqueeViewBounds; // pin a specific viewport on a freeform view (use MarqueeView.CurViewBounds to compute if no region has been selected)
+    pinDocLayout?: boolean; // pin layout info (width/height/x/y)
+    pinAudioPlay?: boolean; // pin audio annotation
+    pinData?: pinDataTypes;
+}
 /**
  * Shared interface among all viewBox'es (ie, react classes that render the contents of a Doc)
  * Many of these methods only make sense for specific viewBox'es, but they should be written to
@@ -25,6 +55,7 @@ import { PinProps } from './nodes/trails';
 export interface ViewBoxInterface {
     fieldKey?: string;
     annotationKey?: string;
+    promoteCollection?: () => void; // moves contents of collection to parent
     updateIcon?: () => void; // updates the icon representation of the document
     getAnchor?: (addAsAnnotation: boolean, pinData?: PinProps) => Doc; // returns an Anchor Doc that represents the current state of the doc's componentview (e.g., the current playhead location of a an audio/video box)
     restoreView?: (viewSpec: Doc) => boolean;
@@ -46,7 +77,7 @@ export interface ViewBoxInterface {
     IsPlaying?: () => boolean; // is a media document playing
     TogglePause?: (keep?: boolean) => void; // toggle media document playing state
     setFocus?: () => void; // sets input focus to the componentView
-    setData?: (data: Field | Promise<RefField | undefined>) => boolean;
+    setData?: (data: FieldType | Promise<RefField | undefined>) => boolean;
     componentUI?: (boundsLeft: number, boundsTop: number) => JSX.Element | null;
     dragStarting?: (snapToDraggedDoc: boolean, showGroupDragTarget: boolean, visited: Set<Doc>) => void;
     dragConfig?: (dragData: DragManager.DocumentDragData) => void; // function to setup dragData in custom way (see TreeViews which add a tree view flag)
@@ -228,7 +259,7 @@ export function ViewBoxAnnotatableComponent<P extends FieldViewProps>() {
             if (toRemove.length !== 0) {
                 const recentlyClosed = this.Document !== Doc.MyRecentlyClosed ? Doc.MyRecentlyClosed : undefined;
                 toRemove.forEach(doc => {
-                    leavePushpin && DocUtils.LeavePushpin(doc, annotationKey ?? this.annotationKey);
+                    // leavePushpin && DocUtils.LeavePushpin(doc, annotationKey ?? this.annotationKey);
                     Doc.RemoveDocFromList(targetDataDoc, annotationKey ?? this.annotationKey, doc, true);
                     doc.embedContainer = undefined;
                     if (recentlyClosed && !dontAddToRemoved && doc.type !== DocumentType.LOADING) {
@@ -237,7 +268,7 @@ export function ViewBoxAnnotatableComponent<P extends FieldViewProps>() {
                     }
                 });
                 if (targetDataDoc.isGroup && DocListCast(targetDataDoc[annotationKey ?? this.annotationKey]).length < 2) {
-                    (DocumentManager.Instance.getFirstDocumentView(targetDataDoc)?.ComponentView as CollectionFreeFormView)?.promoteCollection();
+                    Array.from(targetDataDoc[DocViews])[0]?.ComponentView?.promoteCollection?.();
                 } else {
                     this.isAnyChildContentActive() && this._props.select(false);
                 }
diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx
index 15ce4c15f..16586d922 100644
--- a/src/client/views/DocumentButtonBar.tsx
+++ b/src/client/views/DocumentButtonBar.tsx
@@ -1,32 +1,34 @@
 import { IconLookup, IconProp } from '@fortawesome/fontawesome-svg-core';
+import { faCalendarDays } from '@fortawesome/free-solid-svg-icons';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { Tooltip } from '@mui/material';
-import { action, computed, makeObservable, observable, runInAction } from 'mobx';
+import { Popup } from 'browndash-components';
+import { action, computed, makeObservable, observable } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
-import { emptyFunction, returnFalse, returnTrue, setupMoveUpEvents, simulateMouseClick } from '../../Utils';
+import { FaEdit } from 'react-icons/fa';
+import { returnFalse, returnTrue, setupMoveUpEvents, simulateMouseClick } from '../../ClientUtils';
+import { emptyFunction } from '../../Utils';
 import { Doc } from '../../fields/Doc';
 import { Cast, DocCast } from '../../fields/Types';
 import { DocUtils } from '../documents/Documents';
 import { CalendarManager } from '../util/CalendarManager';
-import { DragManager, dropActionType } from '../util/DragManager';
+import { DragManager } from '../util/DragManager';
+import { dropActionType } from '../util/DropActionTypes';
 import { IsFollowLinkScript } from '../util/LinkFollower';
 import { SelectionManager } from '../util/SelectionManager';
 import { SharingManager } from '../util/SharingManager';
 import { UndoManager, undoBatch } from '../util/UndoManager';
+import { PinProps } from './DocComponent';
 import './DocumentButtonBar.scss';
 import { ObservableReactComponent } from './ObservableReactComponent';
+import { TemplateMenu } from './TemplateMenu';
 import { TabDocView } from './collections/TabDocView';
 import { Colors } from './global/globalEnums';
 import { LinkPopup } from './linking/LinkPopup';
 import { DocumentLinksButton } from './nodes/DocumentLinksButton';
 import { DocumentView, DocumentViewInternal, OpenWhere } from './nodes/DocumentView';
 import { DashFieldView } from './nodes/formattedText/DashFieldView';
-import { PinProps } from './nodes/trails';
-import { faCalendarDays } from '@fortawesome/free-solid-svg-icons';
-import { Popup } from 'browndash-components';
-import { TemplateMenu } from './TemplateMenu';
-import { FaEdit } from 'react-icons/fa';
 
 @observer
 export class DocumentButtonBar extends ObservableReactComponent<{ views: () => (DocumentView | undefined)[]; stack?: any }> {
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 2a44a9739..009097b3b 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -5,9 +5,10 @@ import { action, computed, makeObservable, observable, runInAction } from 'mobx'
 import { observer } from 'mobx-react';
 import * as React from 'react';
 import { FaUndo } from 'react-icons/fa';
-import { Utils, emptyFunction, lightOrDark, numberValue, returnFalse, setupMoveUpEvents } from '../../Utils';
+import { lightOrDark, returnFalse, setupMoveUpEvents } from '../../ClientUtils';
+import { Utils, numberValue, emptyFunction } from '../../Utils';
 import { DateField } from '../../fields/DateField';
-import { Doc, DocListCast, Field, HierarchyMapping, ReverseHierarchyMap } from '../../fields/Doc';
+import { Doc, DocListCast, Field, FieldType, HierarchyMapping, ReverseHierarchyMap } from '../../fields/Doc';
 import { AclAdmin, AclAugment, AclEdit, DocData } from '../../fields/DocSymbols';
 import { InkField } from '../../fields/InkField';
 import { ScriptField } from '../../fields/ScriptField';
@@ -34,7 +35,7 @@ import { DocumentView, OpenWhereMod } from './nodes/DocumentView';
 import { ImageBox } from './nodes/ImageBox';
 import { KeyValueBox } from './nodes/KeyValueBox';
 import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox';
-import { identity } from 'lodash';
+import { Id } from '../../fields/FieldSymbols';
 
 interface DocumentDecorationsProps {
     PanelWidth: number;
@@ -44,6 +45,7 @@ interface DocumentDecorationsProps {
 }
 @observer
 export class DocumentDecorations extends ObservableReactComponent<DocumentDecorationsProps> {
+    // eslint-disable-next-line no-use-before-define
     static Instance: DocumentDecorations;
     private _resizeHdlId = '';
     private _keyinput = React.createRef<HTMLInputElement>();
@@ -123,7 +125,9 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
             this._titleControlString = this._accumulatedTitle;
         } else if (this._titleControlString.startsWith('$')) {
             if (this._accumulatedTitle.startsWith('-->#')) {
-                SelectionManager.Docs.forEach(doc => (doc[DocData].onViewMounted = ScriptField.MakeScript(`updateTagsCollection(this)`)));
+                SelectionManager.Docs.forEach(doc => {
+                    doc[DocData].onViewMounted = ScriptField.MakeScript(`updateTagsCollection(this)`);
+                });
             }
             const titleFieldKey = this._titleControlString.substring(1);
             UndoManager.RunInBatch(
@@ -149,7 +153,7 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
 
     onContainerDown = (e: React.PointerEvent) => {
         const effectiveLayoutAcl = GetEffectiveAcl(SelectionManager.Views[0].Document);
-        if (effectiveLayoutAcl == AclAdmin || effectiveLayoutAcl == AclEdit || effectiveLayoutAcl == AclAugment) {
+        if (effectiveLayoutAcl === AclAdmin || effectiveLayoutAcl === AclEdit || effectiveLayoutAcl === AclAugment) {
             setupMoveUpEvents(this, e, e => this.onBackgroundMove(true, e), emptyFunction, emptyFunction);
             e.stopPropagation();
         }
@@ -157,13 +161,13 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
 
     onTitleDown = (e: React.PointerEvent) => {
         const effectiveLayoutAcl = GetEffectiveAcl(SelectionManager.Views[0].Document);
-        if (effectiveLayoutAcl == AclAdmin || effectiveLayoutAcl == AclEdit || effectiveLayoutAcl == AclAugment) {
+        if (effectiveLayoutAcl === AclAdmin || effectiveLayoutAcl === AclEdit || effectiveLayoutAcl === AclAugment) {
             setupMoveUpEvents(
                 this,
                 e,
                 e => this.onBackgroundMove(true, e),
                 emptyFunction,
-                action(e => {
+                action(() => {
                     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;
@@ -182,7 +186,7 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
     onBackgroundMove = (dragTitle: boolean, e: PointerEvent): boolean => {
         const dragDocView = SelectionManager.Views[0];
         const effectiveLayoutAcl = GetEffectiveAcl(dragDocView.Document);
-        if (effectiveLayoutAcl != AclAdmin && effectiveLayoutAcl != AclEdit && effectiveLayoutAcl != AclAugment) {
+        if (effectiveLayoutAcl !== AclAdmin && effectiveLayoutAcl !== AclEdit && effectiveLayoutAcl !== AclAugment) {
             return false;
         }
         const containers = new Set<Doc | undefined>();
@@ -205,7 +209,9 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
             e.x,
             e.y,
             {
-                dragComplete: action(e => (this._hidden = false)),
+                dragComplete: action(() => {
+                    this._hidden = false;
+                }),
                 hideSource: true,
             }
         );
@@ -217,15 +223,16 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
     onCloseClick = (forceDeleteOrIconify: boolean | undefined) => {
         const views = SelectionManager.Views.filter(v => v && v._props.renderDepth > 0);
         if (forceDeleteOrIconify === false && this._iconifyBatch) return;
-        this._deleteAfterIconify = forceDeleteOrIconify || this._iconifyBatch ? true : false;
-        var iconifyingCount = views.length;
+        this._deleteAfterIconify = !!(forceDeleteOrIconify || this._iconifyBatch);
+        let iconifyingCount = views.length;
         const finished = action((force?: boolean) => {
             if ((force || --iconifyingCount === 0) && this._iconifyBatch) {
                 if (this._deleteAfterIconify) {
                     views.forEach(iconView => {
-                        Doc.setNativeView(iconView.Document);
-                        if (iconView.Document.activeFrame) {
-                            iconView.Document.opacity = 0; // bcz: hacky ... allows inkMasks and other documents to be "turned off" without removing them from the animated collection which allows them to function properly in a presenation.
+                        const iconViewDoc = iconView.Document;
+                        Doc.setNativeView(iconViewDoc);
+                        if (iconViewDoc.activeFrame) {
+                            iconViewDoc.opacity = 0; // bcz: hacky ... allows inkMasks and other documents to be "turned off" without removing them from the animated collection which allows them to function properly in a presenation.
                         } else {
                             iconView._props.removeDocument?.(iconView.Document);
                         }
@@ -240,6 +247,7 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
             (document.activeElement as any).blur?.();
             this._iconifyBatch = UndoManager.StartBatch(forceDeleteOrIconify ? 'delete selected docs' : 'iconifying');
         } else {
+            // eslint-disable-next-line no-param-reassign
             forceDeleteOrIconify = false; // can't force immediate close in the middle of iconifying -- have to wait until iconifying completes
         }
 
@@ -268,7 +276,7 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
                 // open same document in new tab
                 CollectionDockingView.ToggleSplit(selectedDocs[0].Document, OpenWhereMod.right);
             } else {
-                var openDoc = selectedDocs[0].Document;
+                let openDoc = selectedDocs[0].Document;
                 if (openDoc.layout_fieldKey === 'layout_icon') {
                     openDoc = Doc.GetEmbeddings(openDoc).find(embedding => !embedding.embedContainer) ?? Doc.MakeEmbedding(openDoc);
                     Doc.deiconifyView(openDoc);
@@ -294,7 +302,7 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
      */
     @action
     onRadiusDown = (e: React.PointerEvent): void => {
-        SnappingManager.SetIsResizing(SelectionManager.Docs.lastElement());
+        SnappingManager.SetIsResizing(SelectionManager.Docs.lastElement()?.[Id]);
         this._isRounding = true;
         this._resizeUndo = UndoManager.StartBatch('DocDecs set radius');
         setupMoveUpEvents(
@@ -304,14 +312,14 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
                 const [x, y] = [this.Bounds.x + 3, this.Bounds.y + 3];
                 const maxDist = Math.min((this.Bounds.r - this.Bounds.x) / 2, (this.Bounds.b - this.Bounds.y) / 2);
                 const dist = e.clientX < x && e.clientY < y ? 0 : Math.sqrt((e.clientX - x) * (e.clientX - x) + (e.clientY - y) * (e.clientY - y));
-                SelectionManager.Docs.map(doc => {
+                SelectionManager.Docs.forEach(doc => {
                     const docMax = Math.min(NumCast(doc.width) / 2, NumCast(doc.height) / 2);
                     const radius = Math.min(1, dist / maxDist) * docMax; // set radius based on ratio of drag distance to half diagonal distance of bounding box
                     doc._layout_borderRounding = `${radius}px`;
                 });
                 return false;
             },
-            action(e => {
+            action(() => {
                 SnappingManager.SetIsResizing(undefined);
                 this._isRounding = false;
                 this._resizeUndo?.end();
@@ -329,17 +337,18 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
             e,
             returnFalse, // don't care about move or up event,
             emptyFunction, // just care about whether we get a click event
-            e => UndoManager.RunInBatch(() => SelectionManager.Docs.forEach(doc => Doc.toggleLockedPosition(doc)), 'toggleBackground')
+            () => UndoManager.RunInBatch(() => SelectionManager.Docs.forEach(doc => Doc.toggleLockedPosition(doc)), 'toggleBackground')
         );
         e.stopPropagation();
     };
 
     setRotateCenter = (seldocview: DocumentView, rotCenter: number[]) => {
+        const selDoc = seldocview.Document;
         const newloccentern = seldocview.screenToContentsTransform().transformPoint(rotCenter[0], rotCenter[1]);
         const newlocenter = [newloccentern[0] - NumCast(seldocview.layoutDoc._width) / 2, newloccentern[1] - NumCast(seldocview.layoutDoc._height) / 2];
         const final = Utils.rotPt(newlocenter[0], newlocenter[1], -(NumCast(seldocview.Document._rotation) / 180) * Math.PI);
-        seldocview.Document._rotation_centerX = final.x / NumCast(seldocview.layoutDoc._width);
-        seldocview.Document._rotation_centerY = final.y / NumCast(seldocview.layoutDoc._height);
+        selDoc._rotation_centerX = final.x / NumCast(seldocview.layoutDoc._width);
+        selDoc._rotation_centerY = final.y / NumCast(seldocview.layoutDoc._height);
     };
 
     @action
@@ -351,8 +360,8 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
             e,
             (e: PointerEvent, down: number[], delta: number[]) => // return false to keep getting events
                 this.setRotateCenter(seldocview, [this.rotCenter[0] + delta[0], this.rotCenter[1] + delta[1]]) as any as boolean, 
-            action(e => (this._isRotating = false)), // upEvent
-            action(e => (seldocview.Document._rotation_centerX = seldocview.Document._rotation_centerY = 0)),
+            action(() => { this._isRotating = false; }), // upEvent
+            action(() => { seldocview.Document._rotation_centerX = seldocview.Document._rotation_centerY = 0; }),
             true
         ); // prettier-ignore
         e.stopPropagation();
@@ -418,13 +427,15 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
                 this._isRotating = false;
                 rotateUndo?.end();
             }), // upEvent
-            action(e => (this._showRotCenter = !this._showRotCenter)) // clickEvent
+            action(() => {
+                this._showRotCenter = !this._showRotCenter;
+            }) // clickEvent
         );
     };
 
     @action
     onPointerDown = (e: React.PointerEvent): void => {
-        SnappingManager.SetIsResizing(SelectionManager.Docs.lastElement()); // turns off pointer events on things like youtube videos and web pages so that dragging doesn't get "stuck" when cursor moves over them
+        SnappingManager.SetIsResizing(SelectionManager.Docs.lastElement()?.[Id]); // turns off pointer events on things like youtube videos and web pages so that dragging doesn't get "stuck" when cursor moves over them
         setupMoveUpEvents(this, e, this.onPointerMove, this.onPointerUp, emptyFunction);
         e.stopPropagation();
         const id = (this._resizeHdlId = e.currentTarget.className);
@@ -453,12 +464,12 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
         const tl = docView.screenToContentsTransform().inverse().transformPoint(0, 0);
         return project([e.clientX + this._offset.x, e.clientY + this._offset.y], tl, [tl[0] + fixedAspect, tl[1] + 1]);
     };
-    onPointerMove = (e: PointerEvent, down: number[], move: number[]): boolean => {
+    onPointerMove = (e: PointerEvent): boolean => {
         const first = SelectionManager.Views[0];
         const effectiveAcl = GetEffectiveAcl(first.Document);
-        if (!(effectiveAcl == AclAdmin || effectiveAcl == AclEdit || effectiveAcl == AclAugment)) return false;
+        if (!(effectiveAcl === AclAdmin || effectiveAcl === AclEdit || effectiveAcl === AclAugment)) return false;
         if (!first) return false;
-        var fixedAspect = Doc.NativeAspect(first.layoutDoc);
+        const fixedAspect = Doc.NativeAspect(first.layoutDoc);
         const dragHdl = this._resizeHdlId.split(' ')[0].replace('documentDecorations-', '').replace('Resizer', '');
 
         const thisPt = // do snapping of drag point
@@ -476,7 +487,7 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
             const scaleAspect = {x:scale.x === 1 && fixedAspect ? scale.y : scale.x, y: scale.x !== 1 && fixedAspect ? scale.x : scale.y};
             SelectionManager.Views.forEach(docView => 
                 this.resizeView(docView, refPt, scaleAspect, { dragHdl, ctrlKey:e.ctrlKey })); // prettier-ignore
-            await new Promise<any>(res => setTimeout(() => res(this._interactionLock = undefined)));
+            await new Promise<any>(res => { setTimeout(() => { res(this._interactionLock = undefined)})});
         }); // prettier-ignore
 
         return false;
@@ -553,10 +564,11 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
 
             doc._layout_modificationDate = new DateField();
             if (scale.y !== 1) {
-                docView.layoutDoc._layout_autoHeight = undefined;
+                const docLayout = docView.layoutDoc;
+                docLayout._layout_autoHeight = undefined;
                 if (docView.layoutDoc._layout_autoHeight) {
                     // if autoHeight is still on because of a prototype
-                    docView.layoutDoc._layout_autoHeight = false; // then don't inherit, but explicitly set it to false
+                    docLayout._layout_autoHeight = false; // then don't inherit, but explicitly set it to false
                 }
             }
         }
@@ -586,15 +598,17 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
     };
 
     @action
-    onPointerUp = (e: PointerEvent): void => {
+    onPointerUp = (): void => {
         SnappingManager.SetIsResizing(undefined);
         SnappingManager.clearSnapLines();
         this._resizeHdlId = '';
         this._resizeUndo?.end();
 
         // detect layout_autoHeight gesture and apply
-        SelectionManager.Views.forEach(view => NumCast(view.Document._height) < 20 && (view.layoutDoc._layout_autoHeight = true));
-        //need to change points for resize, or else rotation/control points will fail.
+        SelectionManager.Views.forEach(view => {
+            NumCast(view.Document._height) < 20 && (view.layoutDoc._layout_autoHeight = true);
+        });
+        // need to change points for resize, or else rotation/control points will fail.
         this._inkDragDocs
             .map(oldbds => ({ oldbds, inkPts: Cast(oldbds.doc.data, InkField)?.inkData || [] }))
             .forEach(({ oldbds: { doc, x, y, width, height }, inkPts }) => {
@@ -613,7 +627,7 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
         if (SelectionManager.Views.length === 1) {
             const selected = SelectionManager.Views[0];
             if (this._titleControlString.startsWith('$')) {
-                return Field.toJavascriptString(selected.Document[this._titleControlString.substring(1)] as Field) || '-unset-';
+                return Field.toJavascriptString(selected.Document[this._titleControlString.substring(1)] as FieldType) || '-unset-';
             }
             return this._accumulatedTitle;
         }
@@ -648,7 +662,7 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
         const acl = GetEffectiveAcl(!this._showLayoutAcl ? Doc.GetProto(seldocview.Document) : seldocview.Document);
         const docShareMode = HierarchyMapping.get(acl)!.name;
         const shareMode = StrCast(docShareMode);
-        var shareSymbolIcon = ReverseHierarchyMap.get(shareMode)?.image;
+        const shareSymbolIcon = ReverseHierarchyMap.get(shareMode)?.image;
 
         // hide the decorations if the parent chooses to hide it or if the document itself hides it
         const hideDecorations = SnappingManager.IsResizing || seldocview._props.hideDecorations || seldocview.Document.layout_hideDecorations;
@@ -687,7 +701,7 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
         const bounds = this.ClippedBounds;
         const useLock = bounds.r - bounds.x > 135;
         const useRotation = !hideResizers && seldocview.Document.type !== DocumentType.EQUATION && seldocview.CollectionFreeFormDocumentView; // when do we want an object to not rotate?
-        const rotation = SelectionManager.Views.length == 1 ? seldocview.screenToContentsTransform().inverse().RotateDeg : 0;
+        const rotation = SelectionManager.Views.length === 1 ? seldocview.screenToContentsTransform().inverse().RotateDeg : 0;
 
         // Radius constants
         const useRounding = seldocview.ComponentView instanceof ImageBox || seldocview.ComponentView instanceof FormattedTextBox || seldocview.ComponentView instanceof CollectionFreeFormView;
@@ -727,11 +741,13 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
                     name="dynbox"
                     autoComplete="on"
                     value={hideTitle ? '' : this._accumulatedTitle}
-                    onBlur={action((e: React.FocusEvent) => {
+                    onBlur={action(() => {
                         this._editingTitle = false;
                         this.titleBlur();
                     })}
-                    onChange={action(e => !hideTitle && (this._accumulatedTitle = e.target.value))}
+                    onChange={action(e => {
+                        !hideTitle && (this._accumulatedTitle = e.target.value);
+                    })}
                     onKeyDown={hideTitle ? emptyFunction : this.titleEntered}
                     onPointerDown={e => e.stopPropagation()}
                 />
@@ -789,8 +805,8 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
                                     display: hideDeleteButton && hideTitle && hideOpenButton ? 'none' : undefined,
                                 }}
                                 onPointerDown={this.onContainerDown}>
-                                {hideDeleteButton ? null : topBtn('close', 'times', undefined, e => this.onCloseClick(true), 'Close')}
-                                {hideResizers || hideDeleteButton ? null : topBtn('minimize', 'window-maximize', undefined, e => this.onCloseClick(undefined), 'Minimize')}
+                                {hideDeleteButton ? null : topBtn('close', 'times', undefined, () => this.onCloseClick(true), 'Close')}
+                                {hideResizers || hideDeleteButton ? null : topBtn('minimize', 'window-maximize', undefined, () => this.onCloseClick(undefined), 'Minimize')}
                                 {titleArea}
                                 {hideOpenButton ? <div /> : topBtn('open', 'external-link-alt', this.onMaximizeDown, undefined, 'Open in Lightbox (ctrl: as alias, shift: in new collection)')}
                             </div>
diff --git a/src/client/views/FilterPanel.tsx b/src/client/views/FilterPanel.tsx
index 0521c4a4b..d3cebd1c3 100644
--- a/src/client/views/FilterPanel.tsx
+++ b/src/client/views/FilterPanel.tsx
@@ -1,10 +1,12 @@
+/* eslint-disable jsx-a11y/no-static-element-interactions */
+/* eslint-disable jsx-a11y/click-events-have-key-events */
 import { action, computed, makeObservable, observable, ObservableMap } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
 import { Handles, Rail, Slider, Ticks, Tracks } from 'react-compound-slider';
 import { AiOutlineMinusSquare, AiOutlinePlusSquare } from 'react-icons/ai';
 import { CiCircleRemove } from 'react-icons/ci';
-import { Doc, DocListCast, Field, LinkedTo, StrListCast } from '../../fields/Doc';
+import { Doc, DocListCast, Field, FieldType, LinkedTo, StrListCast } from '../../fields/Doc';
 import { Id } from '../../fields/FieldSymbols';
 import { List } from '../../fields/List';
 import { RichTextField } from '../../fields/RichTextField';
@@ -58,7 +60,7 @@ export class FilterPanel extends ObservableReactComponent<filterProps> {
 
     @computed get mapActiveFiltersToFacets() {
         const filters = new Map<string, string>();
-        //this.targetDoc.docFilters
+        // this.targetDoc.docFilters
         this.activeFilters.map(filter => filters.set(filter.split(Doc.FilterSep)[1], filter.split(Doc.FilterSep)[0]));
         return filters;
     }
@@ -73,7 +75,7 @@ export class FilterPanel extends ObservableReactComponent<filterProps> {
     // ["#tags", "width", "height"]
     //
     @computed get activeFacetHeaders() {
-        const activeHeaders = new Array();
+        const activeHeaders = [] as string[];
         this.activeFilters.map(filter => activeHeaders.push(filter.split(Doc.FilterSep)[0]));
 
         return activeHeaders;
@@ -83,19 +85,20 @@ export class FilterPanel extends ObservableReactComponent<filterProps> {
         const valueSet = new Set<string>(childFilters.map(filter => filter.split(Doc.FilterSep)[1]));
         let rtFields = 0;
         let subDocs = childDocs;
-        let gatheredDocs = [] as Doc[];
+        const gatheredDocs = [] as Doc[];
         if (subDocs.length > 0) {
             let newarray: Doc[] = [];
             while (subDocs.length > 0) {
                 newarray = [];
+                // eslint-disable-next-line no-loop-func
                 subDocs.forEach(t => {
                     gatheredDocs.push(t);
                     const facetVal = t[facetKey];
                     if (facetVal instanceof RichTextField || typeof facetVal === 'string') rtFields++;
-                    facetVal !== undefined && valueSet.add(Field.toString(facetVal as Field));
-                    (facetVal === true || facetVal == false) && valueSet.add(Field.toString(!facetVal));
+                    facetVal !== undefined && valueSet.add(Field.toString(facetVal as FieldType));
+                    (facetVal === true || facetVal === false) && valueSet.add(Field.toString(!facetVal));
                     const fieldKey = Doc.LayoutFieldKey(t);
-                    const annos = !Field.toString(Doc.LayoutField(t) as Field).includes('CollectionView');
+                    const annos = !Field.toString(Doc.LayoutField(t) as FieldType).includes('CollectionView');
                     DocListCast(t[annos ? fieldKey + '_annotations' : fieldKey]).forEach(newdoc => newarray.push(newdoc));
                     annos && DocListCast(t[fieldKey + '_sidebar']).forEach(newdoc => newarray.push(newdoc));
                 });
@@ -115,7 +118,7 @@ export class FilterPanel extends ObservableReactComponent<filterProps> {
 
     // @observable _chosenFacets = new ObservableMap<string, 'text' | 'checkbox' | 'slider' | 'range'>();
     @observable _chosenFacetsCollapse = new ObservableMap<string, boolean>();
-    @observable _collapseReturnKeys = new Array();
+    @observable _collapseReturnKeys = [] as string[];
 
     // this computed function gets the active filters and maps them to their headers
     //
@@ -130,11 +133,11 @@ export class FilterPanel extends ObservableReactComponent<filterProps> {
                 const facetValues = FilterPanel.gatherFieldValues(this.targetDocChildren, facetHeader, StrListCast(this.Document.childFilters));
 
                 let nonNumbers = 0;
-                let minVal = Number.MAX_VALUE,
-                    maxVal = -Number.MAX_VALUE;
-                facetValues.strings.map(val => {
+                let minVal = Number.MAX_VALUE;
+                let maxVal = -Number.MAX_VALUE;
+                facetValues.strings.forEach(val => {
                     const num = val ? Number(val) : Number.NaN;
-                    if (Number.isNaN(num)) {
+                    if (isNaN(num)) {
                         val && nonNumbers++;
                     } else {
                         minVal = Math.min(num, minVal);
@@ -144,14 +147,14 @@ export class FilterPanel extends ObservableReactComponent<filterProps> {
 
                 if (facetHeader === 'text') {
                     return { facetHeader, renderType: 'text' };
-                } else if (facetHeader !== 'tags' && nonNumbers / facetValues.strings.length < 0.1) {
+                }
+                if (facetHeader !== 'tags' && nonNumbers / facetValues.strings.length < 0.1) {
                     const extendedMinVal = minVal - Math.min(1, Math.floor(Math.abs(maxVal - minVal) * 0.1));
                     const extendedMaxVal = Math.max(minVal + 1, maxVal + Math.min(1, Math.ceil(Math.abs(maxVal - minVal) * 0.05)));
-                    const ranged = Doc.readDocRangeFilter(this.Document, facetHeader); // not the filter range, but the zooomed in range on the filter
-                    return { facetHeader, renderType: 'range', domain: [extendedMinVal, extendedMaxVal], range: ranged ? ranged : [extendedMinVal, extendedMaxVal] };
-                } else {
-                    return { facetHeader, renderType: 'checkbox' };
+                    const ranged: number[] | undefined = Doc.readDocRangeFilter(this.Document, facetHeader); // not the filter range, but the zooomed in range on the filter
+                    return { facetHeader, renderType: 'range', domain: [extendedMinVal, extendedMaxVal], range: ranged || [extendedMinVal, extendedMaxVal] };
                 }
+                return { facetHeader, renderType: 'checkbox' };
             })
         );
     }
@@ -171,17 +174,17 @@ export class FilterPanel extends ObservableReactComponent<filterProps> {
     sortingCurrentFacetValues = (facetHeader: string) => {
         this._collapseReturnKeys.splice(0);
 
-        Array.from(this.activeRenderedFacetInfos.keys()).map(renderInfo => {
+        Array.from(this.activeRenderedFacetInfos.keys()).forEach(renderInfo => {
             if (renderInfo.renderType === 'range' && renderInfo.facetHeader === facetHeader && renderInfo.range) {
-                this._collapseReturnKeys.push(renderInfo.range.map(number => number.toFixed(2)));
+                this._collapseReturnKeys.push(...renderInfo.range.map(number => number.toFixed(2)));
             }
         });
 
-        for (var key of this.facetValues(facetHeader)) {
+        this.facetValues(facetHeader).forEach(key => {
             if (this.mapActiveFiltersToFacets.get(key)) {
                 this._collapseReturnKeys.push(key);
             }
-        }
+        });
 
         return <div className=" filterbox-collpasedAndActive">{this._collapseReturnKeys.join(',  ')}</div>;
     };
@@ -198,7 +201,7 @@ export class FilterPanel extends ObservableReactComponent<filterProps> {
             );
         else
             allCollectionDocs.forEach(child => {
-                const fieldVal = child[facetHeader] as Field;
+                const fieldVal = child[facetHeader] as FieldType;
                 if (!(fieldVal instanceof List)) {
                     // currently we have no good way of filtering based on a field that is a list
                     set.add(Field.toString(fieldVal));
@@ -209,7 +212,7 @@ export class FilterPanel extends ObservableReactComponent<filterProps> {
 
         let nonNumbers = 0;
 
-        facetValues.map(val => Number.isNaN(Number(val)) && nonNumbers++);
+        facetValues.map(val => isNaN(Number(val)) && nonNumbers++);
         return nonNumbers / facetValues.length > 0.1 ? facetValues.sort() : facetValues.sort((n1: string, n2: string) => Number(n1) - Number(n2));
     };
 
@@ -218,7 +221,7 @@ export class FilterPanel extends ObservableReactComponent<filterProps> {
             <div className="filterBox-treeView">
                 <div className="filterBox-select">
                     <div style={{ width: '100%' }}>
-                        <FieldsDropdown Document={this.Document} selectFunc={this.facetClick} showPlaceholder={true} placeholder="add a filter" addedFields={['acl-Guest', LinkedTo]} />
+                        <FieldsDropdown Document={this.Document} selectFunc={this.facetClick} showPlaceholder placeholder="add a filter" addedFields={['acl-Guest', LinkedTo]} />
                     </div>
                     {/* THE FOLLOWING CODE SHOULD BE DEVELOPER FOR BOOLEAN EXPRESSION (AND / OR) */}
                     {/* <div className="filterBox-select-bool"> 
@@ -234,34 +237,31 @@ export class FilterPanel extends ObservableReactComponent<filterProps> {
 
                 <div className="filterBox-tree" key="tree">
                     {Array.from(this.activeRenderedFacetInfos.keys()).map(
-                        (
-                            renderInfo // iterato over activeFacetRenderInfos  ==> renderInfo  which you can renderInfo.facetHeader
-                        ) => (
+                        // iterate over activeFacetRenderInfos  ==> renderInfo  which you can renderInfo.facetHeader
+                        renderInfo => (
                             <div>
                                 <div className="filterBox-facetHeader">
                                     <div className="filterBox-facetHeader-Header"> </div>
                                     {renderInfo.facetHeader.charAt(0).toUpperCase() + renderInfo.facetHeader.slice(1)}
-
                                     <div
                                         className="filterBox-facetHeader-collapse"
-                                        onClick={action(e => {
+                                        onClick={action(() => {
                                             const collapseBoolValue = this._chosenFacetsCollapse.get(renderInfo.facetHeader);
                                             this._chosenFacetsCollapse.set(renderInfo.facetHeader, !collapseBoolValue);
                                         })}>
                                         {this._chosenFacetsCollapse.get(renderInfo.facetHeader) ? <AiOutlinePlusSquare /> : <AiOutlineMinusSquare />}
                                     </div>
-
                                     <div
                                         className="filterBox-facetHeader-remove"
-                                        onClick={action(e => {
+                                        onClick={action(() => {
                                             if (renderInfo.facetHeader === 'text') {
                                                 Doc.setDocFilter(this.Document, renderInfo.facetHeader, 'match', 'remove');
                                             } else {
-                                                for (var key of this.facetValues(renderInfo.facetHeader)) {
+                                                this.facetValues(renderInfo.facetHeader).forEach((key: string) => {
                                                     if (this.mapActiveFiltersToFacets.get(key)) {
                                                         Doc.setDocFilter(this.Document, renderInfo.facetHeader, key, 'remove');
                                                     }
-                                                }
+                                                });
                                             }
                                             this._selectedFacetHeaders.delete(renderInfo.facetHeader);
                                             this._chosenFacetsCollapse.delete(renderInfo.facetHeader);
@@ -292,7 +292,7 @@ export class FilterPanel extends ObservableReactComponent<filterProps> {
                 return (
                     <input
                         key={this.Document[Id]}
-                        placeholder={'enter text to match'}
+                        placeholder="enter text to match"
                         defaultValue={
                             StrListCast(this.Document._childFilters)
                                 .find(filter => filter.split(Doc.FilterSep)[0] === facetHeader)
@@ -312,7 +312,7 @@ export class FilterPanel extends ObservableReactComponent<filterProps> {
                                 style={{ width: 20, marginLeft: 20 }}
                                 checked={['check', 'exists'].includes(
                                     StrListCast(this.Document._childFilters)
-                                        .find(filter => filter.split(Doc.FilterSep)[0] === facetHeader && filter.split(Doc.FilterSep)[1] == facetValue)
+                                        .find(filter => filter.split(Doc.FilterSep)[0] === facetHeader && filter.split(Doc.FilterSep)[1] === facetValue)
                                         ?.split(Doc.FilterSep)[2] ?? ''
                                 )}
                                 type={type}
@@ -324,55 +324,56 @@ export class FilterPanel extends ObservableReactComponent<filterProps> {
                 });
 
             case 'range':
-                const domain = renderInfoDomain;
-                const range = renderInfoRange;
-                if (domain) {
-                    return (
-                        <div className="sliderBox-outerDiv" style={{ width: '95%', height: 45, float: 'right' }}>
-                            <Slider
-                                mode={2}
-                                step={Math.min(1, 0.1 * (domain[1] - domain[0]))}
-                                domain={[domain[0], domain[1]]} // -1000, 1000
-                                rootStyle={{ position: 'relative', width: '100%' }}
-                                onChange={values => Doc.setDocRangeFilter(this.Document, facetHeader, values)}
-                                values={renderInfoRange!}>
-                                <Rail>{railProps => <TooltipRail {...railProps} />}</Rail>
-                                <Handles>
-                                    {({ handles, activeHandleID, getHandleProps }) => (
-                                        <div className="slider-handles">
-                                            {handles.map((handle, i) => {
-                                                // const value = i === 0 ? defaultValues[0] : defaultValues[1];
-                                                return (
+                {
+                    const domain = renderInfoDomain;
+                    if (domain) {
+                        return (
+                            <div className="sliderBox-outerDiv" style={{ width: '95%', height: 45, float: 'right' }}>
+                                <Slider
+                                    mode={2}
+                                    step={Math.min(1, 0.1 * (domain[1] - domain[0]))}
+                                    domain={[domain[0], domain[1]]} // -1000, 1000
+                                    rootStyle={{ position: 'relative', width: '100%' }}
+                                    onChange={values => Doc.setDocRangeFilter(this.Document, facetHeader, values)}
+                                    values={renderInfoRange!}>
+                                    <Rail>{railProps => <TooltipRail {...railProps} />}</Rail>
+                                    <Handles>
+                                        {({ handles, activeHandleID, getHandleProps }) => (
+                                            <div className="slider-handles">
+                                                {handles.map(handle => (
+                                                    // const value = i === 0 ? defaultValues[0] : defaultValues[1];
                                                     <div>
                                                         <Handle key={handle.id} handle={handle} domain={domain} isActive={handle.id === activeHandleID} getHandleProps={getHandleProps} />
                                                     </div>
-                                                );
-                                            })}
-                                        </div>
-                                    )}
-                                </Handles>
-                                <Tracks left={false} right={false}>
-                                    {({ tracks, getTrackProps }) => (
-                                        <div className="slider-tracks">
-                                            {tracks.map(({ id, source, target }) => (
-                                                <Track key={id} source={source} target={target} disabled={false} getTrackProps={getTrackProps} />
-                                            ))}
-                                        </div>
-                                    )}
-                                </Tracks>
-                                <Ticks count={5}>
-                                    {({ ticks }) => (
-                                        <div className="slider-ticks">
-                                            {ticks.map(tick => (
-                                                <Tick key={tick.id} tick={tick} count={ticks.length} format={(val: number) => val.toString()} />
-                                            ))}
-                                        </div>
-                                    )}
-                                </Ticks>
-                            </Slider>
-                        </div>
-                    );
+                                                ))}
+                                            </div>
+                                        )}
+                                    </Handles>
+                                    <Tracks left={false} right={false}>
+                                        {({ tracks, getTrackProps }) => (
+                                            <div className="slider-tracks">
+                                                {tracks.map(({ id, source, target }) => (
+                                                    <Track key={id} source={source} target={target} disabled={false} getTrackProps={getTrackProps} />
+                                                ))}
+                                            </div>
+                                        )}
+                                    </Tracks>
+                                    <Ticks count={5}>
+                                        {({ ticks }) => (
+                                            <div className="slider-ticks">
+                                                {ticks.map(tick => (
+                                                    <Tick key={tick.id} tick={tick} count={ticks.length} format={(val: number) => val.toString()} />
+                                                ))}
+                                            </div>
+                                        )}
+                                    </Ticks>
+                                </Slider>
+                            </div>
+                        );
+                    }
                 }
+                break;
+            default:
 
             // case 'range'
             // return <Slider ...
@@ -386,5 +387,6 @@ export class FilterPanel extends ObservableReactComponent<filterProps> {
             //     <dimain changing handles >
             //  <?div
         }
+        return undefined;
     }
 }
diff --git a/src/client/views/GestureOverlay.tsx b/src/client/views/GestureOverlay.tsx
index 86b9f5e40..46fa04d72 100644
--- a/src/client/views/GestureOverlay.tsx
+++ b/src/client/views/GestureOverlay.tsx
@@ -2,11 +2,13 @@ import * as fitCurve from 'fit-curve';
 import { action, computed, makeObservable, observable, runInAction } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
-import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnEmptyString, returnFalse, setupMoveUpEvents } from '../../Utils';
+import { returnEmptyDoclist, returnEmptyFilter, returnEmptyString, returnFalse, setupMoveUpEvents } from '../../ClientUtils';
+import { emptyFunction } from '../../Utils';
 import { Doc, Opt } from '../../fields/Doc';
 import { InkData, InkTool } from '../../fields/InkField';
 import { BoolCast, NumCast } from '../../fields/Types';
 import MobileInkOverlay from '../../mobile/MobileInkOverlay';
+import { Gestures } from '../../pen-gestures/GestureTypes';
 import { GestureUtils } from '../../pen-gestures/GestureUtils';
 import { MobileInkOverlayContent } from '../../server/Message';
 import { InteractionUtils } from '../util/InteractionUtils';
@@ -29,15 +31,22 @@ import {
     SetActiveInkWidth,
 } from './InkingStroke';
 import { ObservableReactComponent } from './ObservableReactComponent';
-import { checkInksToGroup } from './global/globalScripts';
 import { DocumentView } from './nodes/DocumentView';
 
+export enum ToolglassTools {
+    InkToText = 'inktotext',
+    IgnoreGesture = 'ignoregesture',
+    RadialMenu = 'radialmenu',
+    None = 'none',
+}
 interface GestureOverlayProps {
     isActive: boolean;
 }
 @observer
 export class GestureOverlay extends ObservableReactComponent<React.PropsWithChildren<GestureOverlayProps>> {
+    // eslint-disable-next-line no-use-before-define
     static Instance: GestureOverlay;
+    // eslint-disable-next-line no-use-before-define
     static Instances: GestureOverlay[] = [];
 
     public static set RecognizeGestures(active) {
@@ -47,7 +56,7 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
         return BoolCast(Doc.UserDoc().recognizeGestures);
     }
 
-    @observable public InkShape: Opt<GestureUtils.Gestures> = undefined;
+    @observable public InkShape: Opt<Gestures> = undefined;
     @observable public SavedColor?: string = undefined;
     @observable public SavedWidth?: number = undefined;
     @observable public Tool: ToolglassTools = ToolglassTools.None;
@@ -55,9 +64,6 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
 
     @observable private _thumbX?: number = undefined;
     @observable private _thumbY?: number = undefined;
-    @observable private _selectedIndex: number = -1;
-    @observable private _menuX: number = -300;
-    @observable private _menuY: number = -300;
     @observable private _pointerY?: number = undefined;
     @observable private _points: { X: number; Y: number }[] = [];
     @observable private _strokes: InkData[] = [];
@@ -117,9 +123,9 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
             const yInGlass = initialPoint.Y > (this._thumbY ?? Number.MAX_SAFE_INTEGER) - this.height && initialPoint.Y < (this._thumbY ?? Number.MAX_SAFE_INTEGER);
             if (this.Tool !== ToolglassTools.None && xInGlass && yInGlass) {
                 switch (this.Tool) {
-                    case ToolglassTools.RadialMenu:
-                        return true;
-                }
+                    case ToolglassTools.RadialMenu: return true;
+                    default: 
+                } // prettier-ignore
             }
         }
         return false;
@@ -128,8 +134,8 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
     @action primCreated() {
         if (!this.KeepPrimitiveMode) {
             this.InkShape = undefined;
-            //get out of ink mode after each stroke=
-            //if (Doc.ActiveTool === InkTool.Highlighter && GestureOverlay.Instance.SavedColor) SetActiveInkColor(GestureOverlay.Instance.SavedColor);
+            // get out of ink mode after each stroke=
+            // if (Doc.ActiveTool === InkTool.Highlighter && GestureOverlay.Instance.SavedColor) SetActiveInkColor(GestureOverlay.Instance.SavedColor);
             Doc.ActiveTool = InkTool.None;
             // SetActiveArrowStart('none');
             // SetActiveArrowEnd('none');
@@ -142,7 +148,7 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
             const B = this.svgBounds;
             const points = this._points.map(p => ({ X: p.X - B.left, Y: p.Y - B.top }));
 
-            //if any of the shape is activated in the CollectionFreeFormViewChrome
+            // if any of the shape is activated in the CollectionFreeFormViewChrome
             if (this.InkShape) {
                 this.makeBezierPolygon(this.InkShape, false);
                 this.dispatchGesture(this.InkShape);
@@ -155,16 +161,17 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
                 let actionPerformed = false;
                 if (GestureOverlay.RecognizeGestures && result && result.Score > 0.7) {
                     switch (result.Name) {
-                        case GestureUtils.Gestures.Line:
-                        case GestureUtils.Gestures.Triangle:
-                        case GestureUtils.Gestures.Rectangle:
-                        case GestureUtils.Gestures.Circle:
+                        case Gestures.Line:
+                        case Gestures.Triangle:
+                        case Gestures.Rectangle:
+                        case Gestures.Circle:
                             this.makeBezierPolygon(result.Name, true);
                             actionPerformed = this.dispatchGesture(result.Name);
                             break;
-                        case GestureUtils.Gestures.Scribble:
+                        case Gestures.Scribble:
                             console.log('scribble');
                             break;
+                        default:
                     }
                 }
 
@@ -178,21 +185,20 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
                     const controlPoints: { X: number; Y: number }[] = [];
 
                     const bezierCurves = (fitCurve as any)(newPoints, 10);
-                    for (const curve of bezierCurves) {
+                    Array.from(bezierCurves).forEach((curve: any) => {
                         controlPoints.push({ X: curve[0][0], Y: curve[0][1] });
                         controlPoints.push({ X: curve[1][0], Y: curve[1][1] });
                         controlPoints.push({ X: curve[2][0], Y: curve[2][1] });
                         controlPoints.push({ X: curve[3][0], Y: curve[3][1] });
-                    }
+                    });
                     const dist = Math.sqrt(
                         (controlPoints[0].X - controlPoints.lastElement().X) * (controlPoints[0].X - controlPoints.lastElement().X) + (controlPoints[0].Y - controlPoints.lastElement().Y) * (controlPoints[0].Y - controlPoints.lastElement().Y)
                     );
+                    // eslint-disable-next-line prefer-destructuring
                     if (controlPoints.length > 4 && dist < 10) controlPoints[controlPoints.length - 1] = controlPoints[0];
                     this._points.length = 0;
                     this._points.push(...controlPoints);
-                    this.dispatchGesture(GestureUtils.Gestures.Stroke);
-                    // TODO: nda - check inks to group here
-                    checkInksToGroup();
+                    this.dispatchGesture(Gestures.Stroke);
                 }
             }
         }
@@ -202,34 +208,34 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
     makeBezierPolygon = (shape: string, gesture: boolean) => {
         const xs = this._points.map(p => p.X);
         const ys = this._points.map(p => p.Y);
-        var right = Math.max(...xs);
-        var left = Math.min(...xs);
-        var bottom = Math.max(...ys);
-        var top = Math.min(...ys);
+        let right = Math.max(...xs);
+        let left = Math.min(...xs);
+        let bottom = Math.max(...ys);
+        let top = Math.min(...ys);
         const firstx = this._points[0].X;
         const firsty = this._points[0].Y;
-        var lastx = this._points[this._points.length - 2].X;
-        var lasty = this._points[this._points.length - 2].Y;
-        var fourth = (lastx - firstx) / 4;
+        let lastx = this._points[this._points.length - 2].X;
+        let lasty = this._points[this._points.length - 2].Y;
+        let fourth = (lastx - firstx) / 4;
         if (isNaN(fourth) || fourth === 0) {
             fourth = 0.01;
         }
-        var m = (lasty - firsty) / (lastx - firstx);
+        let m = (lasty - firsty) / (lastx - firstx);
         if (isNaN(m) || m === 0) {
             m = 0.01;
         }
-        const b = firsty - m * firstx;
+        // const b = firsty - m * firstx;
         if (shape === 'noRec') {
             return false;
         }
         if (!gesture) {
-            //if shape options is activated in inkOptionMenu
-            //take second to last point because _point[length-1] is _points[0]
+            // if shape options is activated in inkOptionMenu
+            // take second to last point because _point[length-1] is _points[0]
             right = this._points[this._points.length - 2].X;
             left = this._points[0].X;
             bottom = this._points[this._points.length - 2].Y;
             top = this._points[0].Y;
-            if (shape !== GestureUtils.Gestures.Arrow && shape !== GestureUtils.Gestures.Line && shape !== GestureUtils.Gestures.Circle) {
+            if (shape !== Gestures.Arrow && shape !== Gestures.Line && shape !== Gestures.Circle) {
                 if (left > right) {
                     const temp = right;
                     right = left;
@@ -244,7 +250,7 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
         }
         this._points.length = 0;
         switch (shape) {
-            case GestureUtils.Gestures.Rectangle:
+            case Gestures.Rectangle:
                 this._points.push({ X: left, Y: top });
                 this._points.push({ X: left, Y: top });
                 this._points.push({ X: right, Y: top });
@@ -267,7 +273,7 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
 
                 break;
 
-            case GestureUtils.Gestures.Triangle:
+            case Gestures.Triangle:
                 this._points.push({ X: left, Y: bottom });
                 this._points.push({ X: left, Y: bottom });
 
@@ -285,39 +291,40 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
                 this._points.push({ X: left, Y: bottom });
 
                 break;
-            case GestureUtils.Gestures.Circle:
-                // Approximation of a circle using 4 Bézier curves in which the constant "c" reduces the maximum radial drift to 0.019608%,
-                // making the curves indistinguishable from a circle.
-                // Source: https://spencermortensen.com/articles/bezier-circle/
-                const c = 0.551915024494;
-                const centerX = (Math.max(left, right) + Math.min(left, right)) / 2;
-                const centerY = (Math.max(top, bottom) + Math.min(top, bottom)) / 2;
-                const radius = Math.max(centerX - Math.min(left, right), centerY - Math.min(top, bottom));
-
-                // Dividing the circle into four equal sections, and fitting each section to a cubic Bézier curve.
-                this._points.push({ X: centerX, Y: centerY + radius });
-                this._points.push({ X: centerX + c * radius, Y: centerY + radius });
-                this._points.push({ X: centerX + radius, Y: centerY + c * radius });
-                this._points.push({ X: centerX + radius, Y: centerY });
-
-                this._points.push({ X: centerX + radius, Y: centerY });
-                this._points.push({ X: centerX + radius, Y: centerY - c * radius });
-                this._points.push({ X: centerX + c * radius, Y: centerY - radius });
-                this._points.push({ X: centerX, Y: centerY - radius });
-
-                this._points.push({ X: centerX, Y: centerY - radius });
-                this._points.push({ X: centerX - c * radius, Y: centerY - radius });
-                this._points.push({ X: centerX - radius, Y: centerY - c * radius });
-                this._points.push({ X: centerX - radius, Y: centerY });
-
-                this._points.push({ X: centerX - radius, Y: centerY });
-                this._points.push({ X: centerX - radius, Y: centerY + c * radius });
-                this._points.push({ X: centerX - c * radius, Y: centerY + radius });
-                this._points.push({ X: centerX, Y: centerY + radius });
-
+            case Gestures.Circle:
+                {
+                    // Approximation of a circle using 4 Bézier curves in which the constant "c" reduces the maximum radial drift to 0.019608%,
+                    // making the curves indistinguishable from a circle.
+                    // Source: https://spencermortensen.com/articles/bezier-circle/
+                    const c = 0.551915024494;
+                    const centerX = (Math.max(left, right) + Math.min(left, right)) / 2;
+                    const centerY = (Math.max(top, bottom) + Math.min(top, bottom)) / 2;
+                    const radius = Math.max(centerX - Math.min(left, right), centerY - Math.min(top, bottom));
+
+                    // Dividing the circle into four equal sections, and fitting each section to a cubic Bézier curve.
+                    this._points.push({ X: centerX, Y: centerY + radius });
+                    this._points.push({ X: centerX + c * radius, Y: centerY + radius });
+                    this._points.push({ X: centerX + radius, Y: centerY + c * radius });
+                    this._points.push({ X: centerX + radius, Y: centerY });
+
+                    this._points.push({ X: centerX + radius, Y: centerY });
+                    this._points.push({ X: centerX + radius, Y: centerY - c * radius });
+                    this._points.push({ X: centerX + c * radius, Y: centerY - radius });
+                    this._points.push({ X: centerX, Y: centerY - radius });
+
+                    this._points.push({ X: centerX, Y: centerY - radius });
+                    this._points.push({ X: centerX - c * radius, Y: centerY - radius });
+                    this._points.push({ X: centerX - radius, Y: centerY - c * radius });
+                    this._points.push({ X: centerX - radius, Y: centerY });
+
+                    this._points.push({ X: centerX - radius, Y: centerY });
+                    this._points.push({ X: centerX - radius, Y: centerY + c * radius });
+                    this._points.push({ X: centerX - c * radius, Y: centerY + radius });
+                    this._points.push({ X: centerX, Y: centerY + radius });
+                }
                 break;
 
-            case GestureUtils.Gestures.Line:
+            case Gestures.Line:
                 if (Math.abs(firstx - lastx) < 10 && Math.abs(firsty - lasty) > 10) {
                     lastx = firstx;
                 }
@@ -330,28 +337,32 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
                 this._points.push({ X: lastx, Y: lasty });
                 this._points.push({ X: lastx, Y: lasty });
                 break;
-            case GestureUtils.Gestures.Arrow:
-                const x1 = left;
-                const y1 = top;
-                const x2 = right;
-                const y2 = bottom;
-                const L1 = Math.sqrt(Math.pow(Math.abs(x1 - x2), 2) + Math.pow(Math.abs(y1 - y2), 2));
-                const L2 = L1 / 5;
-                const angle = 0.785398;
-                const x3 = x2 + (L2 / L1) * ((x1 - x2) * Math.cos(angle) + (y1 - y2) * Math.sin(angle));
-                const y3 = y2 + (L2 / L1) * ((y1 - y2) * Math.cos(angle) - (x1 - x2) * Math.sin(angle));
-                const x4 = x2 + (L2 / L1) * ((x1 - x2) * Math.cos(angle) - (y1 - y2) * Math.sin(angle));
-                const y4 = y2 + (L2 / L1) * ((y1 - y2) * Math.cos(angle) + (x1 - x2) * Math.sin(angle));
-                this._points.push({ X: x1, Y: y1 });
-                this._points.push({ X: x2, Y: y2 });
-                this._points.push({ X: x3, Y: y3 });
-                this._points.push({ X: x4, Y: y4 });
-                this._points.push({ X: x2, Y: y2 });
+            case Gestures.Arrow:
+                {
+                    const x1 = left;
+                    const y1 = top;
+                    const x2 = right;
+                    const y2 = bottom;
+                    const L1 = Math.sqrt(Math.abs(x1 - x2) ** 2 + Math.abs(y1 - y2) ** 2);
+                    const L2 = L1 / 5;
+                    const angle = 0.785398;
+                    const x3 = x2 + (L2 / L1) * ((x1 - x2) * Math.cos(angle) + (y1 - y2) * Math.sin(angle));
+                    const y3 = y2 + (L2 / L1) * ((y1 - y2) * Math.cos(angle) - (x1 - x2) * Math.sin(angle));
+                    const x4 = x2 + (L2 / L1) * ((x1 - x2) * Math.cos(angle) - (y1 - y2) * Math.sin(angle));
+                    const y4 = y2 + (L2 / L1) * ((y1 - y2) * Math.cos(angle) + (x1 - x2) * Math.sin(angle));
+                    this._points.push({ X: x1, Y: y1 });
+                    this._points.push({ X: x2, Y: y2 });
+                    this._points.push({ X: x3, Y: y3 });
+                    this._points.push({ X: x4, Y: y4 });
+                    this._points.push({ X: x2, Y: y2 });
+                }
+                break;
+            default:
         }
         return false;
     };
 
-    dispatchGesture = (gesture: GestureUtils.Gestures, stroke?: InkData, text?: any) => {
+    dispatchGesture = (gesture: Gestures, stroke?: InkData, text?: any) => {
         const points = (stroke ?? this._points).slice();
         return (
             document.elementFromPoint(points[0].X, points[0].Y)?.dispatchEvent(
@@ -387,11 +398,11 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
         const selView = GestureOverlay.DownDocView;
         const width = Number(ActiveInkWidth()) * NumCast(selView?.Document._freeform_scale, 1); // * (selView?.screenToViewTransform().Scale || 1);
         const rect = this._overlayRef.current?.getBoundingClientRect();
-        const B = { left: -20000, right: 20000, top: -20000, bottom: 20000, width: 40000, height: 40000 }; //this.getBounds(this._points, true);
-        B.left = B.left - width / 2;
-        B.right = B.right + width / 2;
+        const B = { left: -20000, right: 20000, top: -20000, bottom: 20000, width: 40000, height: 40000 }; // this.getBounds(this._points, true);
+        B.left -= width / 2;
+        B.right += width / 2;
         B.top = B.top - width / 2 - (rect?.y || 0);
-        B.bottom = B.bottom + width / 2;
+        B.bottom += width / 2;
         B.width += width;
         B.height += width;
         const fillColor = ActiveFillColor();
@@ -401,7 +412,7 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
             this._palette,
             [
                 this._strokes.map((l, i) => {
-                    const b = { left: -20000, right: 20000, top: -20000, bottom: 20000, width: 40000, height: 40000 }; //this.getBounds(l, true);
+                    const b = { left: -20000, right: 20000, top: -20000, bottom: 20000, width: 40000, height: 40000 }; // this.getBounds(l, true);
                     return (
                         <svg key={i} width={b.width} height={b.height} style={{ top: 0, left: 0, transform: `translate(${b.left}px, ${b.top}px)`, pointerEvents: 'none', position: 'absolute', zIndex: 30000, overflow: 'visible' }}>
                             {InteractionUtils.CreatePolyline(
@@ -414,7 +425,7 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
                                 'miter',
                                 'round',
                                 ActiveInkBezierApprox(),
-                                'none' /*ActiveFillColor()*/,
+                                'none' /* ActiveFillColor() */,
                                 ActiveArrowStart(),
                                 ActiveArrowEnd(),
                                 ActiveArrowScale(),
@@ -441,7 +452,7 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
                             'miter',
                             'round',
                             '',
-                            'none' /*ActiveFillColor()*/,
+                            'none' /* ActiveFillColor() */,
                             ActiveArrowStart(),
                             ActiveArrowEnd(),
                             ActiveArrowScale(),
@@ -529,19 +540,14 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
     }
 }
 
-// export class
-
-export enum ToolglassTools {
-    InkToText = 'inktotext',
-    IgnoreGesture = 'ignoregesture',
-    RadialMenu = 'radialmenu',
-    None = 'none',
-}
-
 ScriptingGlobals.add('GestureOverlay', GestureOverlay);
+// eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function setToolglass(tool: any) {
-    runInAction(() => (GestureOverlay.Instance.Tool = tool));
+    runInAction(() => {
+        GestureOverlay.Instance.Tool = tool;
+    });
 });
+// eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function setPen(width: any, color: any, fill: any, arrowStart: any, arrowEnd: any, dash: any) {
     runInAction(() => {
         GestureOverlay.Instance.SavedColor = ActiveInkColor();
@@ -554,6 +560,7 @@ ScriptingGlobals.add(function setPen(width: any, color: any, fill: any, arrowSta
         SetActiveDash(dash);
     });
 });
+// eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function resetPen() {
     runInAction(() => {
         SetActiveInkColor(GestureOverlay.Instance.SavedColor ?? 'rgb(0, 0, 0)');
@@ -561,8 +568,9 @@ ScriptingGlobals.add(function resetPen() {
     });
 }, 'resets the pen tool');
 ScriptingGlobals.add(
+    // eslint-disable-next-line prefer-arrow-callback
     function createText(text: any, x: any, y: any) {
-        GestureOverlay.Instance.dispatchGesture(GestureUtils.Gestures.Text, [{ X: x, Y: y }], text);
+        GestureOverlay.Instance.dispatchGesture(Gestures.Text, [{ X: x, Y: y }], text);
     },
     'creates a text document with inputted text and coordinates',
     '(text: any, x: any, y: any)'
diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts
index 2f64ea28c..5c923d301 100644
--- a/src/client/views/GlobalKeyHandler.ts
+++ b/src/client/views/GlobalKeyHandler.ts
@@ -67,7 +67,6 @@ export class KeyManager {
         // accumulate buffer of characters to insert in a new text note.  once the note is created, it will stop keyboard events from reaching this function.
         if (FormattedTextBox.SelectOnLoadChar) FormattedTextBox.SelectOnLoadChar = FormattedTextBox.SelectOnLoadChar + (e.key === 'Enter' ? '\n' : e.key);
         e.key === 'Control' && (CtrlKey = true);
-        //if (!Doc.noviceMode && e.key.toLocaleLowerCase() === "shift") DocServer.UPDATE_SERVER_CACHE(true);
         const keyname = e.key && e.key.toLowerCase();
         this.handleGreedy(keyname);
 
@@ -89,10 +88,7 @@ export class KeyManager {
         control.preventDefault && e.preventDefault();
     });
 
-    private handleGreedy = action((keyname: string) => {
-        switch (keyname) {
-        }
-    });
+    private handleGreedy = action((/* keyname: string */) => {});
 
     nudge = (x: number, y: number, label: string) => {
         const nudgeable = SelectionManager.Views.some(dv => dv.CollectionFreeFormDocumentView?.nudge);
@@ -105,7 +101,7 @@ export class KeyManager {
             case 'u':
                 if (document.activeElement?.tagName !== 'INPUT' && document.activeElement?.tagName !== 'TEXTAREA') {
                     const ungroupings = SelectionManager.Views;
-                    UndoManager.RunInBatch(() => ungroupings.map(dv => (dv.layoutDoc.group = undefined)), 'ungroup');
+                    UndoManager.RunInBatch(() => ungroupings.forEach(dv => { dv.layoutDoc.group = undefined; }), 'ungroup');
                     SelectionManager.DeselectAll();
                 }
                 break;
@@ -124,41 +120,41 @@ export class KeyManager {
             case ' ':
                 // MarqueeView.DragMarquee = !MarqueeView.DragMarquee; // bcz: this needs a better disclosure UI
                 break;
-            case 'escape':
-                DocumentLinksButton.StartLink = undefined;
-                DocumentLinksButton.StartLinkView = undefined;
-                InkStrokeProperties.Instance._controlButton = false;
-                Doc.ActiveTool = InkTool.None;
-                DragManager.CompleteWindowDrag?.(true);
-                var doDeselect = true;
-                if (SnappingManager.IsDragging) {
-                    DragManager.AbortDrag();
-                } 
-                if (CollectionDockingView.Instance?.HasFullScreen) {
-                    CollectionDockingView.Instance?.CloseFullScreen();
-                } 
-                if (CollectionStackedTimeline.SelectingRegions.size) {
-                    CollectionStackedTimeline.StopSelecting();
-                    doDeselect = false;
-                } else {
-                    doDeselect = !ContextMenu.Instance.closeMenu();
-                }
-                if (doDeselect) {
-                    SelectionManager.DeselectAll();
-                    LightboxView.Instance.SetLightboxDoc(undefined);
+            case 'escape': {
+                    DocumentLinksButton.StartLink = undefined;
+                    DocumentLinksButton.StartLinkView = undefined;
+                    InkStrokeProperties.Instance._controlButton = false;
+                    Doc.ActiveTool = InkTool.None;
+                    DragManager.CompleteWindowDrag?.(true);
+                    let doDeselect = true;
+                    if (SnappingManager.IsDragging) {
+                        DragManager.AbortDrag();
+                    } 
+                    if (CollectionDockingView.Instance?.HasFullScreen) {
+                        CollectionDockingView.Instance?.CloseFullScreen();
+                    } 
+                    if (CollectionStackedTimeline.SelectingRegions.size) {
+                        CollectionStackedTimeline.StopSelecting();
+                        doDeselect = false;
+                    } else {
+                        doDeselect = !ContextMenu.Instance.closeMenu();
+                    }
+                    if (doDeselect) {
+                        SelectionManager.DeselectAll();
+                        LightboxView.Instance.SetLightboxDoc(undefined);
+                    }
+                    // DictationManager.Controls.stop();
+                    GoogleAuthenticationManager.Instance.cancel();
+                    SharingManager.Instance.close();
+                    if (!GroupManager.Instance.isOpen) SettingsManager.Instance.close();
+                    GroupManager.Instance.close();
+                    window.getSelection()?.empty();
+                    document.body.focus();
                 }
-                // DictationManager.Controls.stop();
-                GoogleAuthenticationManager.Instance.cancel();
-                SharingManager.Instance.close();
-                if (!GroupManager.Instance.isOpen) SettingsManager.Instance.close();
-                GroupManager.Instance.close();
-                window.getSelection()?.empty();
-                document.body.focus();
                 break;
-            case 'enter': {
-                //DocumentDecorations.Instance.onCloseClick(false);
+            case 'enter':
+                // DocumentDecorations.Instance.onCloseClick(false);
                 break;
-            }
             case 'delete':
             case 'backspace':
                 if (document.activeElement?.tagName !== 'INPUT' && document.activeElement?.tagName !== 'TEXTAREA') {
@@ -173,6 +169,7 @@ export class KeyManager {
             case 'arrowright': return this.nudge(1,0, 'nudge right');
             case 'arrowup':    return this.nudge(0, -1, 'nudge up');
             case 'arrowdown':  return this.nudge(0, 1, 'nudge down');
+            default:
         } // prettier-ignore
 
         return {
diff --git a/src/client/views/InkControlPtHandles.tsx b/src/client/views/InkControlPtHandles.tsx
index edc6b404b..0358344b9 100644
--- a/src/client/views/InkControlPtHandles.tsx
+++ b/src/client/views/InkControlPtHandles.tsx
@@ -6,7 +6,7 @@ import { ControlPoint, InkData, PointData } from '../../fields/InkField';
 import { List } from '../../fields/List';
 import { listSpec } from '../../fields/Schema';
 import { Cast } from '../../fields/Types';
-import { returnFalse, setupMoveUpEvents } from '../../Utils';
+import { returnFalse, setupMoveUpEvents } from '../../ClientUtils';
 import { SelectionManager } from '../util/SelectionManager';
 import { UndoManager } from '../util/UndoManager';
 import { Colors } from './global/globalEnums';
diff --git a/src/client/views/InkTangentHandles.tsx b/src/client/views/InkTangentHandles.tsx
index c20399698..f3a757f19 100644
--- a/src/client/views/InkTangentHandles.tsx
+++ b/src/client/views/InkTangentHandles.tsx
@@ -6,7 +6,8 @@ import { HandleLine, HandlePoint, InkData } from '../../fields/InkField';
 import { List } from '../../fields/List';
 import { listSpec } from '../../fields/Schema';
 import { Cast } from '../../fields/Types';
-import { emptyFunction, setupMoveUpEvents } from '../../Utils';
+import { emptyFunction } from '../../Utils';
+import { setupMoveUpEvents } from '../../ClientUtils';
 import { Transform } from '../util/Transform';
 import { UndoManager } from '../util/UndoManager';
 import { Colors } from './global/globalEnums';
diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx
index 51baaa23e..9f82fdb18 100644
--- a/src/client/views/InkingStroke.tsx
+++ b/src/client/views/InkingStroke.tsx
@@ -23,18 +23,18 @@
 import { action, computed, IReactionDisposer, observable, reaction } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
+import { DashColor, returnFalse, setupMoveUpEvents } from '../../ClientUtils';
 import { Doc } from '../../fields/Doc';
 import { InkData, InkField } from '../../fields/InkField';
 import { BoolCast, Cast, NumCast, RTFCast, StrCast } from '../../fields/Types';
 import { TraceMobx } from '../../fields/util';
-import { DashColor, returnFalse, setupMoveUpEvents } from '../../Utils';
 import { CognitiveServices } from '../cognitive_services/CognitiveServices';
 import { Docs } from '../documents/Documents';
 import { InteractionUtils } from '../util/InteractionUtils';
 import { SnappingManager } from '../util/SnappingManager';
 import { UndoManager } from '../util/UndoManager';
 import { ContextMenu } from './ContextMenu';
-import { ViewBoxAnnotatableComponent, ViewBoxBaseComponent, ViewBoxInterface } from './DocComponent';
+import { PinProps, ViewBoxAnnotatableComponent, ViewBoxInterface } from './DocComponent';
 import { Colors } from './global/globalEnums';
 import { InkControlPtHandles, InkEndPtHandles } from './InkControlPtHandles';
 import './InkStroke.scss';
@@ -42,8 +42,9 @@ import { InkStrokeProperties } from './InkStrokeProperties';
 import { InkTangentHandles } from './InkTangentHandles';
 import { FieldView, FieldViewProps } from './nodes/FieldView';
 import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox';
-import { PinProps, PresBox } from './nodes/trails';
+import { PresBox } from './nodes/trails';
 import { StyleProp } from './StyleProvider';
+
 const { INK_MASK_SIZE } = require('./global/globalCssVariables.module.scss'); // prettier-ignore
 @observer
 export class InkingStroke extends ViewBoxAnnotatableComponent<FieldViewProps>() implements ViewBoxInterface {
@@ -387,8 +388,7 @@ export class InkingStroke extends ViewBoxAnnotatableComponent<FieldViewProps>()
             1.0,
             false,
             undefined,
-            undefined,
-            color === 'transparent' ? highlightColor : undefined
+            undefined
         );
         const higlightMargin = Math.min(12, Math.max(2, 0.3 * inkStrokeWidth));
         // Invisible polygonal line that enables the ink to be selected by the user.
@@ -415,8 +415,7 @@ export class InkingStroke extends ViewBoxAnnotatableComponent<FieldViewProps>()
                 0.0,
                 false,
                 downHdlr,
-                mask,
-                highlightColor
+                mask
             );
         // bootsrap 3 style sheet sets line height to be 20px for default 14 point font size.
         // this attempts to figure out the lineHeight ratio by inquiring the body's lineHeight and dividing by the fontsize which should yield 1.428571429
@@ -438,7 +437,7 @@ export class InkingStroke extends ViewBoxAnnotatableComponent<FieldViewProps>()
                 <svg
                     className="inkStroke"
                     style={{
-                        transform: isInkMask ? `rotate(-${NumCast(this._props.CollectionFreeFormDocumentView?.()._props.rotation ?? 0)}deg) translate(${InkingStroke.MaskDim / 2}px, ${InkingStroke.MaskDim / 2}px)` : undefined,
+                        transform: isInkMask ? `rotate(-${NumCast(this._props.LocalRotation?.() ?? 0)}deg) translate(${InkingStroke.MaskDim / 2}px, ${InkingStroke.MaskDim / 2}px)` : undefined,
                         // mixBlendMode: this.layoutDoc.tool === InkTool.Highlighter ? 'multiply' : 'unset',
                         cursor: this._props.isSelected() ? 'default' : undefined,
                     }}
diff --git a/src/client/views/LightboxView.tsx b/src/client/views/LightboxView.tsx
index ef4b5b4ca..c34554b56 100644
--- a/src/client/views/LightboxView.tsx
+++ b/src/client/views/LightboxView.tsx
@@ -4,7 +4,8 @@ import { Toggle, ToggleType, Type } from 'browndash-components';
 import { action, computed, makeObservable, observable, runInAction } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
-import { Utils, emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnTrue } from '../../Utils';
+import { ClientUtils, returnEmptyDoclist, returnEmptyFilter, returnTrue } from '../../ClientUtils';
+import { emptyFunction } from '../../Utils';
 import { Doc, DocListCast, FieldResult, Opt } from '../../fields/Doc';
 import { Id } from '../../fields/FieldSymbols';
 import { InkTool } from '../../fields/InkField';
@@ -127,7 +128,9 @@ export class LightboxView extends ObservableReactComponent<LightboxViewProps> {
             DocumentManager.Instance.showDocument(target, { willZoomCentered: true, zoomScale: 0.9 });
             if (this._history.lastElement().target !== target) this._history.push({ doc: lightDoc, target });
         } else if (!target && this._path.length) {
-            savedKeys.forEach(key => (lightDoc[key] = this._savedState[key]));
+            savedKeys.forEach(key => {
+                lightDoc[key] = this._savedState[key];
+            });
             this._path.pop();
         } else {
             this.SetLightboxDoc(target);
@@ -239,7 +242,7 @@ export class LightboxView extends ObservableReactComponent<LightboxViewProps> {
                     downx = e.clientX;
                     downy = e.clientY;
                 }}
-                onClick={e => Utils.isClick(e.clientX, e.clientY, downx, downy, Date.now()) && this.SetLightboxDoc(undefined)}>
+                onClick={e => ClientUtils.isClick(e.clientX, e.clientY, downx, downy, Date.now()) && this.SetLightboxDoc(undefined)}>
                 <div
                     className="lightboxView-contents"
                     style={{
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 58b8d255a..8a060d0e0 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -7,7 +7,8 @@ import { action, computed, configure, makeObservable, observable, reaction, runI
 import { observer } from 'mobx-react';
 import * as React from 'react';
 import '../../../node_modules/browndash-components/dist/styles/global.min.css';
-import { Utils, emptyFunction, lightOrDark, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue, returnZero, setupMoveUpEvents } from '../../Utils';
+import { ClientUtils, lightOrDark, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue, returnZero, setupMoveUpEvents } from '../../ClientUtils';
+import { emptyFunction } from '../../Utils';
 import { Doc, DocListCast, Opt } from '../../fields/Doc';
 import { DocData } from '../../fields/DocSymbols';
 import { DocCast, StrCast } from '../../fields/Types';
@@ -18,10 +19,12 @@ import { Docs } from '../documents/Documents';
 import { CalendarManager } from '../util/CalendarManager';
 import { CaptureManager } from '../util/CaptureManager';
 import { DocumentManager } from '../util/DocumentManager';
-import { DragManager, dropActionType } from '../util/DragManager';
+import { DragManager } from '../util/DragManager';
+import { dropActionType } from '../util/DropActionTypes';
 import { GroupManager } from '../util/GroupManager';
 import { HistoryUtil } from '../util/History';
 import { Hypothesis } from '../util/HypothesisUtils';
+import { UPDATE_SERVER_CACHE } from '../util/LinkManager';
 import { RTFMarkup } from '../util/RTFMarkup';
 import { ScriptingGlobals } from '../util/ScriptingGlobals';
 import { SelectionManager } from '../util/SelectionManager';
@@ -57,7 +60,7 @@ import { AudioBox } from './nodes/AudioBox';
 import { SchemaCSVPopUp } from './nodes/DataVizBox/SchemaCSVPopUp';
 import { DocButtonState } from './nodes/DocumentLinksButton';
 import { DocumentView, DocumentViewInternal, OpenWhere, OpenWhereMod, returnEmptyDocViewList } from './nodes/DocumentView';
-import { ImageBox, ImageEditorData as ImageEditor } from './nodes/ImageBox';
+import { ImageEditorData as ImageEditor } from './nodes/ImageBox';
 import { LinkDescriptionPopup } from './nodes/LinkDescriptionPopup';
 import { LinkDocPreview, LinkInfo } from './nodes/LinkDocPreview';
 import { DirectionsAnchorMenu } from './nodes/MapBox/DirectionsAnchorMenu';
@@ -71,11 +74,13 @@ import { PresBox } from './nodes/trails';
 import { AnchorMenu } from './pdf/AnchorMenu';
 import { GPTPopup } from './pdf/GPTPopup/GPTPopup';
 import { TopBar } from './topbar/TopBar';
-const { LEFT_MENU_WIDTH, TOPBAR_HEIGHT } = require('./global/globalCssVariables.module.scss'); // prettier-ignore
+
 const _global = (window /* browser */ || global) /* node */ as any;
+const { LEFT_MENU_WIDTH, TOPBAR_HEIGHT } = require('./global/globalCssVariables.module.scss'); // prettier-ignore
 
 @observer
 export class MainView extends ObservableReactComponent<{}> {
+    // eslint-disable-next-line no-use-before-define
     public static Instance: MainView;
     public static Live: boolean = false;
     private _docBtnRef = React.createRef<HTMLDivElement>();
@@ -117,8 +122,12 @@ export class MainView extends ObservableReactComponent<{}> {
     }
     @observable mainDoc: Opt<Doc> = undefined;
     @computed private get mainContainer() {
-        if (window.location.pathname.startsWith('/doc/') && Doc.CurrentUserEmail === 'guest') {
-            DocServer.GetRefField(window.location.pathname.substring('/doc/'.length)).then(main => runInAction(() => (this.mainDoc = main as Doc)));
+        if (window.location.pathname.startsWith('/doc/') && ClientUtils.CurrentUserEmail === 'guest') {
+            DocServer.GetRefField(window.location.pathname.substring('/doc/'.length)).then(main =>
+                runInAction(() => {
+                    this.mainDoc = main as Doc;
+                })
+            );
             return this.mainDoc;
         }
         return this.userDoc ? Doc.ActiveDashboard : Doc.GuestDashboard;
@@ -165,7 +174,11 @@ export class MainView extends ObservableReactComponent<{}> {
         scriptTag.async = true;
         scriptTag.defer = true;
         document.body.appendChild(scriptTag);
-        document.getElementById('root')?.addEventListener('scroll', e => (ele => (ele.scrollLeft = ele.scrollTop = 0))(document.getElementById('root')!));
+        document.getElementById('root')?.addEventListener('scroll', () =>
+            (ele => {
+                ele.scrollLeft = ele.scrollTop = 0;
+            })(document.getElementById('root')!)
+        );
         const ele = document.getElementById('loader');
         const prog = document.getElementById('dash-progress');
         if (ele && prog) {
@@ -174,7 +187,9 @@ export class MainView extends ObservableReactComponent<{}> {
                 prog.style.transition = '1s';
                 prog.style.width = '100%';
             }, 0);
-            setTimeout(() => (ele.outerHTML = ''), 1000);
+            setTimeout(() => {
+                ele.outerHTML = '';
+            }, 1000);
         }
         this._sidebarContent.proto = undefined;
         if (!MainView.Live) {
@@ -202,7 +217,7 @@ export class MainView extends ObservableReactComponent<{}> {
                 'text_scrollHeight',
                 'text_height',
                 'hidden',
-                //'type_collection',
+                // 'type_collection',
                 'chromeHidden',
                 'currentFrame',
             ]); // can play with these fields on someone else's
@@ -530,7 +545,7 @@ export class MainView extends ObservableReactComponent<{}> {
     }
 
     private longPressTimer: NodeJS.Timeout | undefined;
-    globalPointerClick = action((e: any) => {
+    globalPointerClick = action(() => {
         this.longPressTimer && clearTimeout(this.longPressTimer);
         DocumentView.LongPress = false;
     });
@@ -540,7 +555,9 @@ export class MainView extends ObservableReactComponent<{}> {
     globalPointerDown = action((e: PointerEvent) => {
         DocumentView.LongPress = false;
         this.longPressTimer = setTimeout(
-            action(() => (DocumentView.LongPress = true)),
+            action(() => {
+                DocumentView.LongPress = true;
+            }),
             1000
         );
         DocumentManager.removeOverlayViews();
@@ -559,7 +576,7 @@ export class MainView extends ObservableReactComponent<{}> {
     });
 
     initEventListeners = () => {
-        window.addEventListener('beforeunload', DocServer.UPDATE_SERVER_CACHE);
+        window.addEventListener('beforeunload', UPDATE_SERVER_CACHE);
         window.addEventListener('drop', e => e.preventDefault(), false); // prevent default behavior of navigating to a new web page
         window.addEventListener('dragover', e => e.preventDefault(), false);
         document.addEventListener('pointerdown', this.globalPointerDown, true);
@@ -625,10 +642,10 @@ export class MainView extends ObservableReactComponent<{}> {
                     isDocumentActive={returnTrue} // headerBar is always documentActive (ie, the docView gets pointer events)
                     isContentActive={returnTrue} // headerBar is awlays contentActive which means its items are always documentActive
                     ScreenToLocalTransform={this.headerBarScreenXf}
-                    childHideResizeHandles={true}
+                    childHideResizeHandles
                     childDragAction={dropActionType.move}
-                    dontRegisterView={true}
-                    hideResizeHandles={true}
+                    dontRegisterView
+                    hideResizeHandles
                     PanelWidth={this.headerBarDocWidth}
                     PanelHeight={this.headerBarDocHeight}
                     renderDepth={0}
@@ -664,7 +681,7 @@ export class MainView extends ObservableReactComponent<{}> {
                     childFilters={returnEmptyFilter}
                     childFiltersByRanges={returnEmptyFilter}
                     searchFilterDocs={returnEmptyDoclist}
-                    suppressSetHeight={true}
+                    suppressSetHeight
                     renderDepth={this._hideUI ? 0 : -1}
                 />
             </>
@@ -673,7 +690,7 @@ export class MainView extends ObservableReactComponent<{}> {
 
     @computed get dockingContent() {
         return (
-            <GestureOverlay isActive={LightboxView.LightboxDoc ? false : true}>
+            <GestureOverlay isActive={!LightboxView.LightboxDoc}>
                 <div
                     key="docking"
                     className={`mainView-dockingContent${this._leftMenuFlyoutWidth ? '-flyout' : ''}`}
@@ -697,9 +714,16 @@ export class MainView extends ObservableReactComponent<{}> {
         setupMoveUpEvents(
             this,
             e,
-            action(e => ((SettingsManager.Instance.propertiesWidth = Math.max(0, this._dashUIWidth - e.clientX)) ? false : false)),
-            action(() => SettingsManager.Instance.propertiesWidth < 5 && (SettingsManager.Instance.propertiesWidth = 0)),
-            action(() => (SettingsManager.Instance.propertiesWidth = this.propertiesWidth() < 15 ? Math.min(this._dashUIWidth - 50, 250) : 0)),
+            action(() => {
+                SettingsManager.Instance.propertiesWidth = Math.max(0, this._dashUIWidth - e.clientX);
+                return !SettingsManager.Instance.propertiesWidth;
+            }),
+            action(() => {
+                SettingsManager.Instance.propertiesWidth < 5 && (SettingsManager.Instance.propertiesWidth = 0);
+            }),
+            action(() => {
+                SettingsManager.Instance.propertiesWidth = this.propertiesWidth() < 15 ? Math.min(this._dashUIWidth - 50, 250) : 0;
+            }),
             false
         );
     };
@@ -709,7 +733,10 @@ export class MainView extends ObservableReactComponent<{}> {
         setupMoveUpEvents(
             this,
             e,
-            action(e => ((this._leftMenuFlyoutWidth = Math.max(e.clientX - 58, 0)) ? false : false)),
+            action(e => {
+                this._leftMenuFlyoutWidth = Math.max(e.clientX - 58, 0);
+                return false;
+            }),
             () => this._leftMenuFlyoutWidth < 5 && this.closeFlyout(),
             this.closeFlyout
         );
@@ -734,7 +761,7 @@ export class MainView extends ObservableReactComponent<{}> {
 
     @computed get flyout() {
         return !this._leftMenuFlyoutWidth ? (
-            <div key="flyout" className={`mainView-libraryFlyout-out`}>
+            <div key="flyout" className="mainView-libraryFlyout-out">
                 {this.docButtons}
             </div>
         ) : (
@@ -837,11 +864,9 @@ export class MainView extends ObservableReactComponent<{}> {
                             </div>
                         )}
                         <div className="properties-container" style={{ width: this.propertiesWidth(), color: SettingsManager.userColor }}>
-                            {
-                                <div style={{ display: this.propertiesWidth() < 10 ? 'none' : undefined }}>
-                                    <PropertiesView styleProvider={DefaultStyleProvider} addDocTab={DocumentViewInternal.addDocTabFunc} width={this.propertiesWidth()} height={this.propertiesHeight()} />
-                                </div>
-                            }
+                            <div style={{ display: this.propertiesWidth() < 10 ? 'none' : undefined }}>
+                                <PropertiesView styleProvider={DefaultStyleProvider} addDocTab={DocumentViewInternal.addDocTabFunc} width={this.propertiesWidth()} height={this.propertiesHeight()} />
+                            </div>
                         </div>
                     </div>
                 </div>
@@ -878,7 +903,7 @@ export class MainView extends ObservableReactComponent<{}> {
         // generate the wrong value from getClientRectangle() -- specifically they return an 'x' that is the flyout's width greater than it should be.
         // interactively adjusting the flyout fixes the problem.  So does programmatically changing the value after a timeout to something *fractionally* different (ie, 1.5, not 1);)
         this._leftMenuFlyoutWidth = this._leftMenuFlyoutWidth || 250;
-        //setTimeout(action(() => (this._leftMenuFlyoutWidth += 0.5)));
+        // setTimeout(action(() => (this._leftMenuFlyoutWidth += 0.5)));
 
         this._sidebarContent.proto = DocCast(button.target);
         SettingsManager.Instance.SetLastPressedBtn(button);
@@ -897,7 +922,7 @@ export class MainView extends ObservableReactComponent<{}> {
 
     buttonBarXf = () => {
         if (!this._docBtnRef.current) return Transform.Identity();
-        const { scale, translateX, translateY } = Utils.GetScreenTransform(this._docBtnRef.current);
+        const { scale, translateX, translateY } = ClientUtils.GetScreenTransform(this._docBtnRef.current);
         return new Transform(-translateX, -translateY, 1 / scale);
     };
 
@@ -940,9 +965,11 @@ export class MainView extends ObservableReactComponent<{}> {
             <div className="mainView-snapLines">
                 <svg style={{ width: '100%', height: '100%' }}>
                     {SnappingManager.HorizSnapLines.map((l, i) => (
+                        // eslint-disable-next-line react/no-array-index-key
                         <line key={i} x1="0" y1={l} x2="2000" y2={l} stroke={lightOrDark(dragPar.layoutDoc.backgroundColor ?? 'gray')} opacity={0.3} strokeWidth={1} strokeDasharray={'2 2'} />
                     ))}
                     {SnappingManager.VertSnapLines.map((l, i) => (
+                        // eslint-disable-next-line react/no-array-index-key
                         <line key={i} y1={this.topOfMainDocContent.toString()} x1={l} y2="2000" x2={l} stroke={lightOrDark(dragPar.layoutDoc.backgroundColor ?? 'gray')} opacity={0.3} strokeWidth={1} strokeDasharray={'2 2'} />
                     ))}
                 </svg>
@@ -961,13 +988,14 @@ export class MainView extends ObservableReactComponent<{}> {
                             values="1 0 0 0 0
                 0 0 0 0 0
                 0 0 0 0 0
-                0 0 0 1 0"></feColorMatrix>
-                        <feGaussianBlur in="color" stdDeviation="4" result="blur"></feGaussianBlur>
-                        <feOffset in="blur" dx="0" dy="0" result="offset"></feOffset>
+                0 0 0 1 0"
+                        />
+                        <feGaussianBlur in="color" stdDeviation="4" result="blur" />
+                        <feOffset in="blur" dx="0" dy="0" result="offset" />
                         <feMerge>
-                            <feMergeNode in="bg"></feMergeNode>
-                            <feMergeNode in="offset"></feMergeNode>
-                            <feMergeNode in="SourceGraphic"></feMergeNode>
+                            <feMergeNode in="bg" />
+                            <feMergeNode in="offset" />
+                            <feMergeNode in="SourceGraphic" />
                         </feMerge>
                     </filter>
                 </defs>
@@ -984,7 +1012,11 @@ export class MainView extends ObservableReactComponent<{}> {
                     color: SettingsManager.userColor,
                     background: SettingsManager.userBackgroundColor,
                 }}
-                onScroll={() => (ele => (ele.scrollTop = ele.scrollLeft = 0))(document.getElementById('root')!)}
+                onScroll={() =>
+                    (ele => {
+                        ele.scrollTop = ele.scrollLeft = 0;
+                    })(document.getElementById('root')!)
+                }
                 ref={r => {
                     r &&
                         new _global.ResizeObserver(
@@ -1009,23 +1041,31 @@ export class MainView extends ObservableReactComponent<{}> {
                 <ComponentDecorations boundsLeft={this.leftScreenOffsetOfMainDocView} boundsTop={this.topOfMainDocContent} />
                 {this._hideUI ? null : <TopBar />}
                 <LinkDescriptionPopup />
-                {DocButtonState.Instance.LinkEditorDocView ? <LinkMenu clearLinkEditor={action(() => (DocButtonState.Instance.LinkEditorDocView = undefined))} docView={DocButtonState.Instance.LinkEditorDocView} /> : null}
-                {LinkInfo.Instance?.LinkInfo ? <LinkDocPreview {...LinkInfo.Instance.LinkInfo} /> : null}
-
+                {DocButtonState.Instance.LinkEditorDocView ? (
+                    <LinkMenu
+                        clearLinkEditor={action(() => {
+                            DocButtonState.Instance.LinkEditorDocView = undefined;
+                        })}
+                        docView={DocButtonState.Instance.LinkEditorDocView}
+                    />
+                ) : null}
+                {LinkInfo.Instance?.LinkInfo ? (
+                    // eslint-disable-next-line react/jsx-props-no-spreading
+                    <LinkDocPreview {...LinkInfo.Instance.LinkInfo} />
+                ) : null}
                 {((page: string) => {
                     // prettier-ignore
                     switch (page) {
-                        default:
-                        case 'dashboard': return (<>
+                        case 'home': return <DashboardView />;
+                        case 'dashboard':
+                        default: return (<>
                                         <div key="dashdiv" style={{ position: 'relative', display: this._hideUI || LightboxView.LightboxDoc ? 'none' : undefined, zIndex: 2001 }}>
                                             <CollectionMenu panelWidth={this.topMenuWidth} panelHeight={this.topMenuHeight} toggleTopBar={this.toggleTopBar} topBarHeight={this.headerBarHeightFunc}/>
                                         </div>
                                         {this.mainDashboardArea}
                                     </> );
-                        case 'home': return <DashboardView />;
                     }
                 })(Doc.ActivePage)}
-
                 <PreviewCursor />
                 <TaskCompletionBox />
                 <ContextMenu />
@@ -1049,15 +1089,19 @@ export class MainView extends ObservableReactComponent<{}> {
     }
 }
 
-ScriptingGlobals.add(function selectMainMenu(doc: Doc, title: string) {
+// eslint-disable-next-line prefer-arrow-callback
+ScriptingGlobals.add(function selectMainMenu(doc: Doc) {
     MainView.Instance.selectMenu(doc);
 });
+// eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function createNewPresentation() {
     return MainView.Instance.createNewPresentation();
 }, 'creates a new presentation when called');
+// eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function openPresentation(pres: Doc) {
     return MainView.Instance.openPresentation(pres);
 }, 'creates a new presentation when called');
+// eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function createNewFolder() {
     return MainView.Instance.createNewFolder();
 }, 'creates a new folder in myFiles when called');
diff --git a/src/client/views/MainViewModal.tsx b/src/client/views/MainViewModal.tsx
index af7f38937..e52562625 100644
--- a/src/client/views/MainViewModal.tsx
+++ b/src/client/views/MainViewModal.tsx
@@ -1,7 +1,7 @@
 import { isDark } from 'browndash-components';
 import { observer } from 'mobx-react';
 import * as React from 'react';
-import { SettingsManager } from '../util/SettingsManager';
+import { SnappingManager } from '../util/SnappingManager';
 import './MainViewModal.scss';
 
 export interface MainViewOverlayProps {
@@ -43,7 +43,7 @@ export class MainViewModal extends React.Component<MainViewOverlayProps> {
                     className="overlay"
                     onClick={this.props?.closeOnExternalClick}
                     style={{
-                        backgroundColor: isDark(SettingsManager.userColor) ? '#DFDFDF30' : '#32323230',
+                        backgroundColor: isDark(SnappingManager.userColor) ? '#DFDFDF30' : '#32323230',
                         ...(p.overlayStyle || {}),
                     }}
                 />
diff --git a/src/client/views/MarqueeAnnotator.tsx b/src/client/views/MarqueeAnnotator.tsx
index 9fa20f642..f92bd68a3 100644
--- a/src/client/views/MarqueeAnnotator.tsx
+++ b/src/client/views/MarqueeAnnotator.tsx
@@ -14,8 +14,8 @@ import { undoable, undoBatch, UndoManager } from '../util/UndoManager';
 import './MarqueeAnnotator.scss';
 import { DocumentView } from './nodes/DocumentView';
 import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox';
-import { AnchorMenu } from './pdf/AnchorMenu';
 import { ObservableReactComponent } from './ObservableReactComponent';
+import { AnchorMenu } from './pdf/AnchorMenu';
 const _global = (window /* browser */ || global) /* node */ as any;
 
 export interface MarqueeAnnotatorProps {
diff --git a/src/client/views/MetadataEntryMenu.tsx b/src/client/views/MetadataEntryMenu.tsx
index 89c3c41f8..9626f1653 100644
--- a/src/client/views/MetadataEntryMenu.tsx
+++ b/src/client/views/MetadataEntryMenu.tsx
@@ -3,7 +3,7 @@ import { observer } from 'mobx-react';
 import * as React from 'react';
 import * as Autosuggest from 'react-autosuggest';
 import { emptyFunction, emptyPath } from '../../Utils';
-import { Doc, DocListCast, Field } from '../../fields/Doc';
+import { Doc, DocListCast, Field, FieldType } from '../../fields/Doc';
 import { undoBatch } from '../util/UndoManager';
 import './MetadataEntryMenu.scss';
 import { KeyValueBox } from './nodes/KeyValueBox';
@@ -12,7 +12,6 @@ export type DocLike = Doc | Doc[] | Promise<Doc> | Promise<Doc[]>;
 export interface MetadataEntryProps {
     docs: Doc[];
     onError?: () => boolean;
-    suggestWithFunction?: boolean;
 }
 
 @observer
@@ -35,10 +34,10 @@ export class MetadataEntryMenu extends React.Component<MetadataEntryProps> {
     };
 
     previewValue = async () => {
-        let field: Field | undefined | null = null;
+        let field: FieldType | undefined | null = null;
         let onProto: boolean = false;
-        let value: string | undefined = undefined;
-        const docs = this.props.docs;
+        let value: string | undefined;
+        const { docs } = this.props;
         for (const doc of docs) {
             const v = await doc[this._currentKey];
             onProto = onProto || !Object.keys(doc).includes(this._currentKey);
diff --git a/src/client/views/ObservableReactComponent.tsx b/src/client/views/ObservableReactComponent.tsx
index 394d1eae2..266411770 100644
--- a/src/client/views/ObservableReactComponent.tsx
+++ b/src/client/views/ObservableReactComponent.tsx
@@ -14,7 +14,10 @@ export abstract class ObservableReactComponent<T> extends React.Component<T, {}>
         makeObservable(this);
     }
     componentDidUpdate(prevProps: Readonly<T>): void {
-        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
+        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/OverlayView.tsx b/src/client/views/OverlayView.tsx
index 15b1f0275..6539c0834 100644
--- a/src/client/views/OverlayView.tsx
+++ b/src/client/views/OverlayView.tsx
@@ -3,13 +3,15 @@ import { observer } from 'mobx-react';
 import { computedFn } from 'mobx-utils';
 import * as React from 'react';
 import ReactLoading from 'react-loading';
-import { Utils, emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnTrue, setupMoveUpEvents } from '../../Utils';
+import { returnEmptyDoclist, returnEmptyFilter, returnTrue, setupMoveUpEvents } from '../../ClientUtils';
+import { Utils, emptyFunction } from '../../Utils';
 import { Doc } from '../../fields/Doc';
 import { Height, Width } from '../../fields/DocSymbols';
 import { Id } from '../../fields/FieldSymbols';
 import { NumCast } from '../../fields/Types';
 import { DocumentType } from '../documents/DocumentTypes';
-import { DragManager, dropActionType } from '../util/DragManager';
+import { DragManager } from '../util/DragManager';
+import { dropActionType } from '../util/DropActionTypes';
 import { Transform } from '../util/Transform';
 import { LightboxView } from './LightboxView';
 import { ObservableReactComponent } from './ObservableReactComponent';
@@ -50,14 +52,14 @@ export class OverlayWindow extends ObservableReactComponent<OverlayWindowProps>
         this.height = opts.height || 200;
     }
 
-    onPointerDown = (_: React.PointerEvent) => {
+    onPointerDown = () => {
         document.removeEventListener('pointermove', this.onPointerMove);
         document.removeEventListener('pointerup', this.onPointerUp);
         document.addEventListener('pointermove', this.onPointerMove);
         document.addEventListener('pointerup', this.onPointerUp);
     };
 
-    onResizerPointerDown = (_: React.PointerEvent) => {
+    onResizerPointerDown = () => {
         document.removeEventListener('pointermove', this.onResizerPointerMove);
         document.removeEventListener('pointerup', this.onResizerPointerUp);
         document.addEventListener('pointermove', this.onResizerPointerMove);
@@ -95,12 +97,12 @@ export class OverlayWindow extends ObservableReactComponent<OverlayWindowProps>
             <div className="overlayWindow-outerDiv" style={{ transform: `translate(${this.x}px, ${this.y}px)`, width: this.width, height: this.height }}>
                 <div className="overlayWindow-titleBar" onPointerDown={this.onPointerDown}>
                     {this._props.overlayOptions.title || 'Untitled'}
-                    <button onClick={this._props.onClick} className="overlayWindow-closeButton">
+                    <button type="button" onClick={this._props.onClick} className="overlayWindow-closeButton">
                         X
                     </button>
                 </div>
                 <div className="overlayWindow-content">{this.props.children}</div>
-                <div className="overlayWindow-resizeDragger" onPointerDown={this.onResizerPointerDown}></div>
+                <div className="overlayWindow-resizeDragger" onPointerDown={this.onResizerPointerDown} />
             </div>
         );
     }
@@ -108,6 +110,7 @@ export class OverlayWindow extends ObservableReactComponent<OverlayWindowProps>
 
 @observer
 export class OverlayView extends ObservableReactComponent<{}> {
+    // eslint-disable-next-line no-use-before-define
     public static Instance: OverlayView;
     @observable.shallow _elements: JSX.Element[] = [];
 
@@ -118,8 +121,9 @@ export class OverlayView extends ObservableReactComponent<{}> {
             OverlayView.Instance = this;
             new _global.ResizeObserver(
                 action((entries: any) => {
-                    for (const entry of entries) {
-                        Doc.MyOverlayDocs.forEach(doc => {
+                    Array.from(entries).forEach((entry: any) => {
+                        Doc.MyOverlayDocs.forEach(docIn => {
+                            const doc = docIn;
                             if (NumCast(doc.overlayX) > entry.contentRect.width - 10) {
                                 doc.overlayX = entry.contentRect.width - 10;
                             }
@@ -127,7 +131,7 @@ export class OverlayView extends ObservableReactComponent<{}> {
                                 doc.overlayY = entry.contentRect.height - 10;
                             }
                         });
-                    }
+                    });
                 })
             ).observe(window.document.body);
         }
@@ -162,7 +166,7 @@ export class OverlayView extends ObservableReactComponent<{}> {
             const index = this._elements.indexOf(contents);
             if (index !== -1) this._elements.splice(index, 1);
         });
-        contents = (
+        const wincontents = (
             <OverlayWindow onClick={remove} key={Utils.GenerateGuid()} overlayOptions={options}>
                 {contents}
             </OverlayWindow>
@@ -177,15 +181,16 @@ export class OverlayView extends ObservableReactComponent<{}> {
     };
 
     docScreenToLocalXf = computedFn(
+        // eslint-disable-next-line prefer-arrow-callback
         function docScreenToLocalXf(this: any, doc: Doc) {
             return () => new Transform(-NumCast(doc.overlayX), -NumCast(doc.overlayY), 1);
-        }.bind(this)
+        }
     );
 
     @computed get overlayDocs() {
         return Doc.MyOverlayDocs.filter(d => !LightboxView.LightboxDoc || d.type === DocumentType.PRES).map(d => {
-            let offsetx = 0,
-                offsety = 0;
+            let offsetx = 0;
+            let offsety = 0;
             const dref = React.createRef<HTMLDivElement>();
             const onPointerMove = action((e: PointerEvent, down: number[]) => {
                 if (e.cancelBubble) return false; // if the overlay doc processed the move event (e.g., to pan its contents), then the event should be marked as canceled since propagation can't be stopped
@@ -198,9 +203,7 @@ export class OverlayView extends ObservableReactComponent<{}> {
                     dragData.offset = [-offsetx, -offsety];
                     dragData.dropAction = dropActionType.move;
                     dragData.removeDocument = this.removeOverlayDoc;
-                    dragData.moveDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (doc: Doc | Doc[]) => boolean): boolean => {
-                        return dragData.removeDocument!(doc) ? addDocument(doc) : false;
-                    };
+                    dragData.moveDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (doc: Doc | Doc[]) => boolean): boolean => (dragData.removeDocument?.(doc) ? addDocument(doc) : false);
                     DragManager.StartDocumentDrag([dref.current!], dragData, down[0], down[1]);
                     return true;
                 }
@@ -227,7 +230,7 @@ export class OverlayView extends ObservableReactComponent<{}> {
                         PanelHeight={d[Height]}
                         ScreenToLocalTransform={this.docScreenToLocalXf(d)}
                         renderDepth={1}
-                        hideDecorations={true}
+                        hideDecorations
                         isDocumentActive={returnTrue}
                         isContentActive={returnTrue}
                         whenChildContentsActiveChanged={emptyFunction}
diff --git a/src/client/views/PreviewCursor.tsx b/src/client/views/PreviewCursor.tsx
index 4b7771f27..6d03034ec 100644
--- a/src/client/views/PreviewCursor.tsx
+++ b/src/client/views/PreviewCursor.tsx
@@ -1,7 +1,7 @@
 import { action, makeObservable, observable, runInAction } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
-import { lightOrDark, returnFalse } from '../../Utils';
+import { lightOrDark, returnFalse } from '../../ClientUtils';
 import { Doc, Opt } from '../../fields/Doc';
 import { DocUtils, Docs, DocumentOptions } from '../documents/Documents';
 import { ImageUtils } from '../util/Import & Export/ImageUtils';
diff --git a/src/client/views/PropertiesButtons.tsx b/src/client/views/PropertiesButtons.tsx
index 517a80d63..c7720d834 100644
--- a/src/client/views/PropertiesButtons.tsx
+++ b/src/client/views/PropertiesButtons.tsx
@@ -46,7 +46,7 @@ export class PropertiesButtons extends React.Component<{}, {}> {
 
     propertyToggleBtn = (label: (on?: any) => string, property: string, tooltip: (on?: any) => string, icon: (on?: any) => any, onClick?: (dv: Opt<DocumentView>, doc: Doc, property: string) => void, useUserDoc?: boolean) => {
         const targetDoc = useUserDoc ? Doc.UserDoc() : this.selectedLayoutDoc;
-        const onPropToggle = (dv: Opt<DocumentView>, doc: Doc, prop: string) => ((dv?.layoutDoc || doc)[prop] = (dv?.layoutDoc || doc)[prop] ? false : true);
+        const onPropToggle = (dv: Opt<DocumentView>, doc: Doc, prop: string) => ((dv?.layoutDoc || doc)[prop] = !(dv?.layoutDoc || doc)[prop]);
         return !targetDoc ? null : (
             <Toggle
                 toggleStatus={BoolCast(targetDoc[property])}
@@ -103,7 +103,7 @@ export class PropertiesButtons extends React.Component<{}, {}> {
             (dv, doc) => {
                 const tdoc = dv?.Document || doc;
                 const newtitle = !tdoc._layout_showTitle ? 'title' : tdoc._layout_showTitle === 'title' ? 'title:hover' : '';
-                tdoc._layout_showTitle = newtitle ? newtitle : undefined;
+                tdoc._layout_showTitle = newtitle || undefined;
             }
         );
     }
@@ -181,9 +181,9 @@ export class PropertiesButtons extends React.Component<{}, {}> {
             on => (on ? 'DISABLE FLASHCARD' : 'ENABLE FLASHCARD'),
             'layout_textPainted',
             on => `${on ? 'Flashcard enabled' : 'Flashcard disabled'} `,
-            on => <MdTouchApp />,
+            () => <MdTouchApp />,
             (dv, doc) => {
-                const on = doc.onPaint ? true : false;
+                const on = !!doc.onPaint;
                 doc[DocData].onPaint = on ? undefined : ScriptField.MakeScript(`toggleDetail(documentView, "textPainted")`, { documentView: 'any' });
                 doc[DocData].layout_textPainted = on ? undefined : `<ComparisonBox {...props} fieldKey={'${dv?.LayoutFieldKey ?? 'text'}'}/>`;
             }
diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx
index ae29382c1..77e68866e 100644
--- a/src/client/views/PropertiesView.tsx
+++ b/src/client/views/PropertiesView.tsx
@@ -1,3 +1,6 @@
+/* eslint-disable jsx-a11y/click-events-have-key-events */
+/* eslint-disable jsx-a11y/no-static-element-interactions */
+/* eslint-disable prettier/prettier */
 import { IconLookup } from '@fortawesome/fontawesome-svg-core';
 import { faAnchor, faArrowRight, faWindowMaximize } from '@fortawesome/free-solid-svg-icons';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
@@ -9,8 +12,9 @@ import { observer } from 'mobx-react';
 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, Field, FieldResult, HierarchyMapping, NumListCast, Opt, ReverseHierarchyMap, StrListCast } from '../../fields/Doc';
+import { ClientUtils, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue, setupMoveUpEvents } from '../../ClientUtils';
+import { emptyFunction } from '../../Utils';
+import { Doc, Field, FieldType, 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';
@@ -54,6 +58,7 @@ interface PropertiesViewProps {
 export class PropertiesView extends ObservableReactComponent<PropertiesViewProps> {
     private _widthUndo?: UndoManager.Batch;
 
+    // eslint-disable-next-line no-use-before-define
     public static Instance: PropertiesView | undefined;
     constructor(props: any) {
         super(props);
@@ -101,7 +106,7 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
     @observable openTransform: boolean = true;
     @observable openFilters: boolean = false;
 
-    //Pres Trails booleans:
+    // Pres Trails booleans:
     @observable openPresTransitions: boolean = true;
     @observable openPresProgressivize: boolean = false;
     @observable openPresVisibilityAndDuration: boolean = false;
@@ -183,35 +188,37 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
                     .forEach(key => doc[key] !== ComputedField.undefined && key && ids.add(key))
             );
 
-            // prettier-ignore
-            Array.from(ids).sort().map(key => {
-                const multiple = Array.from(docs.reduce((set,doc) => set.add(doc[key]), new Set<FieldResult>()).keys()).length > 1;
-                const editableContents = multiple ? '-multiple-' : Field.toKeyValueString(docs[0], key);
-                const displayContents = multiple ? '-multiple-' : Field.toString(docs[0][key] as Field);
-                const contentElement = (
-                    <EditableView
-                        key="editableView"
-                        contents={displayContents}
-                        height={13}
-                        fontSize={10}
-                        GetValue={() => editableContents}
-                        SetValue={(value: string) => {
-                            value !== '-multiple-' && docs.map(doc => KeyValueBox.SetField(doc, key, value, true));
-                            return true;
-                        }}
-                    />);
-                rows.push(
-                    <div style={{ display: 'flex', overflowY: 'visible', marginBottom: '-1px' }} key={key}>
-                        <span style={{ fontWeight: 'bold', whiteSpace: 'nowrap' }}>{key + ':'}</span>
-                        &nbsp;
-                        {contentElement}
-                    </div>
-                );
-            });
+            Array.from(ids)
+                .sort()
+                .forEach(key => {
+                    const multiple = Array.from(docs.reduce((set, doc) => set.add(doc[key]), new Set<FieldResult>()).keys()).length > 1;
+                    const editableContents = multiple ? '-multiple-' : Field.toKeyValueString(docs[0], key);
+                    const displayContents = multiple ? '-multiple-' : Field.toString(docs[0][key] as FieldType);
+                    const contentElement = (
+                        <EditableView
+                            key="editableView"
+                            contents={displayContents}
+                            height={13}
+                            fontSize={10}
+                            GetValue={() => editableContents}
+                            SetValue={(value: string) => {
+                                value !== '-multiple-' && docs.map(doc => KeyValueBox.SetField(doc, key, value, true));
+                                return true;
+                            }}
+                        />
+                    );
+                    rows.push(
+                        <div style={{ display: 'flex', overflowY: 'visible', marginBottom: '-1px' }} key={key}>
+                            <span style={{ fontWeight: 'bold', whiteSpace: 'nowrap' }}>{key + ':'}</span>
+                            &nbsp;
+                            {contentElement}
+                        </div>
+                    );
+                });
 
             rows.push(
                 <div className="propertiesView-field" key="newKeyValue" style={{ marginTop: '3px', backgroundColor: SettingsManager.userBackgroundColor, textAlign: 'center' }}>
-                    <EditableView key="editableView" oneLine contents={'add key:value or #tags'} height={13} fontSize={10} GetValue={() => ''} SetValue={this.setKeyValue} />
+                    <EditableView key="editableView" oneLine contents="add key:value or #tags" height={13} fontSize={10} GetValue={() => ''} SetValue={this.setKeyValue} />
                 </div>
             );
         }
@@ -240,7 +247,8 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
                     KeyValueBox.SetField(doc, 'tags', `"${tags.replace(splits[0] + ':', '')}"`, true);
                 }
                 return true;
-            } else if (value[0] === '#') {
+            }
+            if (value[0] === '#') {
                 const tags = StrListCast(doc.tags);
                 if (!tags.includes(value)) {
                     tags.push(value);
@@ -248,6 +256,7 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
                 }
                 return true;
             }
+            return undefined;
         });
         return false;
     };
@@ -256,7 +265,7 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
     getTransform = () => this.transform;
     propertiesDocViewRef = (ref: HTMLDivElement) => {
         const observer = new _global.ResizeObserver(
-            action((entries: any) => {
+            action(() => {
                 const cliRect = ref.getBoundingClientRect();
                 this.transform = new Transform(-cliRect.x, -cliRect.y, 1);
             })
@@ -265,28 +274,27 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
     };
 
     @computed get contexts() {
-        return !this.selectedDoc ? null : <PropertiesDocContextSelector DocView={this.selectedDocumentView} hideTitle={true} addDocTab={this._props.addDocTab} />;
+        return !this.selectedDoc ? null : <PropertiesDocContextSelector DocView={this.selectedDocumentView} hideTitle addDocTab={this._props.addDocTab} />;
     }
 
     @computed get contextCount() {
         if (this.selectedDocumentView) {
             const target = this.selectedDocumentView.Document;
             return Doc.GetEmbeddings(target).length - 1;
-        } else {
-            return 0;
         }
+        return 0;
     }
 
     @computed get links() {
         const selAnchor = this.selectedDocumentView?.anchorViewDoc ?? LinkManager.Instance.currentLinkAnchor ?? this.selectedDoc;
-        return !selAnchor ? null : <PropertiesDocBacklinksSelector Document={selAnchor} hideTitle={true} addDocTab={this._props.addDocTab} />;
+        return !selAnchor ? null : <PropertiesDocBacklinksSelector Document={selAnchor} hideTitle addDocTab={this._props.addDocTab} />;
     }
 
     @computed get linkCount() {
         const selAnchor = this.selectedDocumentView?.anchorViewDoc ?? LinkManager.Instance.currentLinkAnchor ?? this.selectedDoc;
-        var counter = 0;
+        let counter = 0;
 
-        LinkManager.Links(selAnchor).forEach((l, i) => counter++);
+        LinkManager.Links(selAnchor).forEach(() => counter++);
 
         return counter;
     }
@@ -308,7 +316,7 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
                         fitContentsToBox={returnTrue}
                         styleProvider={DefaultStyleProvider}
                         containerViewPath={returnEmptyDoclist}
-                        dontCenter={'y'}
+                        dontCenter="y"
                         isDocumentActive={returnFalse}
                         isContentActive={emptyFunction}
                         NativeWidth={layoutDoc.type === DocumentType.RTF ? this.rtfWidth : undefined}
@@ -326,13 +334,12 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
                         whenChildContentsActiveChanged={emptyFunction}
                         addDocTab={returnFalse}
                         pinToPres={emptyFunction}
-                        dontRegisterView={true}
+                        dontRegisterView
                     />
                 </div>
             );
-        } else {
-            return null;
         }
+        return null;
     }
 
     /**
@@ -381,7 +388,7 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
         return (
             <div className="expansion-button">
                 <IconButton
-                    icon={<FontAwesomeIcon icon={'ellipsis-h'} />}
+                    icon={<FontAwesomeIcon icon="ellipsis-h" />}
                     size={Size.XSMALL}
                     color={SettingsManager.userColor}
                     onClick={action(() => {
@@ -398,7 +405,7 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
      * @returns a row of the permissions panel
      */
     sharingItem(name: string, admin: boolean, permission: string, showExpansionIcon?: boolean) {
-        if (name == Doc.CurrentUserEmail) {
+        if (name === ClientUtils.CurrentUserEmail) {
             name = 'Me';
         }
         return (
@@ -415,7 +422,7 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
                 {/* {name !== "Me" ? this.notifyIcon : null} */}
                 <div className="propertiesView-sharingTable-item-permission">
                     {this.colorACLDropDown(name, admin, permission, false)}
-                    {(permission === 'Owner' && name == 'Me') || showExpansionIcon ? this.expansionIcon : null}
+                    {(permission === 'Owner' && name === 'Me') || showExpansionIcon ? this.expansionIcon : null}
                 </div>
             </div>
         );
@@ -425,10 +432,10 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
      * @returns a colored dropdown bar reflective of the permission
      */
     colorACLDropDown(name: string, admin: boolean, permission: string, showGuestOptions: boolean) {
-        var shareImage = ReverseHierarchyMap.get(permission)?.image;
+        const shareImage = ReverseHierarchyMap.get(permission)?.image;
         return (
             <div>
-                <div className={'propertiesView-shareDropDown'}>
+                <div className="propertiesView-shareDropDown">
                     <div className={`propertiesView-shareDropDown${permission}`}>
                         <div>{admin && permission !== 'Owner' ? this.getPermissionsSelect(name, permission, showGuestOptions) : concat(shareImage, ' ', permission)}</div>
                     </div>
@@ -440,9 +447,7 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
     /**
      * Sorting algorithm to sort users.
      */
-    sortUsers = (u1: String, u2: String) => {
-        return u1 > u2 ? -1 : u1 === u2 ? 0 : 1;
-    };
+    sortUsers = (u1: String, u2: String) => (u1 > u2 ? -1 : u1 === u2 ? 0 : 1);
 
     /**
      * Sorting algorithm to sort groups.
@@ -468,35 +473,35 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
         const seldoc = this.layoutDocAcls ? this.selectedLayoutDoc : this.selectedDoc?.[DocData];
         // adds each user to usersAdded
         SharingManager.Instance.users.forEach(eachUser => {
-            var userOnDoc = true;
+            let userOnDoc = true;
             if (seldoc) {
                 if (Doc.GetT(seldoc, 'acl-' + normalizeEmail(eachUser.user.email), 'string', true) === '' || Doc.GetT(seldoc, 'acl-' + normalizeEmail(eachUser.user.email), 'string', true) === undefined) {
                     userOnDoc = false;
                 }
             }
-            if (userOnDoc && !usersAdded.includes(eachUser.user.email) && eachUser.user.email !== 'guest' && eachUser.user.email != target.author) {
+            if (userOnDoc && !usersAdded.includes(eachUser.user.email) && eachUser.user.email !== 'guest' && eachUser.user.email !== target.author) {
                 usersAdded.push(eachUser.user.email);
             }
         });
 
         // sorts and then adds each user to the table
         usersAdded.sort(this.sortUsers);
-        usersAdded.map(userEmail => {
+        usersAdded.forEach(userEmail => {
             const userKey = `acl-${normalizeEmail(userEmail)}`;
-            var aclField = Doc.GetT(this.layoutDocAcls ? target : Doc.GetProto(target), userKey, 'string', true);
-            var permission = StrCast(aclField);
+            const aclField = Doc.GetT(this.layoutDocAcls ? target : Doc.GetProto(target), userKey, 'string', true);
+            const permission = StrCast(aclField);
             individualTableEntries.unshift(this.sharingItem(userEmail, showAdmin, permission!, false)); // adds each user
         });
 
         // adds current user
-        var userEmail = Doc.CurrentUserEmail;
-        if (userEmail == 'guest') userEmail = 'Guest';
+        let userEmail = ClientUtils.CurrentUserEmail;
+        if (userEmail === 'guest') userEmail = 'Guest';
         const userKey = `acl-${normalizeEmail(userEmail)}`;
-        if (!usersAdded.includes(userEmail) && userEmail !== 'Guest' && userEmail != target.author) {
-            var permission;
+        if (!usersAdded.includes(userEmail) && userEmail !== 'Guest' && userEmail !== target.author) {
+            let permission;
             if (this.layoutDocAcls) {
                 if (target[DocAcl][userKey]) permission = HierarchyMapping.get(target[DocAcl][userKey])?.name;
-                else if (target['embedContainer']) permission = StrCast(Doc.GetProto(DocCast(target['embedContainer']))[userKey]);
+                else if (target.embedContainer) permission = StrCast(Doc.GetProto(DocCast(target.embedContainer))[userKey]);
                 else permission = StrCast(Doc.GetProto(target)?.[userKey]);
             } else permission = StrCast(target[userKey]);
             individualTableEntries.unshift(this.sharingItem(userEmail, showAdmin, permission!, false)); // adds each user
@@ -509,15 +514,15 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
         const groupTableEntries: JSX.Element[] = [];
         const groupList = GroupManager.Instance?.allGroups || [];
         groupList.sort(this.sortGroups);
-        groupList.map(group => {
-            if (group.title != 'Guest' && this.selectedDoc) {
+        groupList.forEach(group => {
+            if (group.title !== 'Guest' && this.selectedDoc) {
                 const groupKey = 'acl-' + normalizeEmail(StrCast(group.title));
-                if (this.selectedDoc[groupKey] != '' && this.selectedDoc[groupKey] != undefined) {
-                    var permission;
+                if (this.selectedDoc[groupKey] !== '' && this.selectedDoc[groupKey] !== undefined) {
+                    let permission;
                     if (this.layoutDocAcls) {
                         if (target[DocAcl][groupKey]) {
                             permission = HierarchyMapping.get(target[DocAcl][groupKey])?.name;
-                        } else if (target['embedContainer']) permission = StrCast(Doc.GetProto(DocCast(target['embedContainer']))[groupKey]);
+                        } else if (target.embedContainer) permission = StrCast(Doc.GetProto(DocCast(target.embedContainer))[groupKey]);
                         else permission = StrCast(Doc.GetProto(target)?.[groupKey]);
                     } else permission = StrCast(target[groupKey]);
                     groupTableEntries.unshift(this.sharingItem(StrCast(group.title), showAdmin, permission!, false));
@@ -531,22 +536,22 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
         return (
             <div>
                 <div>
-                    <br></br> Individuals with Access to this Document
+                    <br /> Individuals with Access to this Document
                 </div>
                 <div className="propertiesView-sharingTable" style={{ background: SettingsManager.userBackgroundColor, color: SettingsManager.userColor }}>
-                    {<div> {individualTableEntries}</div>}
+                    <div> {individualTableEntries}</div>
                 </div>
                 {groupTableEntries.length > 0 ? (
                     <div>
                         <div>
-                            <br></br> Groups with Access to this Document
+                            <br /> Groups with Access to this Document
                         </div>
                         <div className="propertiesView-sharingTable" style={{ background: SettingsManager.userBackgroundColor, color: SettingsManager.userColor }}>
-                            {<div> {groupTableEntries}</div>}
+                            <div> {groupTableEntries}</div>
                         </div>
                     </div>
                 ) : null}
-                <br></br> Guest
+                <br /> Guest
                 <div>{this.colorACLDropDown('Guest', showAdmin, guestPermission!, true)}</div>
             </div>
         );
@@ -558,7 +563,9 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
     }
 
     @action
-    toggleCheckbox = () => (this.layoutFields = !this.layoutFields);
+    toggleCheckbox = () => {
+        this.layoutFields = !this.layoutFields;
+    };
 
     @computed get color() {
         return SettingsManager.userColor;
@@ -578,21 +585,17 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
         const title = Array.from(titles.keys()).length > 1 ? '--multiple selected--' : StrCast(this.selectedDoc?.title);
         return (
             <div>
-                <EditableText val={title} setVal={this.setTitle} color={this.color} type={Type.SEC} formLabel={'Title'} fillWidth />
+                <EditableText val={title} setVal={this.setTitle} color={this.color} type={Type.SEC} formLabel="Title" fillWidth />
                 {LinkManager.Instance.currentLinkAnchor ? (
                     <p className="propertiesView-titleExtender">
-                        <>
-                            <b>Anchor:</b>
-                            {LinkManager.Instance.currentLinkAnchor.title}
-                        </>
+                        <b>Anchor:</b>
+                        {StrCast(LinkManager.Instance.currentLinkAnchor.title)}
                     </p>
                 ) : null}
                 {this.selectedLink?.title ? (
                     <p className="propertiesView-titleExtender">
-                        <>
-                            <b>Link:</b>
-                            {this.selectedLink.title}
-                        </>
+                        <b>Link:</b>
+                        {StrCast(this.selectedLink.title)}
                     </p>
                 ) : null}
             </div>
@@ -601,8 +604,8 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
 
     @computed get currentType() {
         const documentType = StrCast(this.selectedDoc?.type);
-        var currentType: string = documentType;
-        var capitalizedDocType = Utils.cleanDocumentType(currentType as DocumentType);
+        const currentType: string = documentType;
+        const capitalizedDocType = ClientUtils.cleanDocumentType(currentType as DocumentType);
 
         return (
             <div>
@@ -618,7 +621,7 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
     }
 
     @computed get currentComponent() {
-        var iconName = StrCast(this.selectedDoc?.systemIcon);
+        const iconName = StrCast(this.selectedDoc?.systemIcon);
 
         if (iconName) {
             const Icon = Icons[iconName as keyof typeof Icons];
@@ -650,13 +653,11 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
                     const ys = ink.map(p => p.Y);
                     const left = Math.min(...xs);
                     const top = Math.min(...ys);
-                    const right = Math.max(...xs);
-                    const bottom = Math.max(...ys);
                     _centerPoints.push({ X: left, Y: top });
                 }
             }
 
-            var index = 0;
+            let index = 0;
             if (doc.type === DocumentType.INK && doc.x && doc.y && layout._width && layout._height && doc.data) {
                 layout.rotation = NumCast(layout.rotation) + angle;
                 const inks = Cast(doc.stroke, InkField)?.inkData;
@@ -687,11 +688,13 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
     get controlPointsButton() {
         return (
             <div className="inking-button">
-                <Tooltip title={<div className="dash-tooltip">{'Edit points'}</div>}>
+                <Tooltip title={<div className="dash-tooltip">Edit points</div>}>
                     <div
                         className="inking-button-points"
                         style={{ backgroundColor: InkStrokeProperties.Instance._controlButton ? 'black' : '' }}
-                        onPointerDown={action(() => (InkStrokeProperties.Instance._controlButton = !InkStrokeProperties.Instance._controlButton))}>
+                        onPointerDown={action(() => {
+                            InkStrokeProperties.Instance._controlButton = !InkStrokeProperties.Instance._controlButton;
+                        })}>
                         <FontAwesomeIcon icon="bezier-curve" size="lg" />
                     </div>
                 </Tooltip>
@@ -699,152 +702,129 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
         );
     }
 
-    inputBox = (key: string, value: any, setter: (val: string) => {}, title: string) => {
-        return (
-            <div
-                className="inputBox"
-                style={{
-                    marginRight: title === 'X:' ? '19px' : '',
-                    marginLeft: title === '∠:' ? '39px' : '',
-                }}>
-                <div className="inputBox-title"> {title} </div>
-                <input
-                    className="inputBox-input"
-                    type="text"
-                    value={value}
-                    style={{ color: SettingsManager.userColor, backgroundColor: SettingsManager.userBackgroundColor }}
-                    onChange={e => setter(e.target.value)}
-                    onKeyDown={e => e.stopPropagation()}
-                />
-                <div className="inputBox-button">
-                    <div className="inputBox-button-up" key="up2" onPointerDown={undoBatch(action(() => this.upDownButtons('up', key)))}>
-                        <FontAwesomeIcon icon="caret-up" size="sm" />
-                    </div>
-                    <div className="inputbox-Button-down" key="down2" onPointerDown={undoBatch(action(() => this.upDownButtons('down', key)))}>
-                        <FontAwesomeIcon icon="caret-down" size="sm" />
-                    </div>
+    inputBox = (key: string, value: any, setter: (val: string) => {}, title: string) => (
+        <div
+            className="inputBox"
+            style={{
+                marginRight: title === 'X:' ? '19px' : '',
+                marginLeft: title === '∠:' ? '39px' : '',
+            }}>
+            <div className="inputBox-title"> {title} </div>
+            <input className="inputBox-input" type="text" value={value} style={{ color: SettingsManager.userColor, backgroundColor: SettingsManager.userBackgroundColor }} onChange={e => setter(e.target.value)} onKeyDown={e => e.stopPropagation()} />
+            <div className="inputBox-button">
+                <div className="inputBox-button-up" key="up2" onPointerDown={undoBatch(action(() => this.upDownButtons('up', key)))}>
+                    <FontAwesomeIcon icon="caret-up" size="sm" />
+                </div>
+                <div className="inputbox-Button-down" key="down2" onPointerDown={undoBatch(action(() => this.upDownButtons('down', key)))}>
+                    <FontAwesomeIcon icon="caret-down" size="sm" />
                 </div>
             </div>
-        );
-    };
+        </div>
+    );
 
-    inputBoxDuo = (key: string, value: any, setter: (val: string) => {}, title1: string, key2: string, value2: any, setter2: (val: string) => {}, title2: string) => {
-        return (
-            <div className="inputBox-duo">
-                {this.inputBox(key, value, setter, title1)}
-                {title2 === '' ? null : this.inputBox(key2, value2, setter2, title2)}
-            </div>
-        );
-    };
+    inputBoxDuo = (key: string, value: any, setter: (val: string) => {}, title1: string, key2: string, value2: any, setter2: (val: string) => {}, title2: string) => (
+        <div className="inputBox-duo">
+            {this.inputBox(key, value, setter, title1)}
+            {title2 === '' ? null : this.inputBox(key2, value2, setter2, title2)}
+        </div>
+    );
 
     @action
     upDownButtons = (dirs: string, field: string) => {
         const selDoc = this.selectedDoc;
         if (!selDoc) return;
-        //prettier-ignore
+        // prettier-ignore
         switch (field) {
             case 'Xps': selDoc.x = NumCast(this.selectedDoc?.x) + (dirs === 'up' ? 10 : -10); break;
             case 'Yps': selDoc.y = NumCast(this.selectedDoc?.y) + (dirs === 'up' ? 10 : -10); break;
             case 'stk': selDoc.stroke_width = NumCast(this.selectedDoc?.[DocData].stroke_width) + (dirs === 'up' ? 0.1 : -0.1); break;
-            case 'wid':
-                const oldWidth = NumCast(selDoc._width);
-                const oldHeight = NumCast(selDoc._height);
-                const oldX = NumCast(selDoc.x);
-                const oldY = NumCast(selDoc.y);
-                selDoc._width = oldWidth + (dirs === 'up' ? 10 : -10);
-                if (selDoc.type === DocumentType.INK && selDoc.x && selDoc.y && selDoc._height && selDoc._width) {
-                    const ink = Cast(selDoc.data, InkField)?.inkData;
-                    if (ink) {
-                        const newPoints: { X: number; Y: number }[] = [];
-                        for (var j = 0; j < ink.length; j++) {
-                            // (new x — oldx) + (oldxpoint * newWidt)/oldWidth
-                            const newX = NumCast(selDoc.x) - oldX + (ink[j].X * NumCast(selDoc._width)) / oldWidth;
-                            const newY = NumCast(selDoc.y) - oldY + (ink[j].Y * NumCast(selDoc._height)) / oldHeight;
-                            newPoints.push({ X: newX, Y: newY });
+            case 'wid': {
+                    const oldWidth = NumCast(selDoc._width);
+                    const oldHeight = NumCast(selDoc._height);
+                    const oldX = NumCast(selDoc.x);
+                    const oldY = NumCast(selDoc.y);
+                    selDoc._width = oldWidth + (dirs === 'up' ? 10 : -10);
+                    if (selDoc.type === DocumentType.INK && selDoc.x && selDoc.y && selDoc._height && selDoc._width) {
+                        const ink = Cast(selDoc.data, InkField)?.inkData;
+                        if (ink) {
+                            const newPoints: { X: number; Y: number }[] = [];
+                            for (let j = 0; j < ink.length; j++) {
+                                // (new x — oldx) + (oldxpoint * newWidt)/oldWidth
+                                const newX = NumCast(selDoc.x) - oldX + (ink[j].X * NumCast(selDoc._width)) / oldWidth;
+                                const newY = NumCast(selDoc.y) - oldY + (ink[j].Y * NumCast(selDoc._height)) / oldHeight;
+                                newPoints.push({ X: newX, Y: newY });
+                            }
+                            selDoc.data = new InkField(newPoints);
                         }
-                        selDoc.data = new InkField(newPoints);
                     }
                 }
                 break;
-            case 'hgt':
-                const oWidth = NumCast(selDoc._width);
-                const oHeight = NumCast(selDoc._height);
-                const oX = NumCast(selDoc.x);
-                const oY = NumCast(selDoc.y);
-                selDoc._height = oHeight + (dirs === 'up' ? 10 : -10);
-                if (selDoc.type === DocumentType.INK && selDoc.x && selDoc.y && selDoc._height && selDoc._width) {
-                    const ink = Cast(selDoc.data, InkField)?.inkData;
-                    if (ink) {
-                        const newPoints: { X: number; Y: number }[] = [];
-                        for (var j = 0; j < ink.length; j++) {
-                            // (new x — oldx) + (oldxpoint * newWidt)/oldWidth
-                            const newX = NumCast(selDoc.x) - oX + (ink[j].X * NumCast(selDoc._width)) / oWidth;
-                            const newY = NumCast(selDoc.y) - oY + (ink[j].Y * NumCast(selDoc._height)) / oHeight;
-                            newPoints.push({ X: newX, Y: newY });
+            case 'hgt': {
+                    const oWidth = NumCast(selDoc._width);
+                    const oHeight = NumCast(selDoc._height);
+                    const oX = NumCast(selDoc.x);
+                    const oY = NumCast(selDoc.y);
+                    selDoc._height = oHeight + (dirs === 'up' ? 10 : -10);
+                    if (selDoc.type === DocumentType.INK && selDoc.x && selDoc.y && selDoc._height && selDoc._width) {
+                        const ink = Cast(selDoc.data, InkField)?.inkData;
+                        if (ink) {
+                            const newPoints: { X: number; Y: number }[] = [];
+                            for (let j = 0; j < ink.length; j++) {
+                                // (new x — oldx) + (oldxpoint * newWidt)/oldWidth
+                                const newX = NumCast(selDoc.x) - oX + (ink[j].X * NumCast(selDoc._width)) / oWidth;
+                                const newY = NumCast(selDoc.y) - oY + (ink[j].Y * NumCast(selDoc._height)) / oHeight;
+                                newPoints.push({ X: newX, Y: newY });
+                            }
+                            selDoc.data = new InkField(newPoints);
                         }
-                        selDoc.data = new InkField(newPoints);
                     }
                 }
                 break;
+            default: { /* empty */ }
         }
     };
 
     getField(key: string) {
-        return Field.toString(this.selectedDoc?.[DocData][key] as Field);
+        return Field.toString(this.selectedDoc?.[DocData][key] as FieldType);
     }
 
-    @computed get shapeXps() {
-        return NumCast(this.selectedDoc?.x);
-    }
-    @computed get shapeYps() {
-        return NumCast(this.selectedDoc?.y);
-    }
-    @computed get shapeHgt() {
-        return NumCast(this.selectedDoc?._height);
-    }
-    @computed get shapeWid() {
-        return NumCast(this.selectedDoc?._width);
-    }
-    @computed get strokeThk() {
-        return NumCast(this.selectedDoc?.[DocData].stroke_width);
-    }
-    set shapeXps(value) {
-        this.selectedDoc && (this.selectedDoc.x = Math.round(value * 100) / 100);
-    }
-    set shapeYps(value) {
-        this.selectedDoc && (this.selectedDoc.y = Math.round(value * 100) / 100);
-    }
-    set shapeWid(value) {
-        this.selectedDoc && (this.selectedDoc._width = Math.round(value * 100) / 100);
-    }
-    set shapeHgt(value) {
-        this.selectedDoc && (this.selectedDoc._height = Math.round(value * 100) / 100);
-    }
-    set strokeThk(value) {
-        this.selectedDoc && (this.selectedDoc[DocData].stroke_width = Math.round(value * 100) / 100);
-    }
+    @computed get shapeXps() { return NumCast(this.selectedDoc?.x); } // prettier-ignore
+    set shapeXps(value)  { this.selectedDoc && (this.selectedDoc.x = Math.round(value * 100) / 100); } // prettier-ignore
+    @computed get shapeYps() { return NumCast(this.selectedDoc?.y); } // prettier-ignore
+    set shapeYps(value)  { this.selectedDoc && (this.selectedDoc.y = Math.round(value * 100) / 100); } // prettier-ignore
+    @computed get shapeWid() { return NumCast(this.selectedDoc?._width); } // prettier-ignore
+    set shapeWid(value)  { this.selectedDoc && (this.selectedDoc._width = Math.round(value * 100) / 100); } // prettier-ignore
+    @computed get shapeHgt() { return NumCast(this.selectedDoc?._height); } // prettier-ignore
+    set shapeHgt(value)  { this.selectedDoc && (this.selectedDoc._height = Math.round(value * 100) / 100); } // prettier-ignore
+    @computed get strokeThk(){ return NumCast(this.selectedDoc?.[DocData].stroke_width); } // prettier-ignore
+    set strokeThk(value) { this.selectedDoc && (this.selectedDoc[DocData].stroke_width = Math.round(value * 100) / 100); } // prettier-ignore
 
     @computed get hgtInput() {
         return this.inputBoxDuo(
             'hgt',
             this.shapeHgt,
-            undoable((val: string) => !isNaN(Number(val)) && (this.shapeHgt = +val), 'set height'),
+            undoable((val: string) => {
+                !Number(val) && (this.shapeHgt = +val);
+            }, 'set height'),
             'H:',
             'wid',
             this.shapeWid,
-            undoable((val: string) => !isNaN(Number(val)) && (this.shapeWid = +val), 'set width'),
+            undoable((val: string) => {
+                !isNaN(Number(val)) && (this.shapeWid = +val);
+            }, 'set width'),
             'W:'
         );
     }
     @computed get XpsInput() {
+        // prettier-ignore
         return this.inputBoxDuo(
             'Xps',
             this.shapeXps,
-            undoable((val: string) => val !== '0' && !isNaN(Number(val)) && (this.shapeXps = +val), 'set x coord'),
+            undoable((val: string) => { val !== '0' && !isNaN(Number(val)) && (this.shapeXps = +val);  }, 'set x coord'),
             'X:',
             'Yps',
             this.shapeYps,
-            undoable((val: string) => val !== '0' && !isNaN(Number(val)) && (this.shapeYps = +val), 'set y coord'),
+            undoable((val: string) => { val !== '0' && !isNaN(Number(val)) && (this.shapeYps = +val); }, 'set y coord'),
             'Y:'
         );
     }
@@ -854,18 +834,10 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
 
     private _lastDash: any = '2';
 
-    @computed get colorFil() {
-        return StrCast(this.selectedDoc?.[DocData].fillColor);
-    }
-    @computed get colorStk() {
-        return StrCast(this.selectedDoc?.[DocData].color);
-    }
-    set colorFil(value) {
-        this.selectedDoc && (this.selectedDoc[DocData].fillColor = value ? value : undefined);
-    }
-    set colorStk(value) {
-        this.selectedDoc && (this.selectedDoc[DocData].color = value ? value : undefined);
-    }
+    @computed get colorFil() { return StrCast(this.selectedDoc?.[DocData].fillColor); } // prettier-ignore
+    set colorFil(value) { this.selectedDoc && (this.selectedDoc[DocData].fillColor = value || undefined); } // prettier-ignore
+    @computed get colorStk() { return StrCast(this.selectedDoc?.[DocData].color); } // prettier-ignore
+    set colorStk(value) { this.selectedDoc && (this.selectedDoc[DocData].color = value || undefined); } // prettier-ignore
 
     colorButton(value: string, type: string, setter: () => void) {
         return (
@@ -886,11 +858,9 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
 
     colorPicker(color: string, setter: (color: string) => void) {
         return (
+            // prettier-ignore
             <SketchPicker
-                onChange={undoable(
-                    action((color: ColorResult) => setter(color.hex)),
-                    'set stroke color property'
-                )}
+                onChange={undoable( action((color: ColorResult) => setter(color.hex)), 'set stroke color property'  )}
                 presetColors={['#D0021B', '#F5A623', '#F8E71C', '#8B572A', '#7ED321', '#417505', '#9013FE', '#4A90E2', '#50E3C2', '#B8E986', '#000000', '#4A4A4A', '#9B9B9B', '#FFFFFF', '#f1efeb', 'transparent']}
                 color={color}
             />
@@ -911,10 +881,10 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
     }
 
     @computed get fillPicker() {
-        return this.colorPicker(this.colorFil, (color: string) => (this.colorFil = color));
+        return this.colorPicker(this.colorFil, (color: string) => { this.colorFil = color;  }); // prettier-ignore
     }
     @computed get linePicker() {
-        return this.colorPicker(this.colorStk, (color: string) => (this.colorStk = color));
+        return this.colorPicker(this.colorStk, (color: string) => { this.colorStk = color; }); // prettier-ignore
     }
 
     @computed get strokeAndFill() {
@@ -936,60 +906,48 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
         );
     }
 
-    @computed get dashdStk() {
-        return this.selectedDoc?.stroke_dash || '';
-    }
-    @computed get widthStk() {
-        return this.getField('stroke_width') || '1';
-    }
-    @computed get markScal() {
-        return Number(this.getField('stroke_markerScale') || '1');
-    }
-    @computed get markHead() {
-        return this.getField('stroke_startMarker') || '';
-    }
-    @computed get markTail() {
-        return this.getField('stroke_endMarker') || '';
-    }
+    @computed get dashdStk() {  return this.selectedDoc?.stroke_dash || ''; } // prettier-ignore
     set dashdStk(value) {
         value && (this._lastDash = value);
         this.selectedDoc && (this.selectedDoc[DocData].stroke_dash = value ? this._lastDash : undefined);
     }
-    set markScal(value) {
-        this.selectedDoc && (this.selectedDoc[DocData].stroke_markerScale = Number(value));
-    }
+    @computed get widthStk() { return this.getField('stroke_width') || '1'; } // prettier-ignore
     set widthStk(value) {
         this.selectedDoc && (this.selectedDoc[DocData].stroke_width = Number(value));
     }
+    @computed get markScal() { return Number(this.getField('stroke_markerScale') || '1'); } // prettier-ignore
+    set markScal(value) {
+        this.selectedDoc && (this.selectedDoc[DocData].stroke_markerScale = Number(value));
+    }
+    @computed get markHead() { return this.getField('stroke_startMarker') || ''; } // prettier-ignore
     set markHead(value) {
         this.selectedDoc && (this.selectedDoc[DocData].stroke_startMarker = value);
     }
+    @computed get markTail() {  return this.getField('stroke_endMarker') || ''; } // prettier-ignore
     set markTail(value) {
         this.selectedDoc && (this.selectedDoc[DocData].stroke_endMarker = value);
     }
 
     @computed get stkInput() {
-        return this.regInput('stk', this.widthStk, (val: string) => (this.widthStk = val));
+        return this.regInput('stk', this.widthStk, (val: string) => { this.widthStk = val; }); // prettier-ignore
     }
     @computed get markScaleInput() {
-        return this.regInput('scale', this.markScal.toString(), (val: string) => (this.markScal = Number(val)));
+        return this.regInput('scale', this.markScal.toString(), (val: string) => { this.markScal = Number(val); }); // prettier-ignore
     }
 
-    regInput = (key: string, value: any, setter: (val: string) => {}) => {
-        return (
-            <div className="inputBox">
-                <input className="inputBox-input" type="text" value={value} style={{ color: SettingsManager.userColor, backgroundColor: SettingsManager.userBackgroundColor }} onChange={e => setter(e.target.value)} />
-                <div className="inputBox-button">
-                    <div className="inputBox-button-up" key="up2" onPointerDown={undoBatch(action(() => this.upDownButtons('up', key)))}>
-                        <FontAwesomeIcon icon="caret-up" size="sm" />
-                    </div>
-                    <div className="inputbox-Button-down" key="down2" onPointerDown={undoBatch(action(() => this.upDownButtons('down', key)))}>
-                        <FontAwesomeIcon icon="caret-down" size="sm" />
-                    </div>
+    regInput = (key: string, value: any, setter: (val: string) => {}) => (
+        <div className="inputBox">
+            <input className="inputBox-input" type="text" value={value} style={{ color: SettingsManager.userColor, backgroundColor: SettingsManager.userBackgroundColor }} onChange={e => setter(e.target.value)} />
+            <div className="inputBox-button">
+                <div className="inputBox-button-up" key="up2" onPointerDown={undoBatch(action(() => this.upDownButtons('up', key)))}>
+                    <FontAwesomeIcon icon="caret-up" size="sm" />
+                </div>
+                <div className="inputbox-Button-down" key="down2" onPointerDown={undoBatch(action(() => this.upDownButtons('down', key)))}>
+                    <FontAwesomeIcon icon="caret-down" size="sm" />
                 </div>
             </div>
-        );
-    };
+        </div>
+    );
 
     @action
     CloseAll = () => {
@@ -1005,18 +963,55 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
 
     @computed get widthAndDash() {
         return (
+            // prettier-ignore
             <div className="widthAndDash">
-                <div className="width">{this.getNumber('Thickness', '', 0, Math.max(50, this.strokeThk), this.strokeThk, (val: number) => !isNaN(val) && (this.strokeThk = val), 50, 1)}</div>
-                <div className="width">{this.getNumber('Arrow Scale', '', 0, Math.max(10, this.markScal), this.markScal, (val: number) => !isNaN(val) && (this.markScal = val), 10, 1)}</div>
+                <div className="width">
+                    {this.getNumber(
+                        'Thickness',
+                        '',
+                        0,
+                        Math.max(50, this.strokeThk),
+                        this.strokeThk,
+                        (val: number) => { !isNaN(val) && (this.strokeThk = val); },
+                        50,
+                        1
+                    )}
+                </div>
+                <div className="width">
+                    {this.getNumber(
+                        'Arrow Scale',
+                        '',
+                        0,
+                        Math.max(10, this.markScal),
+                        this.markScal,
+                        (val: number) => {  !isNaN(val) && (this.markScal = val); },
+                        10,
+                        1
+                    )}
+                </div>
 
                 <div className="arrows">
                     <div className="arrows-head">
                         <div className="arrows-head-title">Arrow Head: </div>
-                        <input key="markHead" className="arrows-head-input" type="checkbox" checked={this.markHead !== ''} onChange={undoBatch(action(() => (this.markHead = this.markHead ? '' : 'arrow')))} />
+                        <input
+                            key="markHead"
+                            className="arrows-head-input"
+                            type="checkbox"
+                            checked={this.markHead !== ''}
+                            onChange={undoBatch(action(() => {  this.markHead = this.markHead ? '' : 'arrow';  }))}
+                        />
                     </div>
                     <div className="arrows-tail">
                         <div className="arrows-tail-title">Arrow End: </div>
-                        <input key="markTail" className="arrows-tail-input" type="checkbox" checked={this.markTail !== ''} onChange={undoBatch(action(() => (this.markTail = this.markTail ? '' : 'arrow')))} />
+                        <input
+                            key="markTail"
+                            className="arrows-tail-input"
+                            type="checkbox"
+                            checked={this.markTail !== ''}
+                            onChange={undoBatch(
+                                action(() => { this.markTail = this.markTail ? '' : 'arrow'; })
+                            )}
+                        />
                     </div>
                 </div>
                 <div className="dashed">
@@ -1045,50 +1040,53 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
     setFinalNumber = () => {
         this._sliderBatch?.end();
     };
-    getNumber = (label: string, unit: string, min: number, max: number, number: number, setNumber: any, autorange?: number, autorangeMinVal?: number) => {
-        return (
-            <div key={label + this.selectedDoc?.title}>
-                <NumberInput formLabel={label} formLabelPlacement={'left'} type={Type.SEC} unit={unit} fillWidth color={this.color} number={number} setNumber={setNumber} min={min} max={max} />
-                <Slider
-                    key={label}
-                    onPointerDown={e => (this._sliderBatch = UndoManager.StartBatch('slider ' + label))}
-                    multithumb={false}
-                    color={this.color}
-                    size={Size.XSMALL}
-                    min={min}
-                    max={max}
-                    autorangeMinVal={autorangeMinVal}
-                    autorange={autorange}
-                    number={number}
-                    unit={unit}
-                    decimals={1}
-                    setFinalNumber={this.setFinalNumber}
-                    setNumber={setNumber}
-                    fillWidth
-                />
-            </div>
-        );
-    };
+    getNumber = (label: string, unit: string, min: number, max: number, number: number, setNumber: any, autorange?: number, autorangeMinVal?: number) => (
+        <div key={label + (this.selectedDoc?.title ?? '')}>
+            <NumberInput formLabel={label} formLabelPlacement="left" type={Type.SEC} unit={unit} fillWidth color={this.color} number={number} setNumber={setNumber} min={min} max={max} />
+            <Slider
+                key={label}
+                onPointerDown={() => {
+                    this._sliderBatch = UndoManager.StartBatch('slider ' + label);
+                }}
+                multithumb={false}
+                color={this.color}
+                size={Size.XSMALL}
+                min={min}
+                max={max}
+                autorangeMinVal={autorangeMinVal}
+                autorange={autorange}
+                number={number}
+                unit={unit}
+                decimals={1}
+                setFinalNumber={this.setFinalNumber}
+                setNumber={setNumber}
+                fillWidth
+            />
+        </div>
+    );
 
+    setVal = (func: (doc: Doc, val: number) => void) => (val: number) => this.selectedDoc && !isNaN(val) && func(this.selectedDoc, val);
     @computed get transformEditor() {
         return (
+            // prettier-ignore
             <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.isStack ? null : this.getNumber('Gap', ' px', 0, 200, NumCast(this.selectedDoc!.gridGap), this.setVal((doc: Doc, val: number) => { doc.gridGap = val; })) }
+                {!this.isStack ? null : this.getNumber('xMargin', ' px', 0, 500, NumCast(this.selectedDoc!.xMargin), this.setVal((doc: Doc, val: number) => { doc.xMargin = val; })) }
+                {!this.isStack ? null : this.getNumber('yMargin', ' px', 0, 500, NumCast(this.selectedDoc!.yMargin), this.setVal((doc: Doc, val: number) => { doc.yMargin = val; })) }
+                {!this.isGroup ? null : this.getNumber('Padding', ' px', 0, 500, NumCast(this.selectedDoc!.xPadding), this.setVal((doc: Doc, val: number) => { doc.xPadding = doc.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)}
-                {this.getNumber('Height', ' px', 0, Math.max(1000, this.shapeHgt), this.shapeHgt, (val: number) => !isNaN(val) && (this.shapeHgt = val), 1000, 1)}
-                {this.getNumber('X', ' px', this.shapeXps - 500, this.shapeXps + 500, this.shapeXps, (val: number) => !isNaN(val) && (this.shapeXps = val), 1000)}
-                {this.getNumber('Y', ' px', this.shapeYps - 500, this.shapeYps + 500, this.shapeYps, (val: number) => !isNaN(val) && (this.shapeYps = val), 1000)}
+                {this.getNumber('Width', ' px', 0, Math.max(1000, this.shapeWid), this.shapeWid, this.setVal((doc: Doc, val:number) => {this.shapeWid = val}), 1000, 1)}
+                {this.getNumber('Height', ' px', 0, Math.max(1000, this.shapeHgt), this.shapeHgt, this.setVal((doc: Doc, val:number) => {this.shapeHgt = val}), 1000, 1)}
+                {this.getNumber('X', ' px', this.shapeXps - 500, this.shapeXps + 500, this.shapeXps, this.setVal((doc: Doc, val:number) =>  {this.shapeXps = val}), 1000)}
+                {this.getNumber('Y', ' px', this.shapeYps - 500, this.shapeYps + 500, this.shapeYps, this.setVal((doc: Doc, val:number) => {this.shapeYps = val}), 1000)}
             </div>
         );
     }
 
     @computed get optionsSubMenu() {
         return (
-            <PropertiesSection title="Options" inSection={this.inOptions} isOpen={this.openOptions} setInSection={bool => (this.inOptions = bool)} setIsOpen={bool => (this.openOptions = bool)} onDoubleClick={this.CloseAll}>
+            // prettier-ignore
+            <PropertiesSection title="Options" inSection={this.inOptions} isOpen={this.openOptions} setInSection={bool => { this.inOptions = bool; }} setIsOpen={bool => { this.openOptions = bool; }} onDoubleClick={this.CloseAll}>
                 <PropertiesButtons />
             </PropertiesSection>
         );
@@ -1096,12 +1094,23 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
 
     @computed get sharingSubMenu() {
         return (
-            <PropertiesSection title="Sharing and Permissions" isOpen={this.openSharing} setIsOpen={bool => (this.openSharing = bool)} onDoubleClick={() => this.CloseAll()}>
+            // prettier-ignore
+            <PropertiesSection
+                title="Sharing and Permissions"
+                isOpen={this.openSharing}
+                setIsOpen={bool => { this.openSharing = bool;  }}
+                onDoubleClick={this.CloseAll}>
                 <>
                     {/* <div className="propertiesView-buttonContainer"> */}
                     <div className="propertiesView-acls-checkbox">
                         Layout Permissions
-                        <Checkbox color="primary" onChange={action(() => (this.layoutDocAcls = !this.layoutDocAcls))} checked={this.layoutDocAcls} />
+                        <Checkbox
+                            color="primary"
+                            onChange={action(() => {
+                                this.layoutDocAcls = !this.layoutDocAcls;
+                            })}
+                            checked={this.layoutDocAcls}
+                        />
                     </div>
                     {/* <Tooltip title={<><div className="dash-tooltip">{"Re-distribute sharing settings"}</div></>}>
                     <button onPointerDown={() => SharingManager.Instance.distributeOverCollection(this.selectedDoc!)}>
@@ -1141,7 +1150,8 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
 
     @computed get filtersSubMenu() {
         return (
-            <PropertiesSection title="Filters" isOpen={this.openFilters} setIsOpen={bool => (this.openFilters = bool)} onDoubleClick={() => this.CloseAll()}>
+            // prettier-ignore
+            <PropertiesSection title="Filters" isOpen={this.openFilters} setIsOpen={bool => { this.openFilters = bool; }} onDoubleClick={this.CloseAll}>
                 <div className="propertiesView-content filters" style={{ position: 'relative', height: 'auto' }}>
                     <FilterPanel Document={this.selectedDoc ?? Doc.ActiveDashboard!} />
                 </div>
@@ -1151,11 +1161,12 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
 
     @computed get inkSubMenu() {
         return (
+            // prettier-ignore
             <>
-                <PropertiesSection title="Appearance" isOpen={this.openAppearance} setIsOpen={bool => (this.openAppearance = bool)} onDoubleClick={() => this.CloseAll()}>
+                <PropertiesSection title="Appearance" isOpen={this.openAppearance} setIsOpen={bool => { this.openAppearance = bool; }} onDoubleClick={this.CloseAll}>
                     {this.selectedLayoutDoc?.layout_isSvg ? this.appearanceEditor : null}
                 </PropertiesSection>
-                <PropertiesSection title="Transform" isOpen={this.openTransform} setIsOpen={bool => (this.openTransform = bool)} onDoubleClick={() => this.CloseAll()}>
+                <PropertiesSection title="Transform" isOpen={this.openTransform} setIsOpen={bool => { this.openTransform = bool; }} onDoubleClick={this.CloseAll}>
                     {this.transformEditor}
                 </PropertiesSection>
             </>
@@ -1164,7 +1175,13 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
 
     @computed get fieldsSubMenu() {
         return (
-            <PropertiesSection title="Fields & Tags" isOpen={this.openFields} setIsOpen={bool => (this.openFields = bool)} onDoubleClick={() => this.CloseAll()}>
+            <PropertiesSection
+                title="Fields & Tags"
+                isOpen={this.openFields}
+                setIsOpen={bool => {
+                    this.openFields = bool;
+                }}
+                onDoubleClick={this.CloseAll}>
                 <div className="propertiesView-content fields">{Doc.noviceMode ? this.noviceFields : this.expandedField}</div>
             </PropertiesSection>
         );
@@ -1172,7 +1189,13 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
 
     @computed get contextsSubMenu() {
         return (
-            <PropertiesSection title="Other Contexts" isOpen={this.openContexts} setIsOpen={bool => (this.openContexts = bool)} onDoubleClick={() => this.CloseAll()}>
+            <PropertiesSection
+                title="Other Contexts"
+                isOpen={this.openContexts}
+                setIsOpen={bool => {
+                    this.openContexts = bool;
+                }}
+                onDoubleClick={this.CloseAll}>
                 {this.contextCount > 0 ? this.contexts : 'There are no other contexts.'}
             </PropertiesSection>
         );
@@ -1180,7 +1203,13 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
 
     @computed get linksSubMenu() {
         return (
-            <PropertiesSection title="Linked To" isOpen={this.openLinks} setIsOpen={bool => (this.openLinks = bool)} onDoubleClick={this.CloseAll}>
+            <PropertiesSection
+                title="Linked To"
+                isOpen={this.openLinks}
+                setIsOpen={bool => {
+                    this.openLinks = bool;
+                }}
+                onDoubleClick={this.CloseAll}>
                 {this.linkCount > 0 ? this.links : 'There are no current links.'}
             </PropertiesSection>
         );
@@ -1188,14 +1217,20 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
 
     @computed get layoutSubMenu() {
         return (
-            <PropertiesSection title="Layout" isOpen={this.openLayout} setIsOpen={bool => (this.openLayout = bool)} onDoubleClick={this.CloseAll}>
+            <PropertiesSection
+                title="Layout"
+                isOpen={this.openLayout}
+                setIsOpen={bool => {
+                    this.openLayout = bool;
+                }}
+                onDoubleClick={this.CloseAll}>
                 {this.layoutPreview}
             </PropertiesSection>
         );
     }
 
     @computed get description() {
-        return Field.toString(this.selectedLink?.link_description as any as Field);
+        return Field.toString(this.selectedLink?.link_description as any as FieldType);
     }
     @computed get relationship() {
         return StrCast(this.selectedLink?.link_relationship);
@@ -1250,12 +1285,12 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
                 // if the relationship is already in the list AND the new rel is different from the prev rel, update the rel sizes
             } else if (linkRelationshipList && value !== prevRelationship) {
                 const index = linkRelationshipList.indexOf(value);
-                //increment size of new relationship size
+                // increment size of new relationship size
                 if (index !== -1 && index < linkRelationshipSizes.length) {
                     const pvalue = linkRelationshipSizes[index];
                     linkRelationshipSizes[index] = pvalue === undefined || !Number.isFinite(pvalue) ? 1 : pvalue + 1;
                 }
-                //decrement the size of the previous relationship if it already exists (i.e. not default 'link' relationship upon link creation)
+                // decrement the size of the previous relationship if it already exists (i.e. not default 'link' relationship upon link creation)
                 if (linkRelationshipList.includes(prevRelationship)) {
                     const pindex = linkRelationshipList.indexOf(prevRelationship);
                     if (pindex !== -1 && pindex < linkRelationshipSizes.length) {
@@ -1265,21 +1300,25 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
                 }
             }
             this.relationshipButtonColor = 'rgb(62, 133, 55)';
-            setTimeout(
-                action(() => (this.relationshipButtonColor = '')),
-                750
-            );
+            setTimeout(action(() => { this.relationshipButtonColor = ''; }),  750); // prettier-ignore
             return true;
         }
+        return undefined;
     });
 
-    changeFollowBehavior = undoable((loc: Opt<string>) => this.sourceAnchor && (this.sourceAnchor.followLinkLocation = loc), 'change follow behavior');
+    changeFollowBehavior = undoable((loc: Opt<string>) => {
+        this.sourceAnchor && (this.sourceAnchor.followLinkLocation = loc);
+    }, 'change follow behavior');
 
     @undoBatch
-    changeAnimationBehavior = action((behavior: string) => this.sourceAnchor && (this.sourceAnchor.followLinkAnimEffect = behavior));
+    changeAnimationBehavior = action((behavior: string) => {
+        this.sourceAnchor && (this.sourceAnchor.followLinkAnimEffect = behavior);
+    });
 
     @undoBatch
-    changeEffectDirection = action((effect: PresEffectDirection) => this.sourceAnchor && (this.sourceAnchor.followLinkAnimDirection = effect));
+    changeEffectDirection = action((effect: PresEffectDirection) => {
+        this.sourceAnchor && (this.sourceAnchor.followLinkAnimDirection = effect);
+    });
 
     animationDirection = (direction: PresEffectDirection, icon: string, gridColumn: number, gridRow: number, opts: object) => {
         const lanch = this.sourceAnchor;
@@ -1300,7 +1339,7 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
         document.getElementById('link_description_input')?.blur();
     };
 
-    onDescriptionKey = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
+    onDescriptionKey = () => {
         // if (e.key === 'Enter') {
         //     this.setDescripValue(this.description);
         //     document.getElementById('link_description_input')?.blur();
@@ -1320,7 +1359,13 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
     };
 
     toggleLinkProp = (e: React.PointerEvent, prop: string) => {
-        setupMoveUpEvents(this, e, returnFalse, emptyFunction, undoBatch(action(() => this.selectedLink && (this.selectedLink[prop] = !this.selectedLink[prop]))));
+        setupMoveUpEvents(
+            this,
+            e,
+            returnFalse,
+            emptyFunction,
+            undoBatch(action(() => { this.selectedLink && (this.selectedLink[prop] = !this.selectedLink[prop]); })) // prettier-ignore
+        );
     };
 
     @computed get destinationAnchor() {
@@ -1343,12 +1388,10 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
                 e,
                 returnFalse,
                 emptyFunction,
-                undoBatch(
-                    action(() => {
-                        anchor[prop] = anchor[prop] === value ? ovalue : value;
-                        this.selectedDoc && cb(anchor[prop]);
-                    })
-                )
+                undoBatch(action(() => {
+                    anchor[prop] = anchor[prop] === value ? ovalue : value;
+                    this.selectedDoc && cb(anchor[prop]);
+                })) // prettier-ignore
             );
     };
 
@@ -1357,7 +1400,7 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
         return (
             <input
                 style={{ color: SettingsManager.userColor, backgroundColor: SettingsManager.userBackgroundColor }}
-                autoComplete={'off'}
+                autoComplete="off"
                 id="link_relationship_input"
                 value={StrCast(this.selectedLink?.link_relationship)}
                 onKeyDown={this.onRelationshipKey}
@@ -1459,11 +1502,14 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
                             '10',
                             NumCast(this.sourceAnchor?.followLinkTransitionTime) / 1000,
                             true,
-                            (val: string) => PresBox.SetTransitionTime(val, (timeInMS: number) => this.sourceAnchor && (this.sourceAnchor.followLinkTransitionTime = timeInMS)),
+                            (val: string) =>
+                                PresBox.SetTransitionTime(val, (timeInMS: number) => {
+                                    this.sourceAnchor && (this.sourceAnchor.followLinkTransitionTime = timeInMS);
+                                }),
                             indent
                         )}{' '}
                         <div
-                            className={'slider-headers'}
+                            className="slider-headers"
                             style={{
                                 display: 'grid',
                                 justifyContent: 'space-between',
@@ -1478,86 +1524,118 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
                         </div>{' '}
                         <div className="propertiesView-input inline">
                             <p>Play Target Audio</p>
-                            <button
-                                style={{ background: !this.sourceAnchor?.followLinkAudio ? '' : '#4476f7', borderRadius: 3 }}
-                                onPointerDown={e => this.toggleAnchorProp(e, 'followLinkAudio', this.sourceAnchor)}
-                                onClick={e => e.stopPropagation()}
-                                className="propertiesButton">
-                                <FontAwesomeIcon className="fa-icon" icon={faAnchor as IconLookup} size="lg" />
-                            </button>
+                            {
+                                // eslint-disable-next-line jsx-a11y/control-has-associated-label
+                                <button
+                                    type="button"
+                                    style={{ background: !this.sourceAnchor?.followLinkAudio ? '' : '#4476f7', borderRadius: 3 }}
+                                    onPointerDown={e => this.toggleAnchorProp(e, 'followLinkAudio', this.sourceAnchor)}
+                                    onClick={e => e.stopPropagation()}
+                                    className="propertiesButton">
+                                    <FontAwesomeIcon className="fa-icon" icon={faAnchor as IconLookup} size="lg" />
+                                </button>
+                            }
                         </div>
                         <div className="propertiesView-input inline">
                             <p>Play Target Video</p>
-                            <button
-                                style={{ background: !this.sourceAnchor?.followLinkVideo ? '' : '#4476f7', borderRadius: 3 }}
-                                onPointerDown={e => this.toggleAnchorProp(e, 'followLinkVideo', this.sourceAnchor)}
-                                onClick={e => e.stopPropagation()}
-                                className="propertiesButton">
-                                <FontAwesomeIcon className="fa-icon" icon={faAnchor as IconLookup} size="lg" />
-                            </button>
+                            {
+                                // eslint-disable-next-line jsx-a11y/control-has-associated-label
+                                <button
+                                    type="button"
+                                    style={{ background: !this.sourceAnchor?.followLinkVideo ? '' : '#4476f7', borderRadius: 3 }}
+                                    onPointerDown={e => this.toggleAnchorProp(e, 'followLinkVideo', this.sourceAnchor)}
+                                    onClick={e => e.stopPropagation()}
+                                    className="propertiesButton">
+                                    <FontAwesomeIcon className="fa-icon" icon={faAnchor as IconLookup} size="lg" />
+                                </button>
+                            }
                         </div>
                         <div className="propertiesView-input inline">
                             <p>Zoom Text Selections</p>
-                            <button
-                                style={{ background: !this.sourceAnchor?.followLinkZoomText ? '' : '#4476f7', borderRadius: 3 }}
-                                onPointerDown={e => this.toggleAnchorProp(e, 'followLinkZoomText', this.sourceAnchor)}
-                                onClick={e => e.stopPropagation()}
-                                className="propertiesButton">
-                                <FontAwesomeIcon className="fa-icon" icon={faAnchor as IconLookup} size="lg" />
-                            </button>
+                            {
+                                // eslint-disable-next-line jsx-a11y/control-has-associated-label
+                                <button
+                                    type="button"
+                                    style={{ background: !this.sourceAnchor?.followLinkZoomText ? '' : '#4476f7', borderRadius: 3 }}
+                                    onPointerDown={e => this.toggleAnchorProp(e, 'followLinkZoomText', this.sourceAnchor)}
+                                    onClick={e => e.stopPropagation()}
+                                    className="propertiesButton">
+                                    <FontAwesomeIcon className="fa-icon" icon={faAnchor as IconLookup} size="lg" />
+                                </button>
+                            }
                         </div>
                         <div className="propertiesView-input inline">
                             <p>Toggle Follow to Outer Context</p>
-                            <button
-                                style={{ background: !this.sourceAnchor?.followLinkToOuterContext ? '' : '#4476f7', borderRadius: 3 }}
-                                onPointerDown={e => this.toggleAnchorProp(e, 'followLinkToOuterContext', this.sourceAnchor)}
-                                onClick={e => e.stopPropagation()}
-                                className="propertiesButton">
-                                <FontAwesomeIcon className="fa-icon" icon={faWindowMaximize as IconLookup} size="lg" />
-                            </button>
+                            {
+                                // eslint-disable-next-line jsx-a11y/control-has-associated-label
+                                <button
+                                    type="button"
+                                    style={{ background: !this.sourceAnchor?.followLinkToOuterContext ? '' : '#4476f7', borderRadius: 3 }}
+                                    onPointerDown={e => this.toggleAnchorProp(e, 'followLinkToOuterContext', this.sourceAnchor)}
+                                    onClick={e => e.stopPropagation()}
+                                    className="propertiesButton">
+                                    <FontAwesomeIcon className="fa-icon" icon={faWindowMaximize as IconLookup} size="lg" />
+                                </button>
+                            }
                         </div>
                         <div className="propertiesView-input inline">
                             <p>Toggle Target (Show/Hide)</p>
-                            <button
-                                style={{ background: !this.sourceAnchor?.followLinkToggle ? '' : '#4476f7', borderRadius: 3 }}
-                                onPointerDown={e => this.toggleAnchorProp(e, 'followLinkToggle', this.sourceAnchor)}
-                                onClick={e => e.stopPropagation()}
-                                className="propertiesButton">
-                                <FontAwesomeIcon className="fa-icon" icon={faAnchor as IconLookup} size="lg" />
-                            </button>
+                            {
+                                // eslint-disable-next-line jsx-a11y/control-has-associated-label
+                                <button
+                                    type="button"
+                                    style={{ background: !this.sourceAnchor?.followLinkToggle ? '' : '#4476f7', borderRadius: 3 }}
+                                    onPointerDown={e => this.toggleAnchorProp(e, 'followLinkToggle', this.sourceAnchor)}
+                                    onClick={e => e.stopPropagation()}
+                                    className="propertiesButton">
+                                    <FontAwesomeIcon className="fa-icon" icon={faAnchor as IconLookup} size="lg" />
+                                </button>
+                            }
                         </div>
                         <div className="propertiesView-input inline">
                             <p>Ease Transitions</p>
-                            <button
-                                style={{ background: this.sourceAnchor?.followLinkEase === 'linear' ? '' : '#4476f7', borderRadius: 3 }}
-                                onPointerDown={e => this.toggleAnchorProp(e, 'followLinkEase', this.sourceAnchor, 'ease', 'linear')}
-                                onClick={e => e.stopPropagation()}
-                                className="propertiesButton">
-                                <FontAwesomeIcon className="fa-icon" icon={faAnchor as IconLookup} size="lg" />
-                            </button>
+                            {
+                                // eslint-disable-next-line jsx-a11y/control-has-associated-label
+                                <button
+                                    type="button"
+                                    style={{ background: this.sourceAnchor?.followLinkEase === 'linear' ? '' : '#4476f7', borderRadius: 3 }}
+                                    onPointerDown={e => this.toggleAnchorProp(e, 'followLinkEase', this.sourceAnchor, 'ease', 'linear')}
+                                    onClick={e => e.stopPropagation()}
+                                    className="propertiesButton">
+                                    <FontAwesomeIcon className="fa-icon" icon={faAnchor as IconLookup} size="lg" />
+                                </button>
+                            }
                         </div>
                         <div className="propertiesView-input inline">
                             <p>Capture Offset to Target</p>
-                            <button
-                                style={{ background: this.sourceAnchor?.followLinkXoffset === undefined ? '' : '#4476f7', borderRadius: 3 }}
-                                onPointerDown={e => {
-                                    this.toggleAnchorProp(e, 'followLinkXoffset', this.sourceAnchor, NumCast(this.destinationAnchor?.x) - NumCast(this.sourceAnchor?.x), undefined);
-                                    this.toggleAnchorProp(e, 'followLinkYoffset', this.sourceAnchor, NumCast(this.destinationAnchor?.y) - NumCast(this.sourceAnchor?.y), undefined);
-                                }}
-                                onClick={e => e.stopPropagation()}
-                                className="propertiesButton">
-                                <FontAwesomeIcon className="fa-icon" icon={faAnchor as IconLookup} size="lg" />
-                            </button>
+                            {
+                                // eslint-disable-next-line jsx-a11y/control-has-associated-label
+                                <button
+                                    type="button"
+                                    style={{ background: this.sourceAnchor?.followLinkXoffset === undefined ? '' : '#4476f7', borderRadius: 3 }}
+                                    onPointerDown={e => {
+                                        this.toggleAnchorProp(e, 'followLinkXoffset', this.sourceAnchor, NumCast(this.destinationAnchor?.x) - NumCast(this.sourceAnchor?.x), undefined);
+                                        this.toggleAnchorProp(e, 'followLinkYoffset', this.sourceAnchor, NumCast(this.destinationAnchor?.y) - NumCast(this.sourceAnchor?.y), undefined);
+                                    }}
+                                    onClick={e => e.stopPropagation()}
+                                    className="propertiesButton">
+                                    <FontAwesomeIcon className="fa-icon" icon={faAnchor as IconLookup} size="lg" />
+                                </button>
+                            }
                         </div>
                         <div className="propertiesView-input inline">
                             <p>Center Target (no zoom)</p>
-                            <button
-                                style={{ background: this.sourceAnchor?.followLinkZoom ? '' : '#4476f7', borderRadius: 3 }}
-                                onPointerDown={e => this.toggleAnchorProp(e, 'followLinkZoom', this.sourceAnchor)}
-                                onClick={e => e.stopPropagation()}
-                                className="propertiesButton">
-                                <FontAwesomeIcon className="fa-icon" icon={faAnchor as IconLookup} size="lg" />
-                            </button>
+                            {
+                                // eslint-disable-next-line jsx-a11y/control-has-associated-label
+                                <button
+                                    type="button"
+                                    style={{ background: this.sourceAnchor?.followLinkZoom ? '' : '#4476f7', borderRadius: 3 }}
+                                    onPointerDown={e => this.toggleAnchorProp(e, 'followLinkZoom', this.sourceAnchor)}
+                                    onClick={e => e.stopPropagation()}
+                                    className="propertiesButton">
+                                    <FontAwesomeIcon className="fa-icon" icon={faAnchor as IconLookup} size="lg" />
+                                </button>
+                            }
                         </div>
                         <div className="propertiesView-input inline" style={{ display: 'grid', gridTemplateColumns: '78px calc(100% - 108px) 50px' }}>
                             <p>Zoom %</p>
@@ -1565,24 +1643,28 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
                                 <input className="presBox-input" style={{ width: '100%', color: SettingsManager.userColor, backgroundColor: SettingsManager.userBackgroundColor }} readOnly={true} type="number" value={zoom} />
                                 <div className="ribbon-propertyUpDown" style={{ display: 'flex', flexDirection: 'column' }}>
                                     <div className="ribbon-propertyUpDownItem" onClick={undoBatch(() => this.setZoom(String(zoom), 0.1))}>
-                                        <FontAwesomeIcon icon={'caret-up'} />
+                                        <FontAwesomeIcon icon="caret-up" />
                                     </div>
                                     <div className="ribbon-propertyUpDownItem" onClick={undoBatch(() => this.setZoom(String(zoom), -0.1))}>
-                                        <FontAwesomeIcon icon={'caret-down'} />
+                                        <FontAwesomeIcon icon="caret-down" />
                                     </div>
                                 </div>
                             </div>
-                            <button
-                                style={{ background: !targZoom || this.sourceAnchor?.followLinkZoomScale === 0 ? '' : '#4476f7', borderRadius: 3, gridColumn: 3 }}
-                                onPointerDown={e => this.toggleAnchorProp(e, 'followLinkZoom', this.sourceAnchor)}
-                                onClick={e => e.stopPropagation()}
-                                className="propertiesButton">
-                                <FontAwesomeIcon className="fa-icon" icon={faArrowRight as IconLookup} size="lg" />
-                            </button>
+                            {
+                                // eslint-disable-next-line jsx-a11y/control-has-associated-label
+                                <button
+                                    type="button"
+                                    style={{ background: !targZoom || this.sourceAnchor?.followLinkZoomScale === 0 ? '' : '#4476f7', borderRadius: 3, gridColumn: 3 }}
+                                    onPointerDown={e => this.toggleAnchorProp(e, 'followLinkZoom', this.sourceAnchor)}
+                                    onClick={e => e.stopPropagation()}
+                                    className="propertiesButton">
+                                    <FontAwesomeIcon className="fa-icon" icon={faArrowRight as IconLookup} size="lg" />
+                                </button>
+                            }
                         </div>
                         {!targZoom ? null : PresBox.inputter('0', '1', '100', zoom, true, this.setZoom, 30)}
                         <div
-                            className={'slider-headers'}
+                            className="slider-headers"
                             style={{
                                 display: !targZoom ? 'none' : 'grid',
                                 justifyContent: 'space-between',
@@ -1594,7 +1676,7 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
                             }}>
                             <div className="slider-text">0%</div>
                             <div className="slider-text">100%</div>
-                        </div>{' '}
+                        </div>
                     </div>
                 )}
             </>
@@ -1622,128 +1704,136 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
                     </div>
                 </div>
             );
-        } else {
-            if (this.selectedDoc && !this.isPres) {
-                return (
-                    <div
-                        className="propertiesView"
-                        style={{
-                            background: SettingsManager.userBackgroundColor,
-                            color: SettingsManager.userColor,
-                            width: this._props.width,
-                            minWidth: this._props.width,
-                        }}>
-                        <div className="propertiesView-propAndInfoGrouping">
-                            <div className="propertiesView-sectionTitle" style={{ width: this._props.width }}>
-                                Properties
-                                <div className="propertiesView-info" onClick={() => window.open('https://brown-dash.github.io/Dash-Documentation/properties')}>
-                                    <IconButton icon={<FontAwesomeIcon icon="info-circle" />} color={SettingsManager.userColor} />
-                                </div>
+        }
+        if (this.selectedDoc && !this.isPres) {
+            return (
+                <div
+                    className="propertiesView"
+                    style={{
+                        background: SettingsManager.userBackgroundColor,
+                        color: SettingsManager.userColor,
+                        width: this._props.width,
+                        minWidth: this._props.width,
+                    }}>
+                    <div className="propertiesView-propAndInfoGrouping">
+                        <div className="propertiesView-sectionTitle" style={{ width: this._props.width }}>
+                            Properties
+                            <div className="propertiesView-info" onClick={() => window.open('https://brown-dash.github.io/Dash-Documentation/properties')}>
+                                <IconButton icon={<FontAwesomeIcon icon="info-circle" />} color={SettingsManager.userColor} />
                             </div>
                         </div>
+                    </div>
 
-                        <div className="propertiesView-name">{this.editableTitle}</div>
-                        <div className="propertiesView-type"> {this.currentType} </div>
-                        {this.fieldsSubMenu}
-                        {this.optionsSubMenu}
-                        {this.linksSubMenu}
-                        {!this.selectedLink || !this.openLinks ? null : this.linkProperties}
-                        {this.inkSubMenu}
-                        {this.contextsSubMenu}
-                        {isNovice ? null : this.sharingSubMenu}
-                        {this.filtersSubMenu}
-                        {isNovice ? null : this.layoutSubMenu}
+                    <div className="propertiesView-name">{this.editableTitle}</div>
+                    <div className="propertiesView-type"> {this.currentType} </div>
+                    {this.fieldsSubMenu}
+                    {this.optionsSubMenu}
+                    {this.linksSubMenu}
+                    {!this.selectedLink || !this.openLinks ? null : this.linkProperties}
+                    {this.inkSubMenu}
+                    {this.contextsSubMenu}
+                    {isNovice ? null : this.sharingSubMenu}
+                    {this.filtersSubMenu}
+                    {isNovice ? null : this.layoutSubMenu}
+                </div>
+            );
+        }
+        if (this.isPres && PresBox.Instance) {
+            const selectedItem: boolean = PresBox.Instance.selectedArray.size > 0;
+            const type = [DocumentType.AUDIO, DocumentType.VID].includes(DocCast(PresBox.Instance.activeItem?.annotationOn)?.type as any as DocumentType)
+                ? (DocCast(PresBox.Instance.activeItem?.annotationOn)?.type as any as DocumentType)
+                : PresBox.targetRenderedDoc(PresBox.Instance.activeItem)?.type;
+            return (
+                <div className="propertiesView" style={{ width: this._props.width }}>
+                    <div className="propertiesView-sectionTitle" style={{ width: this._props.width }}>
+                        Presentation
                     </div>
-                );
-            }
-            if (this.isPres && PresBox.Instance) {
-                const selectedItem: boolean = PresBox.Instance.selectedArray.size > 0;
-                const type = [DocumentType.AUDIO, DocumentType.VID].includes(DocCast(PresBox.Instance.activeItem?.annotationOn)?.type as any as DocumentType)
-                    ? (DocCast(PresBox.Instance.activeItem?.annotationOn)?.type as any as DocumentType)
-                    : PresBox.targetRenderedDoc(PresBox.Instance.activeItem)?.type;
-                return (
-                    <div className="propertiesView" style={{ width: this._props.width }}>
-                        <div className="propertiesView-sectionTitle" style={{ width: this._props.width }}>
-                            Presentation
-                        </div>
-                        <div className="propertiesView-name" style={{ borderBottom: 0 }}>
-                            {this.editableTitle}
-                            <div className="propertiesView-presSelected">
-                                <div className="propertiesView-selectedCount">{PresBox.Instance.selectedArray.size} selected</div>
-                                <div className="propertiesView-selectedList">{PresBox.Instance.listOfSelected}</div>
-                            </div>
+                    <div className="propertiesView-name" style={{ borderBottom: 0 }}>
+                        {this.editableTitle}
+                        <div className="propertiesView-presSelected">
+                            <div className="propertiesView-selectedCount">{PresBox.Instance.selectedArray.size} selected</div>
+                            <div className="propertiesView-selectedList">{PresBox.Instance.listOfSelected}</div>
                         </div>
-                        {!selectedItem ? null : (
-                            <div className="propertiesView-presentationTrails">
-                                <div
-                                    className="propertiesView-presentationTrails-title"
-                                    onPointerDown={action(() => (this.openPresTransitions = !this.openPresTransitions))}
-                                    style={{
-                                        color: SettingsManager.userColor,
-                                        backgroundColor: this.openPresTransitions ? SettingsManager.userVariantColor : SettingsManager.userBackgroundColor,
-                                    }}>
-                                    &nbsp; <FontAwesomeIcon style={{ alignSelf: 'center' }} icon={'rocket'} /> &nbsp; Transitions
-                                    <div className="propertiesView-presentationTrails-title-icon">
-                                        <FontAwesomeIcon icon={this.openPresTransitions ? 'caret-down' : 'caret-right'} size="lg" />
-                                    </div>
+                    </div>
+                    {!selectedItem ? null : (
+                        <div className="propertiesView-presentationTrails">
+                            <div
+                                className="propertiesView-presentationTrails-title"
+                                onPointerDown={action(() => {
+                                    this.openPresTransitions = !this.openPresTransitions;
+                                })}
+                                style={{
+                                    color: SettingsManager.userColor,
+                                    backgroundColor: this.openPresTransitions ? SettingsManager.userVariantColor : SettingsManager.userBackgroundColor,
+                                }}>
+                                &nbsp; <FontAwesomeIcon style={{ alignSelf: 'center' }} icon={'rocket'} /> &nbsp; Transitions
+                                <div className="propertiesView-presentationTrails-title-icon">
+                                    <FontAwesomeIcon icon={this.openPresTransitions ? 'caret-down' : 'caret-right'} size="lg" />
                                 </div>
-                                {this.openPresTransitions ? <div className="propertiesView-presentationTrails-content">{PresBox.Instance.transitionDropdown}</div> : null}
                             </div>
-                        )}
-                        {!selectedItem ? null : (
-                            <div className="propertiesView-presentationTrails">
-                                <div
-                                    className="propertiesView-presentationTrails-title"
-                                    onPointerDown={action(() => (this.openPresVisibilityAndDuration = !this.openPresVisibilityAndDuration))}
-                                    style={{
-                                        color: SettingsManager.userColor,
-                                        backgroundColor: this.openPresVisibilityAndDuration ? SettingsManager.userVariantColor : SettingsManager.userBackgroundColor,
-                                    }}>
-                                    &nbsp; <FontAwesomeIcon style={{ alignSelf: 'center' }} icon={'rocket'} /> &nbsp; Visibility
-                                    <div className="propertiesView-presentationTrails-title-icon">
-                                        <FontAwesomeIcon icon={this.openPresVisibilityAndDuration ? 'caret-down' : 'caret-right'} size="lg" />
-                                    </div>
+                            {this.openPresTransitions ? <div className="propertiesView-presentationTrails-content">{PresBox.Instance.transitionDropdown}</div> : null}
+                        </div>
+                    )}
+                    {!selectedItem ? null : (
+                        <div className="propertiesView-presentationTrails">
+                            <div
+                                className="propertiesView-presentationTrails-title"
+                                onPointerDown={action(() => {
+                                    this.openPresVisibilityAndDuration = !this.openPresVisibilityAndDuration;
+                                })}
+                                style={{
+                                    color: SettingsManager.userColor,
+                                    backgroundColor: this.openPresVisibilityAndDuration ? SettingsManager.userVariantColor : SettingsManager.userBackgroundColor,
+                                }}>
+                                &nbsp; <FontAwesomeIcon style={{ alignSelf: 'center' }} icon="rocket" /> &nbsp; Visibility
+                                <div className="propertiesView-presentationTrails-title-icon">
+                                    <FontAwesomeIcon icon={this.openPresVisibilityAndDuration ? 'caret-down' : 'caret-right'} size="lg" />
                                 </div>
-                                {this.openPresVisibilityAndDuration ? <div className="propertiesView-presentationTrails-content">{PresBox.Instance.visibilityDurationDropdown}</div> : null}
                             </div>
-                        )}
-                        {!selectedItem ? null : (
-                            <div className="propertiesView-presentationTrails">
-                                <div
-                                    className="propertiesView-presentationTrails-title"
-                                    onPointerDown={action(() => (this.openPresProgressivize = !this.openPresProgressivize))}
-                                    style={{
-                                        color: SettingsManager.userColor,
-                                        backgroundColor: this.openPresProgressivize ? SettingsManager.userVariantColor : SettingsManager.userBackgroundColor,
-                                    }}>
-                                    &nbsp; <FontAwesomeIcon style={{ alignSelf: 'center' }} icon={'rocket'} /> &nbsp; Progressivize
-                                    <div className="propertiesView-presentationTrails-title-icon">
-                                        <FontAwesomeIcon icon={this.openPresProgressivize ? 'caret-down' : 'caret-right'} size="lg" />
-                                    </div>
+                            {this.openPresVisibilityAndDuration ? <div className="propertiesView-presentationTrails-content">{PresBox.Instance.visibilityDurationDropdown}</div> : null}
+                        </div>
+                    )}
+                    {!selectedItem ? null : (
+                        <div className="propertiesView-presentationTrails">
+                            <div
+                                className="propertiesView-presentationTrails-title"
+                                onPointerDown={action(() => {
+                                    this.openPresProgressivize = !this.openPresProgressivize;
+                                })}
+                                style={{
+                                    color: SettingsManager.userColor,
+                                    backgroundColor: this.openPresProgressivize ? SettingsManager.userVariantColor : SettingsManager.userBackgroundColor,
+                                }}>
+                                &nbsp; <FontAwesomeIcon style={{ alignSelf: 'center' }} icon="rocket" /> &nbsp; Progressivize
+                                <div className="propertiesView-presentationTrails-title-icon">
+                                    <FontAwesomeIcon icon={this.openPresProgressivize ? 'caret-down' : 'caret-right'} size="lg" />
                                 </div>
-                                {this.openPresProgressivize ? <div className="propertiesView-presentationTrails-content">{PresBox.Instance.progressivizeDropdown}</div> : null}
                             </div>
-                        )}
-                        {!selectedItem || (type !== DocumentType.VID && type !== DocumentType.AUDIO) ? null : (
-                            <div className="propertiesView-presentationTrails">
-                                <div
-                                    className="propertiesView-presentationTrails-title"
-                                    onPointerDown={action(() => (this.openSlideOptions = !this.openSlideOptions))}
-                                    style={{
-                                        color: SettingsManager.userColor,
-                                        backgroundColor: this.openSlideOptions ? SettingsManager.userVariantColor : SettingsManager.userBackgroundColor,
-                                    }}>
-                                    &nbsp; <FontAwesomeIcon style={{ alignSelf: 'center' }} icon={type === DocumentType.AUDIO ? 'file-audio' : 'file-video'} /> &nbsp; {type === DocumentType.AUDIO ? 'Audio Options' : 'Video Options'}
-                                    <div className="propertiesView-presentationTrails-title-icon">
-                                        <FontAwesomeIcon icon={this.openSlideOptions ? 'caret-down' : 'caret-right'} size="lg" />
-                                    </div>
+                            {this.openPresProgressivize ? <div className="propertiesView-presentationTrails-content">{PresBox.Instance.progressivizeDropdown}</div> : null}
+                        </div>
+                    )}
+                    {!selectedItem || (type !== DocumentType.VID && type !== DocumentType.AUDIO) ? null : (
+                        <div className="propertiesView-presentationTrails">
+                            <div
+                                className="propertiesView-presentationTrails-title"
+                                onPointerDown={action(() => {
+                                    this.openSlideOptions = !this.openSlideOptions;
+                                })}
+                                style={{
+                                    color: SettingsManager.userColor,
+                                    backgroundColor: this.openSlideOptions ? SettingsManager.userVariantColor : SettingsManager.userBackgroundColor,
+                                }}>
+                                &nbsp; <FontAwesomeIcon style={{ alignSelf: 'center' }} icon={type === DocumentType.AUDIO ? 'file-audio' : 'file-video'} /> &nbsp; {type === DocumentType.AUDIO ? 'Audio Options' : 'Video Options'}
+                                <div className="propertiesView-presentationTrails-title-icon">
+                                    <FontAwesomeIcon icon={this.openSlideOptions ? 'caret-down' : 'caret-right'} size="lg" />
                                 </div>
-                                {this.openSlideOptions ? <div className="propertiesView-presentationTrails-content">{PresBox.Instance.mediaOptionsDropdown}</div> : null}
                             </div>
-                        )}
-                    </div>
-                );
-            }
+                            {this.openSlideOptions ? <div className="propertiesView-presentationTrails-content">{PresBox.Instance.mediaOptionsDropdown}</div> : null}
+                        </div>
+                    )}
+                </div>
+            );
         }
+        return null;
     }
 }
diff --git a/src/client/views/ScriptBox.tsx b/src/client/views/ScriptBox.tsx
index 623201ed1..365980f33 100644
--- a/src/client/views/ScriptBox.tsx
+++ b/src/client/views/ScriptBox.tsx
@@ -4,7 +4,7 @@ import * as React from 'react';
 import { Doc, Opt } from '../../fields/Doc';
 import { ScriptField } from '../../fields/ScriptField';
 import { ScriptCast } from '../../fields/Types';
-import { emptyFunction } from '../../Utils';
+import { emptyFunction } from '../../ClientUtils';
 import { DragManager } from '../util/DragManager';
 import { CompileScript } from '../util/Scripting';
 import { EditableView } from './EditableView';
diff --git a/src/client/views/SidebarAnnos.tsx b/src/client/views/SidebarAnnos.tsx
index 3ad3c92da..bcd82887c 100644
--- a/src/client/views/SidebarAnnos.tsx
+++ b/src/client/views/SidebarAnnos.tsx
@@ -1,8 +1,10 @@
 import { computed, makeObservable } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
-import { emptyFunction, returnAll, returnFalse, returnOne, returnZero } from '../../Utils';
-import { Doc, DocListCast, Field, FieldResult, StrListCast } from '../../fields/Doc';
+import { ClientUtils, returnAll, returnFalse, returnOne, returnZero } from '../../ClientUtils';
+import { emptyFunction } from '../../Utils';
+import { Doc, DocListCast, Field, FieldType, FieldResult, StrListCast } from '../../fields/Doc';
+import { DocData } from '../../fields/DocSymbols';
 import { Id } from '../../fields/FieldSymbols';
 import { List } from '../../fields/List';
 import { RichTextField } from '../../fields/RichTextField';
@@ -18,7 +20,6 @@ import { StyleProp } from './StyleProvider';
 import { CollectionStackingView } from './collections/CollectionStackingView';
 import { FieldViewProps } from './nodes/FieldView';
 import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox';
-import { DocData } from '../../fields/DocSymbols';
 
 interface ExtraProps {
     fieldKey: string;
@@ -44,7 +45,7 @@ export class SidebarAnnos extends ObservableReactComponent<FieldViewProps & Extr
 
     _stackRef = React.createRef<CollectionStackingView>();
     @computed get allMetadata() {
-        const keys = new Map<string, FieldResult<Field>>();
+        const keys = new Map<string, FieldResult<FieldType>>();
         DocListCast(this._props.Document[this.sidebarKey]).forEach(doc =>
             SearchUtil.documentKeys(doc)
                 .filter(key => key[0] && key[0] !== '_' && key[0] === key[0].toUpperCase())
@@ -95,7 +96,7 @@ export class SidebarAnnos extends ObservableReactComponent<FieldViewProps & Extr
                 return {
                     type: 'dashField',
                     attrs: { fieldKey: key, docId: '', hideKey: false, hideValue: false, editable: true },
-                    marks: [{ type: 'pFontSize', attrs: { fontSize: '12px' } }, { type: 'strong' }, { type: 'user_mark', attrs: { userid: Doc.CurrentUserEmail, modified: 0 } }],
+                    marks: [{ type: 'pFontSize', attrs: { fontSize: '12px' } }, { type: 'strong' }, { type: 'user_mark', attrs: { userid: ClientUtils.CurrentUserEmail, modified: 0 } }],
                 };
             });
 
@@ -196,7 +197,7 @@ export class SidebarAnnos extends ObservableReactComponent<FieldViewProps & Extr
                 </div>
             );
         };
-        const renderMeta = (tag: string, dflt: FieldResult<Field>) => {
+        const renderMeta = (tag: string, dflt: FieldResult<FieldType>) => {
             const active = this.childFilters().includes(`${tag}${Doc.FilterSep}${Doc.FilterAny}${Doc.FilterSep}exists`);
             return (
                 <div key={tag} className={`sidebarAnnos-filterTag${active ? '-active' : ''}`} onClick={e => Doc.setDocFilter(this._props.Document, tag, Doc.FilterAny, 'exists', true, undefined, e.shiftKey)}>
diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx
index dcec2fe3d..15e309ca0 100644
--- a/src/client/views/StyleProvider.tsx
+++ b/src/client/views/StyleProvider.tsx
@@ -7,10 +7,10 @@ import { extname } from 'path';
 import * as React from 'react';
 import { BsArrowDown, BsArrowDownUp, BsArrowUp } from 'react-icons/bs';
 import { FaFilter } from 'react-icons/fa';
+import { ClientUtils, DashColor, lightOrDark } from '../../ClientUtils';
 import { Doc, Opt, StrListCast } from '../../fields/Doc';
 import { DocViews } from '../../fields/DocSymbols';
 import { BoolCast, Cast, DocCast, ImageCast, NumCast, ScriptCast, StrCast } from '../../fields/Types';
-import { DashColor, lightOrDark, Utils } from '../../Utils';
 import { CollectionViewType, DocumentType } from '../documents/DocumentTypes';
 import { DocFocusOrOpen, DocumentManager } from '../util/DocumentManager';
 import { IsFollowLinkScript } from '../util/LinkFollower';
@@ -26,6 +26,7 @@ import { FieldViewProps } from './nodes/FieldView';
 import { KeyValueBox } from './nodes/KeyValueBox';
 import { PropertiesView } from './PropertiesView';
 import './StyleProvider.scss';
+import { ScriptField } from '../../fields/ScriptField';
 
 export enum StyleProp {
     TreeViewIcon = 'treeView_Icon',
@@ -70,6 +71,20 @@ function togglePaintView(e: React.MouseEvent, doc: Opt<Doc>, props: Opt<FieldVie
     ScriptCast(doc?.onPaint)?.script.run(scriptProps);
 }
 
+export function styleFromLayoutString(doc: Doc, props: FieldViewProps, scale: number) {
+    const style: { [key: string]: any } = {};
+    const divKeys = ['width', 'height', 'fontSize', 'transform', 'left', 'backgroundColor', 'left', 'right', 'top', 'bottom', 'pointerEvents', 'position'];
+    const replacer = (match: any, expr: string, offset: any, string: any) => {
+        // bcz: this executes a script to convert a property expression string:  { script }  into a value
+        return ScriptField.MakeFunction(expr, { self: Doc.name, this: Doc.name, scale: 'number' })?.script.run({ this: doc, self: doc, scale }).result?.toString() ?? '';
+    };
+    divKeys.map((prop: string) => {
+        const p = (props as any)[prop];
+        typeof p === 'string' && (style[prop] = p?.replace(/{([^.'][^}']+)}/g, replacer));
+    });
+    return style;
+}
+
 export function wavyBorderPath(pw: number, ph: number, inset: number = 0.05) {
     return `M ${pw * 0.5} ${ph * inset}   C ${pw * 0.6} ${ph * inset} ${pw * (1 - 2 * inset)} 0 ${pw * (1 - inset)} ${ph * inset}   C ${pw} ${ph * (2 * inset)} ${pw * (1 - inset)} ${ph * 0.25} ${pw * (1 - inset)} ${ph * 0.3}   C ${
         pw * (1 - inset)
@@ -156,7 +171,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps &
                         doc._layout_showTitle,
                         props?.layout_showTitle?.() ||
                             (!Doc.IsSystem(doc) && [DocumentType.COL, DocumentType.FUNCPLOT, DocumentType.LABEL, DocumentType.RTF, DocumentType.IMG, DocumentType.VID].includes(doc.type as any)
-                                ? doc.author === Doc.CurrentUserEmail
+                                ? doc.author === ClientUtils.CurrentUserEmail
                                     ? StrCast(Doc.UserDoc().layout_showTitle)
                                     : remoteDocHeader
                                 : '')
@@ -300,7 +315,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps &
                 const showFilterIcon =
                     StrListCast(doc?._childFilters).length || StrListCast(doc?._childFiltersByRanges).length
                         ? 'green' // #18c718bd' //'hasFilter'
-                        : props?.childFilters?.().filter(f => Utils.IsRecursiveFilter(f) && f !== Utils.noDragDocsFilter).length || props?.childFiltersByRanges().length
+                        : props?.childFilters?.().filter(f => ClientUtils.IsRecursiveFilter(f) && f !== ClientUtils.noDragDocsFilter).length || props?.childFiltersByRanges().length
                         ? 'orange' //'inheritsFilter'
                         : undefined;
                 return !showFilterIcon ? null : (
@@ -308,7 +323,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps &
                         <Dropdown
                             type={Type.TERT}
                             dropdownType={DropdownType.CLICK}
-                            fillWidth
+                        fillWidth
                             iconProvider={(active:boolean) => <div className='styleProvider-filterShift'><FaFilter/></div>}
                             closeOnSelect={true}
                             setSelectedVal={
diff --git a/src/client/views/TemplateMenu.tsx b/src/client/views/TemplateMenu.tsx
index 5df0bea1a..1d02568dd 100644
--- a/src/client/views/TemplateMenu.tsx
+++ b/src/client/views/TemplateMenu.tsx
@@ -1,12 +1,13 @@
-import { computed, observable, ObservableSet, runInAction } from 'mobx';
+import { computed, ObservableSet, runInAction } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
+import { returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue } from '../../ClientUtils';
 import { Doc, DocListCast } from '../../fields/Doc';
 import { DocData } from '../../fields/DocSymbols';
 import { ScriptField } from '../../fields/ScriptField';
 import { Cast, DocCast, StrCast } from '../../fields/Types';
 import { TraceMobx } from '../../fields/util';
-import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue } from '../../Utils';
+import { emptyFunction } from '../../Utils';
 import { Docs, DocUtils } from '../documents/Documents';
 import { ScriptingGlobals } from '../util/ScriptingGlobals';
 import { Transform } from '../util/Transform';
@@ -15,21 +16,6 @@ import { DocumentView, returnEmptyDocViewList } from './nodes/DocumentView';
 import { DefaultStyleProvider } from './StyleProvider';
 import './TemplateMenu.scss';
 
-@observer
-class TemplateToggle extends React.Component<{ template: string; checked: boolean; toggle: (event: React.ChangeEvent<HTMLInputElement>, template: string) => void }> {
-    render() {
-        if (this.props.template) {
-            return (
-                <li className="templateToggle">
-                    <input type="checkbox" checked={this.props.checked} onChange={event => this.props.toggle(event, this.props.template)} />
-                    {this.props.template}
-                </li>
-            );
-        } else {
-            return null;
-        }
-    }
-}
 @observer
 class OtherToggle extends React.Component<{ checked: boolean; name: string; toggle: (event: React.ChangeEvent<HTMLInputElement>) => void }> {
     render() {
@@ -50,12 +36,25 @@ export interface TemplateMenuProps {
 export class TemplateMenu extends React.Component<TemplateMenuProps> {
     _addedKeys = new ObservableSet();
     _customRef = React.createRef<HTMLInputElement>();
-    @observable private _hidden: boolean = true;
+
+    componentDidMount() {
+        !this._addedKeys && (this._addedKeys = new ObservableSet());
+        [...Array.from(Object.keys(this.props.docViews[0].Document[DocData])), ...Array.from(Object.keys(this.props.docViews[0].Document))]
+            .filter(key => key.startsWith('layout_') && (
+                StrCast(this.props.docViews[0].Document[key]).startsWith("<") ||
+                DocCast(this.props.docViews[0].Document[key])?.isTemplateDoc
+            ))
+            .forEach(key => runInAction(() => this._addedKeys.add(key.replace('layout_', '')))); // prettier-ignore
+    }
+    @computed get scriptField() {
+        const script = ScriptField.MakeScript('docs.map(d => switchView(d, this))', { this: Doc.name }, { docs: this.props.docViews.map(dv => dv.Document) as any });
+        return script ? () => script : undefined;
+    }
 
     toggleLayout = (e: React.ChangeEvent<HTMLInputElement>, layout: string): void => {
         this.props.docViews.map(dv => dv.switchViews(e.target.checked, layout, undefined, true));
     };
-    toggleDefault = (e: React.ChangeEvent<HTMLInputElement>): void => {
+    toggleDefault = (): void => {
         this.props.docViews.map(dv => dv.switchViews(false, 'layout'));
     };
 
@@ -65,21 +64,8 @@ export class TemplateMenu extends React.Component<TemplateMenuProps> {
             runInAction(() => this._addedKeys.add(this._customRef.current!.value));
         }
     };
-    componentDidMount() {
-        !this._addedKeys && (this._addedKeys = new ObservableSet());
-        [...Array.from(Object.keys(this.props.docViews[0].Document[DocData])), ...Array.from(Object.keys(this.props.docViews[0].Document))]
-            .filter(key => key.startsWith('layout_') && (
-                StrCast(this.props.docViews[0].Document[key]).startsWith("<") ||
-                DocCast(this.props.docViews[0].Document[key])?.isTemplateDoc
-            ))
-            .map(key => runInAction(() => this._addedKeys.add(key.replace('layout_', '')))); // prettier-ignore
-    }
 
     return100 = () => 300;
-    @computed get scriptField() {
-        const script = ScriptField.MakeScript('docs.map(d => switchView(d, this))', { this: Doc.name }, { docs: this.props.docViews.map(dv => dv.Document) as any });
-        return script ? () => script : undefined;
-    }
     templateIsUsed = (selDoc: Doc, templateDoc: Doc) => {
         const template = StrCast(templateDoc.dragFactory ? Cast(templateDoc.dragFactory, Doc, null)?.title : templateDoc.title);
         return StrCast(selDoc.layout_fieldKey) === 'layout_' + template ? 'check' : 'unchecked';
@@ -88,10 +74,11 @@ export class TemplateMenu extends React.Component<TemplateMenuProps> {
         TraceMobx();
         const firstDoc = this.props.docViews[0].Document;
         const templateName = StrCast(firstDoc.layout_fieldKey, 'layout').replace('layout_', '');
-        const noteTypes = DocListCast(Cast(Doc.UserDoc()['template_notes'], Doc, null)?.data);
-        const addedTypes = DocListCast(Cast(Doc.UserDoc()['template_clickFuncs'], Doc, null)?.data);
+        const noteTypes = DocListCast(Cast(Doc.UserDoc().template_notes, Doc, null)?.data);
+        const addedTypes = DocListCast(Cast(Doc.UserDoc().template_clickFuncs, Doc, null)?.data);
         const templateMenu: Array<JSX.Element> = [];
         templateMenu.push(<OtherToggle key="default" name={firstDoc.layout instanceof Doc ? StrCast(firstDoc.layout.title) : 'Default'} checked={templateName === 'layout'} toggle={this.toggleDefault} />);
+        // eslint-disable-next-line no-return-assign
         addedTypes.concat(noteTypes).map(template => (template.treeView_Checked = this.templateIsUsed(firstDoc, template)));
         this._addedKeys &&
             Array.from(this._addedKeys)
@@ -122,10 +109,10 @@ export class TemplateMenu extends React.Component<TemplateMenuProps> {
                     addDocTab={returnFalse}
                     PanelWidth={this.return100}
                     PanelHeight={this.return100}
-                    treeViewHideHeaderFields={true}
-                    treeViewHideTitle={true}
-                    dontRegisterView={true}
-                    fieldKey={'data'}
+                    treeViewHideHeaderFields
+                    treeViewHideTitle
+                    dontRegisterView
+                    fieldKey="data"
                     moveDocument={returnFalse}
                     removeDocument={returnFalse}
                     addDocument={returnFalse}
@@ -135,10 +122,9 @@ export class TemplateMenu extends React.Component<TemplateMenuProps> {
     }
 }
 
-ScriptingGlobals.add(function switchView(doc: Doc, template: Doc | undefined) {
-    if (template?.dragFactory) {
-        template = Cast(template.dragFactory, Doc, null);
-    }
+// eslint-disable-next-line prefer-arrow-callback
+ScriptingGlobals.add(function switchView(doc: Doc, templateIn: Doc | undefined) {
+    const template = templateIn?.dragFactory ? Cast(templateIn.dragFactory, Doc, null) : templateIn;
     const templateTitle = StrCast(template?.title);
     return templateTitle && DocUtils.makeCustomViewClicked(doc, Docs.Create.FreeformDocument, templateTitle, template);
 });
diff --git a/src/client/views/Touchable.tsx b/src/client/views/Touchable.tsx
deleted file mode 100644
index 436cb688f..000000000
--- a/src/client/views/Touchable.tsx
+++ /dev/null
@@ -1,213 +0,0 @@
-import * as React from 'react';
-import { action } from 'mobx';
-import { InteractionUtils } from '../util/InteractionUtils';
-
-const HOLD_DURATION = 1000;
-
-export abstract class Touchable<T = {}> extends React.Component<React.PropsWithChildren<T>> {
-    //private holdTimer: NodeJS.Timeout | undefined;
-    private moveDisposer?: InteractionUtils.MultiTouchEventDisposer;
-    private endDisposer?: InteractionUtils.MultiTouchEventDisposer;
-    private holdMoveDisposer?: InteractionUtils.MultiTouchEventDisposer;
-    private holdEndDisposer?: InteractionUtils.MultiTouchEventDisposer;
-
-    protected abstract _multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer;
-    protected _touchDrag: boolean = false;
-    protected prevPoints: Map<number, React.Touch> = new Map<number, React.Touch>();
-
-    public FirstX: number = 0;
-    public FirstY: number = 0;
-    public SecondX: number = 0;
-    public SecondY: number = 0;
-
-    /**
-     * When a touch even starts, we keep track of each touch that is associated with that event
-     */
-    @action
-    protected onTouchStart = (e: Event, me: InteractionUtils.MultiTouchEvent<React.TouchEvent>): void => {
-        const actualPts: React.Touch[] = [];
-        const te = me.touchEvent;
-        // loop through all touches on screen
-        for (const pt of me.touches) {
-            actualPts.push(pt);
-            if (this.prevPoints.has(pt.identifier)) {
-                this.prevPoints.set(pt.identifier, pt);
-            }
-            // only add the ones that are targeted on "this" element, but with the identifier that the screen touch gives
-            for (const tPt of me.changedTouches) {
-                if (pt.clientX === tPt.clientX && pt.clientY === tPt.clientY) {
-                    // pen is also a touch, but with a radius of 0.5 (at least with the surface pens)
-                    // and this seems to be the only way of differentiating pen and touch on touch events
-                    if ((pt as any).radiusX > 1 && (pt as any).radiusY > 1) {
-                        this.prevPoints.set(pt.identifier, pt);
-                    }
-                }
-            }
-        }
-
-        const ptsToDelete: number[] = [];
-        this.prevPoints.forEach(pt => {
-            if (!actualPts.includes(pt)) {
-                ptsToDelete.push(pt.identifier);
-            }
-        });
-
-        ptsToDelete.forEach(pt => this.prevPoints.delete(pt));
-
-        if (this.prevPoints.size) {
-            switch (this.prevPoints.size) {
-                case 1:
-                    this.handle1PointerDown(te, me);
-                    te.persist();
-                    // -- code for radial menu --
-                    // if (this.holdTimer) {
-                    //     clearTimeout(this.holdTimer)
-                    //     this.holdTimer = undefined;
-                    // }
-                    break;
-                case 2:
-                    this.handle2PointersDown(te, me);
-                    break;
-            }
-        }
-    };
-
-    /**
-     * Handle touch move event
-     */
-    @action
-    protected onTouch = (e: Event, me: InteractionUtils.MultiTouchEvent<TouchEvent>): void => {
-        const te = me.touchEvent;
-        const myTouches = InteractionUtils.GetMyTargetTouches(me, this.prevPoints, true);
-
-        // if we're not actually moving a lot, don't consider it as dragging yet
-        if (!InteractionUtils.IsDragging(this.prevPoints, myTouches, 5) && !this._touchDrag) return;
-        this._touchDrag = true;
-        switch (myTouches.length) {
-            case 1:
-                this.handle1PointerMove(te, me);
-                break;
-            case 2:
-                this.handle2PointersMove(te, me);
-                break;
-        }
-
-        for (const pt of me.touches) {
-            if (pt && this.prevPoints.has(pt.identifier)) {
-                this.prevPoints.set(pt.identifier, pt);
-            }
-        }
-    };
-
-    @action
-    protected onTouchEnd = (e: Event, me: InteractionUtils.MultiTouchEvent<TouchEvent>): void => {
-        // remove all the touches associated with the event
-        const te = me.touchEvent;
-        for (const pt of me.changedTouches) {
-            if (pt) {
-                if (this.prevPoints.has(pt.identifier)) {
-                    this.prevPoints.delete(pt.identifier);
-                }
-            }
-        }
-        this._touchDrag = false;
-        te.stopPropagation();
-
-        // if (e.targetTouches.length === 0) {
-        //     this.prevPoints.clear();
-        // }
-
-        if (this.prevPoints.size === 0) {
-            this.cleanUpInteractions();
-        }
-        e.stopPropagation();
-    };
-
-    cleanUpInteractions = (): void => {
-        this.removeMoveListeners();
-        this.removeEndListeners();
-    };
-
-    handle1PointerMove = (e: TouchEvent, me: InteractionUtils.MultiTouchEvent<TouchEvent>): any => {
-        e.stopPropagation();
-        e.preventDefault();
-    };
-
-    handle2PointersMove = (e: TouchEvent, me: InteractionUtils.MultiTouchEvent<TouchEvent>): any => {
-        e.stopPropagation();
-        e.preventDefault();
-    };
-
-    handle1PointerDown = (e: React.TouchEvent, me: InteractionUtils.MultiTouchEvent<React.TouchEvent>): any => {
-        this.removeMoveListeners();
-        this.addMoveListeners();
-        this.removeEndListeners();
-        this.addEndListeners();
-    };
-
-    handle2PointersDown = (e: React.TouchEvent, me: InteractionUtils.MultiTouchEvent<React.TouchEvent>): any => {
-        this.removeMoveListeners();
-        this.addMoveListeners();
-        this.removeEndListeners();
-        this.addEndListeners();
-    };
-
-    handle1PointerHoldStart = (e: Event, me: InteractionUtils.MultiTouchEvent<React.TouchEvent>): any => {
-        e.stopPropagation();
-        me.touchEvent.stopPropagation();
-        this.removeMoveListeners();
-        this.removeEndListeners();
-        this.removeHoldMoveListeners();
-        this.removeHoldEndListeners();
-        this.addHoldMoveListeners();
-        this.addHoldEndListeners();
-    };
-
-    addMoveListeners = () => {
-        const handler = (e: Event) => this.onTouch(e, (e as CustomEvent<InteractionUtils.MultiTouchEvent<TouchEvent>>).detail);
-        document.addEventListener('dashOnTouchMove', handler);
-        this.moveDisposer = () => document.removeEventListener('dashOnTouchMove', handler);
-    };
-    addEndListeners = () => {
-        const handler = (e: Event) => this.onTouchEnd(e, (e as CustomEvent<InteractionUtils.MultiTouchEvent<TouchEvent>>).detail);
-        document.addEventListener('dashOnTouchEnd', handler);
-        this.endDisposer = () => document.removeEventListener('dashOnTouchEnd', handler);
-    };
-
-    addHoldMoveListeners = () => {
-        const handler = (e: Event) => this.handle1PointerHoldMove(e, (e as CustomEvent<InteractionUtils.MultiTouchEvent<TouchEvent>>).detail);
-        document.addEventListener('dashOnTouchHoldMove', handler);
-        this.holdMoveDisposer = () => document.removeEventListener('dashOnTouchHoldMove', handler);
-    };
-
-    addHoldEndListeners = () => {
-        const handler = (e: Event) => this.handle1PointerHoldEnd(e, (e as CustomEvent<InteractionUtils.MultiTouchEvent<TouchEvent>>).detail);
-        document.addEventListener('dashOnTouchHoldEnd', handler);
-        this.holdEndDisposer = () => document.removeEventListener('dashOnTouchHoldEnd', handler);
-    };
-
-    removeMoveListeners = () => this.moveDisposer?.();
-    removeEndListeners = () => this.endDisposer?.();
-    removeHoldMoveListeners = () => this.holdMoveDisposer?.();
-    removeHoldEndListeners = () => this.holdEndDisposer?.();
-
-    handle1PointerHoldMove = (e: Event, me: InteractionUtils.MultiTouchEvent<TouchEvent>): void => {
-        // e.stopPropagation();
-        // me.touchEvent.stopPropagation();
-    };
-
-    handle1PointerHoldEnd = (e: Event, me: InteractionUtils.MultiTouchEvent<TouchEvent>): void => {
-        e.stopPropagation();
-        me.touchEvent.stopPropagation();
-        this.removeHoldMoveListeners();
-        this.removeHoldEndListeners();
-
-        me.touchEvent.stopPropagation();
-        me.touchEvent.preventDefault();
-    };
-
-    handleHandDown = (e: React.TouchEvent) => {
-        // e.stopPropagation();
-        // e.preventDefault();
-    };
-}
diff --git a/src/client/views/animationtimeline/Timeline.tsx b/src/client/views/animationtimeline/Timeline.tsx
index cc4da1694..c3e513154 100644
--- a/src/client/views/animationtimeline/Timeline.tsx
+++ b/src/client/views/animationtimeline/Timeline.tsx
@@ -4,7 +4,8 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { action, computed, makeObservable, observable } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
-import { Utils, emptyFunction, setupMoveUpEvents } from '../../../Utils';
+import { setupMoveUpEvents } from '../../../ClientUtils';
+import { Utils, emptyFunction } from '../../../Utils';
 import { Doc, DocListCast } from '../../../fields/Doc';
 import { BoolCast, NumCast, StrCast } from '../../../fields/Types';
 import { DocumentType } from '../../documents/DocumentTypes';
diff --git a/src/client/views/animationtimeline/TimelineMenu.tsx b/src/client/views/animationtimeline/TimelineMenu.tsx
index 97a571dc4..20da21cb9 100644
--- a/src/client/views/animationtimeline/TimelineMenu.tsx
+++ b/src/client/views/animationtimeline/TimelineMenu.tsx
@@ -1,3 +1,4 @@
+/* eslint-disable jsx-a11y/click-events-have-key-events */
 import { IconLookup } from '@fortawesome/fontawesome-svg-core';
 import { faChartLine, faClipboard } from '@fortawesome/free-solid-svg-icons';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
@@ -9,6 +10,7 @@ import './TimelineMenu.scss';
 
 @observer
 export class TimelineMenu extends React.Component {
+    // eslint-disable-next-line no-use-before-define
     public static Instance: TimelineMenu;
 
     @observable private _opacity = 0;
@@ -67,16 +69,19 @@ export class TimelineMenu extends React.Component {
             this._currentMenu.push(
                 <div key={Utils.GenerateGuid()} className="timeline-menu-item">
                     <FontAwesomeIcon icon={faChartLine as IconLookup} size="lg" />
-                    <p
-                        className="timeline-menu-desc"
-                        onClick={e => {
-                            e.preventDefault();
-                            e.stopPropagation();
-                            event(e);
-                            this.closeMenu();
-                        }}>
-                        {title}
-                    </p>
+                    {
+                        // eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions
+                        <p
+                            className="timeline-menu-desc"
+                            onClick={e => {
+                                e.preventDefault();
+                                e.stopPropagation();
+                                event(e);
+                                this.closeMenu();
+                            }}>
+                            {title}
+                        </p>
+                    }
                 </div>
             );
         }
diff --git a/src/client/views/collections/CollectionCalendarView.tsx b/src/client/views/collections/CollectionCalendarView.tsx
index cbcc980a9..578ce77a5 100644
--- a/src/client/views/collections/CollectionCalendarView.tsx
+++ b/src/client/views/collections/CollectionCalendarView.tsx
@@ -1,7 +1,8 @@
 import { computed, makeObservable } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
-import { dateRangeStrToDates, emptyFunction, returnTrue } from '../../../Utils';
+import { emptyFunction } from '../../../Utils';
+import { dateRangeStrToDates, returnTrue } from '../../../ClientUtils';
 import { Doc, DocListCast } from '../../../fields/Doc';
 import { StrCast } from '../../../fields/Types';
 import { CollectionStackingView } from './CollectionStackingView';
diff --git a/src/client/views/collections/CollectionCarousel3DView.tsx b/src/client/views/collections/CollectionCarousel3DView.tsx
index 4e4bd43bf..8d8f41126 100644
--- a/src/client/views/collections/CollectionCarousel3DView.tsx
+++ b/src/client/views/collections/CollectionCarousel3DView.tsx
@@ -1,9 +1,12 @@
+/* eslint-disable jsx-a11y/no-static-element-interactions */
+/* eslint-disable jsx-a11y/click-events-have-key-events */
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { computed, makeObservable } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
-import { Utils, emptyFunction, returnFalse, returnZero } from '../../../Utils';
-import { Doc, DocListCast } from '../../../fields/Doc';
+import { returnZero } from '../../../ClientUtils';
+import { Utils } from '../../../Utils';
+import { Doc, DocListCast, Opt } from '../../../fields/Doc';
 import { Id } from '../../../fields/FieldSymbols';
 import { DocCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
 import { DocumentType } from '../../documents/DocumentTypes';
@@ -14,12 +17,13 @@ import { DocumentView } from '../nodes/DocumentView';
 import { FocusViewOptions } from '../nodes/FieldView';
 import './CollectionCarousel3DView.scss';
 import { CollectionSubView } from './CollectionSubView';
+
 const { CAROUSEL3D_CENTER_SCALE, CAROUSEL3D_SIDE_SCALE, CAROUSEL3D_TOP } = require('../global/globalCssVariables.module.scss');
 
 @observer
 export class CollectionCarousel3DView extends CollectionSubView() {
     @computed get scrollSpeed() {
-        return this.layoutDoc._autoScrollSpeed ? NumCast(this.layoutDoc._autoScrollSpeed) : 1000; //default scroll speed
+        return this.layoutDoc._autoScrollSpeed ? NumCast(this.layoutDoc._autoScrollSpeed) : 1000; // default scroll speed
     }
     constructor(props: any) {
         super(props);
@@ -48,16 +52,16 @@ export class CollectionCarousel3DView extends CollectionSubView() {
     panelHeight = () => this._props.PanelHeight() * Number(CAROUSEL3D_SIDE_SCALE);
     onChildDoubleClick = () => ScriptCast(this.layoutDoc.onChildDoubleClick);
     isContentActive = () => this._props.isSelected() || this._props.isContentActive() || this._props.isAnyChildContentActive();
-    isChildContentActive = () => (this.isContentActive() ? true : false);
+    isChildContentActive = () => !!this.isContentActive();
     childScreenToLocal = () =>
         this._props // document's left is the panel shifted by the doc's index * panelWidth/#docs.  But it scales by centerScale around its center, so it's left moves left by the distance of the left from the center (panelwidth/2) * the scale delta (centerScale-1)
             .ScreenToLocalTransform() // the top behaves the same way ecept it's shifted by the 'top' amount specified for the panel in css and then by the scale factor.
             .translate(-this.panelWidth() + ((this.centerScale - 1) * this.panelWidth()) / 2, -((Number(CAROUSEL3D_TOP) / 100) * this._props.PanelHeight()) + ((this.centerScale - 1) * this.panelHeight()) / 2)
             .scale(1 / this.centerScale);
 
-    focus = (anchor: Doc, options: FocusViewOptions) => {
+    focus = (anchor: Doc, options: FocusViewOptions): Opt<number> => {
         const docs = DocListCast(this.Document[this.fieldKey ?? Doc.LayoutFieldKey(this.Document)]);
-        if (anchor.type !== DocumentType.CONFIG && !docs.includes(anchor)) return;
+        if (anchor.type !== DocumentType.CONFIG && !docs.includes(anchor)) return undefined;
         options.didMove = true;
         const target = DocCast(anchor.annotationOn) ?? anchor;
         const index = docs.indexOf(target);
@@ -66,37 +70,34 @@ export class CollectionCarousel3DView extends CollectionSubView() {
     };
     @computed get content() {
         const currentIndex = NumCast(this.layoutDoc._carousel_index);
-        const displayDoc = (childPair: { layout: Doc; data: Doc }) => {
-            return (
-                <DocumentView
-                    {...this._props}
-                    Document={childPair.layout}
-                    TemplateDataDocument={childPair.data}
-                    //suppressSetHeight={true}
-                    NativeWidth={returnZero}
-                    NativeHeight={returnZero}
-                    layout_fitWidth={undefined}
-                    onDoubleClickScript={this.onChildDoubleClick}
-                    renderDepth={this._props.renderDepth + 1}
-                    LayoutTemplate={this._props.childLayoutTemplate}
-                    LayoutTemplateString={this._props.childLayoutString}
-                    focus={this.focus}
-                    ScreenToLocalTransform={this.childScreenToLocal}
-                    isContentActive={this.isChildContentActive}
-                    isDocumentActive={this._props.childDocumentsActive?.() || this.Document._childDocumentsActive ? this._props.isDocumentActive : this.isContentActive}
-                    PanelWidth={this.panelWidth}
-                    PanelHeight={this.panelHeight}
-                />
-            );
-        };
-
-        return this.carouselItems.map((childPair, index) => {
-            return (
-                <div key={childPair.layout[Id]} className={`collectionCarousel3DView-item${index === currentIndex ? '-active' : ''} ${index}`} style={{ width: this.panelWidth() }}>
-                    {displayDoc(childPair)}
-                </div>
-            );
-        });
+        const displayDoc = (childPair: { layout: Doc; data: Doc }) => (
+            <DocumentView
+                // eslint-disable-next-line react/jsx-props-no-spreading
+                {...this._props}
+                Document={childPair.layout}
+                TemplateDataDocument={childPair.data}
+                // suppressSetHeight={true}
+                NativeWidth={returnZero}
+                NativeHeight={returnZero}
+                layout_fitWidth={undefined}
+                onDoubleClickScript={this.onChildDoubleClick}
+                renderDepth={this._props.renderDepth + 1}
+                LayoutTemplate={this._props.childLayoutTemplate}
+                LayoutTemplateString={this._props.childLayoutString}
+                focus={this.focus}
+                ScreenToLocalTransform={this.childScreenToLocal}
+                isContentActive={this.isChildContentActive}
+                isDocumentActive={this._props.childDocumentsActive?.() || this.Document._childDocumentsActive ? this._props.isDocumentActive : this.isContentActive}
+                PanelWidth={this.panelWidth}
+                PanelHeight={this.panelHeight}
+            />
+        );
+
+        return this.carouselItems.map((childPair, index) => (
+            <div key={childPair.layout[Id]} className={`collectionCarousel3DView-item${index === currentIndex ? '-active' : ''} ${index}`} style={{ width: this.panelWidth() }}>
+                {displayDoc(childPair)}
+            </div>
+        ));
     }
 
     changeSlide = (direction: number) => {
@@ -124,21 +125,21 @@ export class CollectionCarousel3DView extends CollectionSubView() {
     };
 
     toggleAutoScroll = (direction: number) => {
-        this.layoutDoc.autoScrollOn = this.layoutDoc.autoScrollOn ? false : true;
+        this.layoutDoc.autoScrollOn = !this.layoutDoc.autoScrollOn;
         this.layoutDoc.autoScrollOn ? this.startAutoScroll(direction) : this.stopAutoScroll();
     };
 
     fadeScrollButton = () => {
         window.setTimeout(() => {
-            !this.layoutDoc.autoScrollOn && (this.layoutDoc.showScrollButton = 'none'); //fade away after 1.5s if it's not clicked.
+            !this.layoutDoc.autoScrollOn && (this.layoutDoc.showScrollButton = 'none'); // fade away after 1.5s if it's not clicked.
         }, 1500);
     };
 
     @computed get buttons() {
         return (
             <div className="arrow-buttons">
-                <div title="click to go back" key="back" className="carousel3DView-back" onClick={e => this.onArrowClick(-1)} />
-                <div title="click to advance" key="fwd" className="carousel3DView-fwd" onClick={e => this.onArrowClick(1)} />
+                <div title="click to go back" key="back" className="carousel3DView-back" onClick={() => this.onArrowClick(-1)} />
+                <div title="click to advance" key="fwd" className="carousel3DView-fwd" onClick={() => this.onArrowClick(1)} />
                 {/* {this.autoScrollButton} */}
             </div>
         );
@@ -149,17 +150,25 @@ export class CollectionCarousel3DView extends CollectionSubView() {
         return (
             <>
                 <div className={`carousel3DView-back-scroll${whichButton === 'back' ? '' : '-hidden'}`} style={{ background: `${StrCast(this.Document.backgroundColor)}` }} onClick={() => this.toggleAutoScroll(-1)}>
-                    {this.layoutDoc.autoScrollOn ? <FontAwesomeIcon icon={'pause'} size={'1x'} /> : <FontAwesomeIcon icon={'angle-double-left'} size={'1x'} />}
+                    {this.layoutDoc.autoScrollOn ? <FontAwesomeIcon icon="pause" size="1x" /> : <FontAwesomeIcon icon="angle-double-left" size="1x" />}
                 </div>
                 <div className={`carousel3DView-fwd-scroll${whichButton === 'fwd' ? '' : '-hidden'}`} style={{ background: `${StrCast(this.Document.backgroundColor)}` }} onClick={() => this.toggleAutoScroll(1)}>
-                    {this.layoutDoc.autoScrollOn ? <FontAwesomeIcon icon={'pause'} size={'1x'} /> : <FontAwesomeIcon icon={'angle-double-right'} size={'1x'} />}
+                    {this.layoutDoc.autoScrollOn ? <FontAwesomeIcon icon="pause" size="1x" /> : <FontAwesomeIcon icon="angle-double-right" size="1x" />}
                 </div>
             </>
         );
     }
 
     @computed get dots() {
-        return this.carouselItems.map((_child, index) => <div key={Utils.GenerateGuid()} className={`dot${index === NumCast(this.layoutDoc._carousel_index) ? '-active' : ''}`} onClick={() => (this.layoutDoc._carousel_index = index)} />);
+        return this.carouselItems.map((_child, index) => (
+            <div
+                key={Utils.GenerateGuid()}
+                className={`dot${index === NumCast(this.layoutDoc._carousel_index) ? '-active' : ''}`}
+                onClick={() => {
+                    this.layoutDoc._carousel_index = index;
+                }}
+            />
+        ));
     }
     @computed get translateX() {
         const index = NumCast(this.layoutDoc._carousel_index);
diff --git a/src/client/views/collections/CollectionCarouselView.tsx b/src/client/views/collections/CollectionCarouselView.tsx
index 9c370bfbb..6dda6e545 100644
--- a/src/client/views/collections/CollectionCarouselView.tsx
+++ b/src/client/views/collections/CollectionCarouselView.tsx
@@ -2,9 +2,11 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { computed, makeObservable } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
-import { StopEvent, emptyFunction, returnFalse, returnOne, returnZero } from '../../../Utils';
+import { emptyFunction } from '../../../Utils';
+import { StopEvent, returnFalse, returnOne, returnZero } from '../../../ClientUtils';
 import { Doc, Opt } from '../../../fields/Doc';
 import { DocCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
+import { DocumentType } from '../../documents/DocumentTypes';
 import { DragManager } from '../../util/DragManager';
 import { StyleProp } from '../StyleProvider';
 import { DocumentView } from '../nodes/DocumentView';
@@ -12,7 +14,6 @@ import { FieldViewProps } from '../nodes/FieldView';
 import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox';
 import './CollectionCarouselView.scss';
 import { CollectionSubView } from './CollectionSubView';
-import { DocumentType } from '../../documents/DocumentTypes';
 
 @observer
 export class CollectionCarouselView extends CollectionSubView() {
diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index b2897a9b7..9a1781b93 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -2,25 +2,26 @@ import { action, IReactionDisposer, makeObservable, observable, reaction } from
 import { observer } from 'mobx-react';
 import * as React from 'react';
 import * as ReactDOM from 'react-dom/client';
-import * as GoldenLayout from '../../../client/goldenLayout';
+import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, DivHeight, DivWidth, incrementTitleCopy } from '../../../ClientUtils';
 import { Doc, DocListCast, Field, Opt } from '../../../fields/Doc';
 import { AclAdmin, AclEdit, DocData } from '../../../fields/DocSymbols';
 import { Id } from '../../../fields/FieldSymbols';
 import { InkTool } from '../../../fields/InkField';
 import { List } from '../../../fields/List';
-import { FieldValue, ImageCast, NumCast, StrCast } from '../../../fields/Types';
+import { ImageCast, NumCast, StrCast } from '../../../fields/Types';
 import { ImageField } from '../../../fields/URLField';
 import { GetEffectiveAcl, inheritParentAcls, SetPropSetterCb } from '../../../fields/util';
-import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, DivHeight, DivWidth, emptyFunction, incrementTitleCopy } from '../../../Utils';
+import { emptyFunction } from '../../../Utils';
 import { DocServer } from '../../DocServer';
 import { Docs } from '../../documents/Documents';
 import { CollectionViewType, DocumentType } from '../../documents/DocumentTypes';
+import * as GoldenLayout from '../../goldenLayout';
 import { DocumentManager } from '../../util/DocumentManager';
 import { DragManager } from '../../util/DragManager';
 import { InteractionUtils } from '../../util/InteractionUtils';
 import { ScriptingGlobals } from '../../util/ScriptingGlobals';
 import { SelectionManager } from '../../util/SelectionManager';
-import { SettingsManager } from '../../util/SettingsManager';
+import { SnappingManager } from '../../util/SnappingManager';
 import { undoable, undoBatch, UndoManager } from '../../util/UndoManager';
 import { DashboardView } from '../DashboardView';
 import { LightboxView } from '../LightboxView';
@@ -32,11 +33,12 @@ import './CollectionDockingView.scss';
 import { CollectionFreeFormView } from './collectionFreeForm';
 import { CollectionSubView } from './CollectionSubView';
 import { TabDocView } from './TabDocView';
-import { ComputedField } from '../../../fields/ScriptField';
+
 const _global = (window /* browser */ || global) /* node */ as any;
 
 @observer
 export class CollectionDockingView extends CollectionSubView() {
+    // eslint-disable-next-line no-use-before-define
     @observable public static Instance: CollectionDockingView | undefined = undefined;
     public static makeDocumentConfig(document: Doc, panelName?: string, width?: number, keyValue?: boolean) {
         return {
@@ -68,7 +70,7 @@ export class CollectionDockingView extends CollectionSubView() {
         super(props);
         makeObservable(this);
         if (this._props.renderDepth < 0) CollectionDockingView.Instance = this;
-        //Why is this here?
+        // Why is this here?
         (window as any).React = React;
         (window as any).ReactDOM = ReactDOM;
         DragManager.StartWindowDrag = this.StartOtherDrag;
@@ -201,6 +203,7 @@ export class CollectionDockingView extends CollectionSubView() {
             } else if (instance._goldenLayout.root.contentItems[0].isRow) {
                 // if row
                 switch (pullSide) {
+                    // eslint-disable-next-line default-case-last
                     default:
                     case OpenWhereMod.none:
                     case OpenWhereMod.right:
@@ -210,7 +213,7 @@ export class CollectionDockingView extends CollectionSubView() {
                         glayRoot.contentItems[0].addChild(newContentItem(), 0);
                         break;
                     case OpenWhereMod.top:
-                    case OpenWhereMod.bottom:
+                    case OpenWhereMod.bottom: {
                         // if not going in a row layout, must add already existing content into column
                         const rowlayout = glayRoot.contentItems[0];
                         const newColumn = rowlayout.layoutManager.createContentItem({ type: 'column' }, instance._goldenLayout);
@@ -229,6 +232,7 @@ export class CollectionDockingView extends CollectionSubView() {
 
                         rowlayout.config.height = 50;
                         newItem.config.height = 50;
+                    }
                 }
             } else {
                 // if (instance._goldenLayout.root.contentItems[0].isColumn) { // if column
@@ -241,7 +245,7 @@ export class CollectionDockingView extends CollectionSubView() {
                         break;
                     case 'left':
                     case 'right':
-                    default:
+                    default: {
                         // if not going in a row layout, must add already existing content into column
                         const collayout = glayRoot.contentItems[0];
                         const newRow = collayout.layoutManager.createContentItem({ type: 'row' }, instance._goldenLayout);
@@ -260,6 +264,7 @@ export class CollectionDockingView extends CollectionSubView() {
 
                         collayout.config.width = 50;
                         newItem.config.width = 50;
+                    }
                 }
             }
             instance._ignoreStateChange = JSON.stringify(instance._goldenLayout.toConfig());
@@ -278,10 +283,10 @@ export class CollectionDockingView extends CollectionSubView() {
     }
     setupGoldenLayout = async () => {
         if (this._unmounting) return;
-        //const config = StrCast(this.Document.dockingConfig, JSON.stringify(DashboardView.resetDashboard(this.Document)));
+        // const config = StrCast(this.Document.dockingConfig, JSON.stringify(DashboardView.resetDashboard(this.Document)));
         const config = StrCast(this.Document.dockingConfig);
         if (config) {
-            const matches = config.match(/\"documentId\":\"[a-z0-9-]+\"/g);
+            const matches = config.match(/"documentId":"[a-z0-9-]+"/g);
             const docids = matches?.map(m => m.replace('"documentId":"', '').replace('"', '')) ?? [];
 
             await Promise.all(docids.map(id => DocServer.GetRefField(id)));
@@ -289,12 +294,13 @@ export class CollectionDockingView extends CollectionSubView() {
             if (this._goldenLayout) {
                 if (config === JSON.stringify(this._goldenLayout.toConfig())) {
                     return;
-                } else {
-                    try {
-                        this._goldenLayout.unbind('tabCreated', this.tabCreated);
-                        this._goldenLayout.unbind('tabDestroyed', this.tabDestroyed);
-                        this._goldenLayout.unbind('stackCreated', this.stackCreated);
-                    } catch (e) {}
+                }
+                try {
+                    this._goldenLayout.unbind('tabCreated', this.tabCreated);
+                    this._goldenLayout.unbind('tabDestroyed', this.tabDestroyed);
+                    this._goldenLayout.unbind('stackCreated', this.stackCreated);
+                } catch (e) {
+                    /* empty */
                 }
                 this.tabMap.clear();
                 this._goldenLayout.destroy();
@@ -323,7 +329,7 @@ export class CollectionDockingView extends CollectionSubView() {
      */
     titleChanged = (target: any, value: any) => {
         const title = Field.toString(value);
-        if (title.startsWith('@') && !title.substring(1).match(/[\(\)\[\]@]/) && title.length > 1) {
+        if (title.startsWith('@') && !title.substring(1).match(/[()[\]@]/) && title.length > 1) {
             const embedding = DocListCast(target.proto_embeddings).lastElement();
             embedding && Doc.AddToMyPublished(embedding);
         } else if (!title.startsWith('@')) {
@@ -337,7 +343,7 @@ export class CollectionDockingView extends CollectionSubView() {
         if (this._containerRef.current) {
             this._lightboxReactionDisposer = reaction(
                 () => LightboxView.LightboxDoc,
-                doc => setTimeout(() => !doc && this.onResize(undefined))
+                doc => setTimeout(() => !doc && this.onResize())
             );
             new _global.ResizeObserver(this.onResize).observe(this._containerRef.current);
             this._reactionDisposer = reaction(
@@ -356,12 +362,12 @@ export class CollectionDockingView extends CollectionSubView() {
                 { fireImmediately: true }
             );
             reaction(
-                () => [SettingsManager.userBackgroundColor, SettingsManager.userBackgroundColor],
+                () => [SnappingManager.userBackgroundColor, SnappingManager.userBackgroundColor],
                 () => {
                     clearStyleSheetRules(CollectionDockingView._highlightStyleSheet);
-                    addStyleSheetRule(CollectionDockingView._highlightStyleSheet, 'lm_controls', { background: `${SettingsManager.userBackgroundColor} !important` });
-                    addStyleSheetRule(CollectionDockingView._highlightStyleSheet, 'lm_controls', { color: `${SettingsManager.userColor} !important` });
-                    addStyleSheetRule(SettingsManager._settingsStyle, 'lm_header', { background: `${SettingsManager.userBackgroundColor} !important` });
+                    addStyleSheetRule(CollectionDockingView._highlightStyleSheet, 'lm_controls', { background: `${SnappingManager.userBackgroundColor} !important` });
+                    addStyleSheetRule(CollectionDockingView._highlightStyleSheet, 'lm_controls', { color: `${SnappingManager.userColor} !important` });
+                    addStyleSheetRule(SnappingManager.SettingsStyle, 'lm_header', { background: `${SnappingManager.userBackgroundColor} !important` });
                 },
                 { fireImmediately: true }
             );
@@ -374,7 +380,9 @@ export class CollectionDockingView extends CollectionSubView() {
         try {
             this._goldenLayout.unbind('stackCreated', this.stackCreated);
             this._goldenLayout.unbind('tabDestroyed', this.tabDestroyed);
-        } catch (e) {}
+        } catch (e) {
+            /* empty */
+        }
         this._goldenLayout?.destroy();
         window.removeEventListener('resize', this.onResize);
         window.removeEventListener('mouseup', this.onPointerUp);
@@ -384,7 +392,7 @@ export class CollectionDockingView extends CollectionSubView() {
     };
 
     @action
-    onResize = (event: any) => {
+    onResize = () => {
         const cur = this._containerRef.current;
         // bcz: since GoldenLayout isn't a React component itself, we need to notify it to resize when its document container's size has changed
         !LightboxView.LightboxDoc && cur && this._goldenLayout?.updateSize(cur.getBoundingClientRect().width, cur.getBoundingClientRect().height);
@@ -392,7 +400,7 @@ export class CollectionDockingView extends CollectionSubView() {
 
     endUndoBatch = () => {
         const json = JSON.stringify(this._goldenLayout.toConfig());
-        const matches = json.match(/\"documentId\":\"[a-z0-9-]+\"/g);
+        const matches = json.match(/"documentId":"[a-z0-9-]+"/g);
         const docids = matches?.map(m => m.replace('"documentId":"', '').replace('"', ''));
         const docs = !docids
             ? []
@@ -415,7 +423,7 @@ export class CollectionDockingView extends CollectionSubView() {
     };
 
     @action
-    onPointerUp = (e: MouseEvent): void => {
+    onPointerUp = (): void => {
         window.removeEventListener('pointerup', this.onPointerUp);
         DragManager.CompleteWindowDrag = undefined;
         setTimeout(this.endUndoBatch, 100);
@@ -453,24 +461,27 @@ export class CollectionDockingView extends CollectionSubView() {
         if (content) {
             const _width = DivWidth(content);
             const _height = DivHeight(content);
-            return CollectionFreeFormView.UpdateIcon(this.layoutDoc[Id] + '-icon' + new Date().getTime(), content, _width, _height, _width, _height, 0, 1, true, this.layoutDoc[Id] + '-icon', (iconFile, _nativeWidth, _nativeHeight) => {
+            return CollectionFreeFormView.UpdateIcon(this.layoutDoc[Id] + '-icon' + new Date().getTime(), content, _width, _height, _width, _height, 0, 1, true, this.layoutDoc[Id] + '-icon', iconFile => {
                 const proto = this.dataDoc; // Cast(img.proto, Doc, null)!;
-                proto['thumb_nativeWidth'] = _width;
-                proto['thumb_nativeHeight'] = _height;
+                proto.thumb_nativeWidth = _width;
+                proto.thumb_nativeHeight = _height;
                 proto.thumb = new ImageField(iconFile);
             });
         }
+        return undefined;
     }
     public static async TakeSnapshot(doc: Doc | undefined, clone = false) {
         if (!doc) return undefined;
         let json = StrCast(doc.dockingConfig);
         if (clone) {
             const cloned = await Doc.MakeClone(doc);
-            Array.from(cloned.map.entries()).map(entry => (json = json.replace(entry[0], entry[1][Id])));
+            Array.from(cloned.map.entries()).forEach(entry => {
+                json = json.replace(entry[0], entry[1][Id]);
+            });
             cloned.clone[DocData].dockingConfig = json;
             return DashboardView.openDashboard(cloned.clone);
         }
-        const matches = json.match(/\"documentId\":\"[a-z0-9-]+\"/g);
+        const matches = json.match(/"documentId":"[a-z0-9-]+"/g);
         const origtabids = matches?.map(m => m.replace('"documentId":"', '').replace('"', '')) || [];
         const origtabs = origtabids
             .map(id => DocServer.GetCachedRefField(id))
@@ -508,19 +519,20 @@ export class CollectionDockingView extends CollectionSubView() {
 
     tabDestroyed = (tab: any) => {
         this._flush = this._flush ?? UndoManager.StartBatch('tab movement');
-        if (tab.DashDoc && ![DocumentType.PRES].includes(tab.DashDoc?.type) && !tab.contentItem.config.props.keyValue) {
-            Doc.AddDocToList(Doc.MyHeaderBar, 'data', tab.DashDoc, undefined, undefined, true);
+        const dashDoc = tab.DashDoc;
+        if (dashDoc && ![DocumentType.PRES].includes(dashDoc.type) && !tab.contentItem.config.props.keyValue) {
+            Doc.AddDocToList(Doc.MyHeaderBar, 'data', dashDoc, undefined, undefined, true);
             // if you close a tab that is not embedded somewhere else (an embedded Doc can be opened simultaneously in a tab), then add the tab to recently closed
-            if (tab.DashDoc.embedContainer === this.Document) tab.DashDoc.embedContainer = undefined;
-            if (!tab.DashDoc.embedContainer) {
-                Doc.AddDocToList(Doc.MyRecentlyClosed, 'data', tab.DashDoc, undefined, true, true);
-                Doc.RemoveEmbedding(tab.DashDoc, tab.DashDoc);
+            if (dashDoc.embedContainer === this.Document) dashDoc.embedContainer = undefined;
+            if (!dashDoc.embedContainer) {
+                Doc.AddDocToList(Doc.MyRecentlyClosed, 'data', dashDoc, undefined, true, true);
+                Doc.RemoveEmbedding(dashDoc, dashDoc);
             }
         }
         if (CollectionDockingView.Instance) {
             const dview = CollectionDockingView.Instance.Document;
-            const fieldKey = CollectionDockingView.Instance.props.fieldKey;
-            Doc.RemoveDocFromList(dview, fieldKey, tab.DashDoc);
+            const { fieldKey } = CollectionDockingView.Instance.props;
+            Doc.RemoveDocFromList(dview, fieldKey, dashDoc);
             this.tabMap.delete(tab);
             tab._disposers && Object.values(tab._disposers).forEach((disposer: any) => disposer?.());
             this.stateChanged();
@@ -531,8 +543,8 @@ export class CollectionDockingView extends CollectionSubView() {
         tab.contentItem.element[0]?.firstChild?.firstChild?.InitTab?.(tab); // have to explicitly initialize tabs that reuse contents from previous tabs (ie, when dragging a tab around a new tab is created for the old content)
     };
 
-    stackCreated = (stack: any) => {
-        stack = stack.header ? stack : stack.origin;
+    stackCreated = (stackIn: any) => {
+        const stack = stackIn.header ? stackIn : stackIn.origin;
         stack.header?.element.on('mousedown', (e: any) => {
             const dashboard = Doc.ActiveDashboard;
             if (dashboard && e.target === stack.header?.element[0] && e.button === 2) {
@@ -550,32 +562,29 @@ export class CollectionDockingView extends CollectionSubView() {
             }
         });
 
-        let addNewDoc = undoable(
-            action(() => {
-                const dashboard = Doc.ActiveDashboard;
-                if (dashboard) {
-                    dashboard.pane_count = NumCast(dashboard.pane_count) + 1;
-                    const docToAdd = Docs.Create.FreeformDocument([], {
-                        _width: this._props.PanelWidth(),
-                        _height: this._props.PanelHeight(),
-                        _layout_fitWidth: true,
-                        _freeform_backgroundGrid: true,
-                        title: `Untitled Tab ${NumCast(dashboard.pane_count)}`,
-                    });
-                    Doc.AddDocToList(Doc.MyHeaderBar, 'data', docToAdd, undefined, undefined, true);
-                    inheritParentAcls(this.dataDoc, docToAdd, false);
-                    CollectionDockingView.AddSplit(docToAdd, OpenWhereMod.none, stack);
-                }
-            }),
-            'add new tab'
-        );
+        const addNewDoc = undoable(() => {
+            const dashboard = Doc.ActiveDashboard;
+            if (dashboard) {
+                dashboard.pane_count = NumCast(dashboard.pane_count) + 1;
+                const docToAdd = Docs.Create.FreeformDocument([], {
+                    _width: this._props.PanelWidth(),
+                    _height: this._props.PanelHeight(),
+                    _layout_fitWidth: true,
+                    _freeform_backgroundGrid: true,
+                    title: `Untitled Tab ${NumCast(dashboard.pane_count)}`,
+                });
+                Doc.AddDocToList(Doc.MyHeaderBar, 'data', docToAdd, undefined, undefined, true);
+                inheritParentAcls(this.dataDoc, docToAdd, false);
+                CollectionDockingView.AddSplit(docToAdd, OpenWhereMod.none, stack);
+            }
+        }, 'add new tab');
 
         stack.header?.controlsContainer
-            .find('.lm_close') //get the close icon
-            .off('click') //unbind the current click handler
+            .find('.lm_close') // get the close icon
+            .off('click') // unbind the current click handler
             .click(
                 action(() => {
-                    //if (confirm('really close this?')) {
+                    // if (confirm('really close this?')) {
                     if ((!stack.parent.isRoot && !stack.parent.parent.isRoot) || stack.parent.contentItems.length > 1) {
                         const batch = UndoManager.StartBatch('close stack');
                         stack.remove();
@@ -595,11 +604,11 @@ export class CollectionDockingView extends CollectionSubView() {
             }
         });
         stack.header?.controlsContainer
-            .find('.lm_maximise') //get the close icon
+            .find('.lm_maximise') // get the close icon
             .click(() => setTimeout(this.stateChanged));
         stack.header?.controlsContainer
-            .find('.lm_popout') //get the popout icon
-            .off('click') //unbind the current click handler
+            .find('.lm_popout') // get the popout icon
+            .off('click') // unbind the current click handler
             .click(addNewDoc);
     };
 
@@ -609,13 +618,14 @@ export class CollectionDockingView extends CollectionSubView() {
             <div>
                 {href ? (
                     <img
+                        alt="thumbnail of nested dashboard"
                         style={{ background: 'white', top: 0, position: 'absolute' }}
                         src={href} // + '?d=' + (new Date()).getTime()}
                         width={this._props.PanelWidth()}
                         height={this._props.PanelHeight()}
                     />
                 ) : (
-                    <p>nested dashboards has no thumbnail</p>
+                    <p>nested dashboard has no thumbnail</p>
                 )}
             </div>
         ) : (
@@ -625,6 +635,7 @@ export class CollectionDockingView extends CollectionSubView() {
 }
 
 ScriptingGlobals.add(
+    // eslint-disable-next-line prefer-arrow-callback
     function openInLightbox(doc: any) {
         LightboxView.Instance.AddDocTab(doc, OpenWhere.lightbox);
     },
@@ -632,26 +643,37 @@ ScriptingGlobals.add(
     '(doc: any)'
 );
 ScriptingGlobals.add(
+    // eslint-disable-next-line prefer-arrow-callback
     function openDoc(doc: any, where: OpenWhere) {
         switch (where) {
             case OpenWhere.addRight:
                 return CollectionDockingView.AddSplit(doc, OpenWhereMod.right);
             case OpenWhere.overlay:
+            default:
                 // prettier-ignore
                 switch (doc) {
                     case '<ScriptingRepl />': return OverlayView.Instance.addWindow(<ScriptingRepl />, { x: 300, y: 100, width: 200, height: 200, title: 'Scripting REPL' });
                     case "<UndoStack />": return OverlayView.Instance.addWindow(<UndoStack />, { x: 300, y: 100, width: 200, height: 200, title: 'Undo stack' });
+                    default: 
                 }
                 Doc.AddToMyOverlay(doc);
+                return true;
         }
     },
     'opens up document in location specified',
     '(doc: any)'
 );
 ScriptingGlobals.add(
+    // eslint-disable-next-line prefer-arrow-callback
     function openRepl() {
         return 'openRepl';
     },
     'opens up document in screen overlay layer',
     '(doc: any)'
 );
+// eslint-disable-next-line prefer-arrow-callback
+ScriptingGlobals.add(async function snapshotDashboard() {
+    const batch = UndoManager.StartBatch('snapshot');
+    await CollectionDockingView.TakeSnapshot(Doc.ActiveDashboard);
+    batch.end();
+}, 'creates a snapshot copy of a dashboard');
diff --git a/src/client/views/collections/CollectionMasonryViewFieldRow.tsx b/src/client/views/collections/CollectionMasonryViewFieldRow.tsx
index 7dcfd32bd..2d906dfe7 100644
--- a/src/client/views/collections/CollectionMasonryViewFieldRow.tsx
+++ b/src/client/views/collections/CollectionMasonryViewFieldRow.tsx
@@ -2,7 +2,8 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { action, computed, makeObservable, observable, runInAction } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
-import { emptyFunction, numberRange, returnEmptyString, returnFalse, setupMoveUpEvents } from '../../../Utils';
+import { returnEmptyString, returnFalse, setupMoveUpEvents } from '../../../ClientUtils';
+import { emptyFunction, numberRange } from '../../../Utils';
 import { Doc, DocListCast } from '../../../fields/Doc';
 import { DocData } from '../../../fields/DocSymbols';
 import { PastelSchemaPalette, SchemaHeaderField } from '../../../fields/SchemaHeaderField';
diff --git a/src/client/views/collections/CollectionMenu.tsx b/src/client/views/collections/CollectionMenu.tsx
index 4f25f69ef..a23725348 100644
--- a/src/client/views/collections/CollectionMenu.tsx
+++ b/src/client/views/collections/CollectionMenu.tsx
@@ -4,7 +4,8 @@ import { Toggle, ToggleType, Type } from 'browndash-components';
 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 { ClientUtils, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue, setupMoveUpEvents } from '../../../ClientUtils';
+import { emptyFunction } from '../../../Utils';
 import { Doc, DocListCast, Opt } from '../../../fields/Doc';
 import { DocData } from '../../../fields/DocSymbols';
 import { List } from '../../../fields/List';
@@ -12,7 +13,8 @@ import { ObjectField } from '../../../fields/ObjectField';
 import { RichTextField } from '../../../fields/RichTextField';
 import { BoolCast, Cast, DocCast, NumCast, StrCast } from '../../../fields/Types';
 import { CollectionViewType, DocumentType } from '../../documents/DocumentTypes';
-import { DragManager, dropActionType } from '../../util/DragManager';
+import { DragManager } from '../../util/DragManager';
+import { dropActionType } from '../../util/DropActionTypes';
 import { SelectionManager } from '../../util/SelectionManager';
 import { SettingsManager } from '../../util/SettingsManager';
 import { Transform } from '../../util/Transform';
@@ -79,7 +81,7 @@ export class CollectionMenu extends AntimodeMenu<CollectionMenuProps> {
 
     buttonBarXf = () => {
         if (!this._docBtnRef.current) return Transform.Identity();
-        const { scale, translateX, translateY } = Utils.GetScreenTransform(this._docBtnRef.current);
+        const { scale, translateX, translateY } = ClientUtils.GetScreenTransform(this._docBtnRef.current);
         return new Transform(-translateX, -translateY, 1 / scale);
     };
 
@@ -123,7 +125,7 @@ export class CollectionMenu extends AntimodeMenu<CollectionMenuProps> {
         const propTitle = SettingsManager.Instance.propertiesWidth > 0 ? 'Close Properties' : 'Open Properties';
 
         const hardCodedButtons = (
-            <div className={`hardCodedButtons`}>
+            <div className="hardCodedButtons">
                 <Toggle
                     toggleType={ToggleType.BUTTON}
                     type={Type.PRIM}
@@ -145,7 +147,7 @@ export class CollectionMenu extends AntimodeMenu<CollectionMenuProps> {
             </div>
         );
 
-        //dash col linear view buttons
+        // dash col linear view buttons
         const contMenuButtons = (
             <div
                 className="collectionMenu-container"
@@ -172,7 +174,7 @@ const stopPropagation = (e: React.SyntheticEvent) => e.stopPropagation();
 
 @observer
 export class CollectionViewBaseChrome extends React.Component<CollectionViewMenuProps> {
-    //(!)?\(\(\(doc.(\w+) && \(doc.\w+ as \w+\).includes\(\"(\w+)\"\)
+    // (!)?\(\(\(doc.(\w+) && \(doc.\w+ as \w+\).includes\(\"(\w+)\"\)
 
     get document() {
         return this.props.docView?.Document;
@@ -206,14 +208,18 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewMenu
         params: ['target', 'source'],
         title: 'child click view',
         script: 'this.target.childClickedOpenTemplateView = getDocTemplate(this.source?.[0])',
-        immediate: undoBatch((source: Doc[]) => source.length && (this.target.childClickedOpenTemplateView = Doc.getDocTemplate(source?.[0]))),
+        immediate: undoBatch((source: Doc[]) => {
+            source.length && (this.target.childClickedOpenTemplateView = Doc.getDocTemplate(source?.[0]));
+        }),
         initialize: emptyFunction,
     };
     _contentCommand = {
         params: ['target', 'source'],
         title: 'set content',
         script: 'getProto(this.target).data = copyField(this.source);',
-        immediate: undoBatch((source: Doc[]) => (this.target[DocData].data = new List<Doc>(source))),
+        immediate: undoBatch((source: Doc[]) => {
+            this.target[DocData].data = new List<Doc>(source);
+        }),
         initialize: emptyFunction,
     };
     _onClickCommand = {
@@ -224,14 +230,14 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewMenu
              getProto(this.proxy[0]).target = this.target.target;
              getProto(this.proxy[0]).source = copyField(this.target.source); 
             }}`,
-        immediate: undoBatch((source: Doc[]) => {}),
+        immediate: undoBatch(() => {}),
         initialize: emptyFunction,
     };
     _viewCommand = {
         params: ['target'],
         title: 'bookmark view',
         script: "this.target._freeform_panX = self['target-freeform_panX']; this.target._freeform_panY = this['target-freeform_panY']; this.target._freeform_scale = this['target_freeform_scale']; gotoFrame(this.target, this['target-currentFrame']);",
-        immediate: undoBatch((source: Doc[]) => {
+        immediate: undoBatch(() => {
             this.target._freeform_panX = 0;
             this.target._freeform_panY = 0;
             this.target._freeform_scale = 1;
@@ -248,14 +254,18 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewMenu
         params: ['target'],
         title: 'fit content',
         script: 'this.target._freeform_fitContentsToBox = !this.target._freeform_fitContentsToBox;',
-        immediate: undoBatch((source: Doc[]) => (this.target._freeform_fitContentsToBox = !this.target._freeform_fitContentsToBox)),
+        immediate: undoBatch(() => {
+            this.target._freeform_fitContentsToBox = !this.target._freeform_fitContentsToBox;
+        }),
         initialize: emptyFunction,
     };
     _fitContentCommand = {
         params: ['target'],
         title: 'toggle clusters',
         script: 'this.target._freeform_useClusters = !this.target._freeform_useClusters;',
-        immediate: undoBatch((source: Doc[]) => (this.target._freeform_useClusters = !this.target._freeform_useClusters)),
+        immediate: undoBatch(() => {
+            this.target._freeform_useClusters = !this.target._freeform_useClusters;
+        }),
         initialize: emptyFunction,
     };
     _saveFilterCommand = {
@@ -263,7 +273,7 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewMenu
         title: 'save filter',
         script: `this.target._childFilters = compareLists(this['target-childFilters'],this.target._childFilters) ? undefined : copyField(this['target-childFilters']); 
         this.target._searchFilterDocs = compareLists(this['target-searchFilterDocs'],this.target._searchFilterDocs) ? undefined: copyField(this['target-searchFilterDocs']);`,
-        immediate: undoBatch((source: Doc[]) => {
+        immediate: undoBatch(() => {
             this.target._childFilters = undefined;
             this.target._searchFilterDocs = undefined;
         }),
@@ -332,12 +342,10 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewMenu
     @undoBatch
     viewChanged = (e: React.ChangeEvent) => {
         const target = this.document !== Doc.MyLeftSidebarPanel ? this.document : DocCast(this.document.proto);
-        //@ts-ignore
         target._type_collection = e.target.selectedOptions[0].value;
     };
 
     commandChanged = (e: React.ChangeEvent) => {
-        //@ts-ignore
         runInAction(() => (this._currentKey = e.target.selectedOptions[0].value));
     };
 
diff --git a/src/client/views/collections/CollectionNoteTakingView.tsx b/src/client/views/collections/CollectionNoteTakingView.tsx
index d8a0aebb1..4ceeb631d 100644
--- a/src/client/views/collections/CollectionNoteTakingView.tsx
+++ b/src/client/views/collections/CollectionNoteTakingView.tsx
@@ -1,7 +1,8 @@
 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';
+import { ClientUtils, DivHeight, lightOrDark, returnZero, smoothScroll } from '../../../ClientUtils';
+import { Doc, Field, FieldType, Opt } from '../../../fields/Doc';
 import { DocData } from '../../../fields/DocSymbols';
 import { Copy, Id } from '../../../fields/FieldSymbols';
 import { List } from '../../../fields/List';
@@ -9,14 +10,16 @@ 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, lightOrDark, returnZero, smoothScroll, Utils } from '../../../Utils';
+import { emptyFunction } from '../../../Utils';
 import { Docs, DocUtils } from '../../documents/Documents';
-import { DragManager, dropActionType } from '../../util/DragManager';
+import { DragManager } from '../../util/DragManager';
+import { dropActionType } from '../../util/DropActionTypes';
 import { SnappingManager } from '../../util/SnappingManager';
 import { Transform } from '../../util/Transform';
 import { undoable, undoBatch } from '../../util/UndoManager';
 import { ContextMenu } from '../ContextMenu';
 import { ContextMenuProps } from '../ContextMenuItem';
+import { FieldsDropdown } from '../FieldsDropdown';
 import { Colors } from '../global/globalEnums';
 import { LightboxView } from '../LightboxView';
 import { DocumentView } from '../nodes/DocumentView';
@@ -27,7 +30,7 @@ import './CollectionNoteTakingView.scss';
 import { CollectionNoteTakingViewColumn } from './CollectionNoteTakingViewColumn';
 import { CollectionNoteTakingViewDivider } from './CollectionNoteTakingViewDivider';
 import { CollectionSubView } from './CollectionSubView';
-import { FieldsDropdown } from '../FieldsDropdown';
+
 const _global = (window /* browser */ || global) /* node */ as any;
 
 /**
@@ -300,7 +303,7 @@ export class CollectionNoteTakingView extends CollectionSubView() {
     // getDocTransform is used to get the coordinates of a document when we go from a view like freeform to columns
     getDocTransform(doc: Doc, dref?: DocumentView) {
         const y = this._scroll; // required for document decorations to update when the text box container is scrolled
-        const { translateX, translateY } = Utils.GetScreenTransform(dref?.ContentDiv || undefined);
+        const { translateX, translateY } = ClientUtils.GetScreenTransform(dref?.ContentDiv || undefined);
         // the document view may center its contents and if so, will prepend that onto the screenToLocalTansform.  so we have to subtract that off
         return new Transform(-translateX + (dref?.centeringX || 0), -translateY + (dref?.centeringY || 0), 1).scale(this.ScreenToLocalBoxXf().Scale);
     }
@@ -308,7 +311,7 @@ export class CollectionNoteTakingView extends CollectionSubView() {
     // how to get the width of a document. Currently returns the width of the column (minus margins)
     // if a note doc. Otherwise, returns the normal width (for graphs, images, etc...)
     getDocWidth(d: Doc) {
-        const heading = !d[this.notetakingCategoryField] ? 'unset' : Field.toString(d[this.notetakingCategoryField] as Field);
+        const heading = !d[this.notetakingCategoryField] ? 'unset' : Field.toString(d[this.notetakingCategoryField] as FieldType);
         const existingHeader = this.colHeaderData.find(sh => sh.heading === heading);
         const existingWidth = this.layoutDoc._notetaking_columns_autoSize ? 1 / (this.colHeaderData.length ?? 1) : existingHeader?.width ? existingHeader.width : 0;
         const maxWidth = existingWidth > 0 ? existingWidth * this.availableWidth : this.maxColWidth;
diff --git a/src/client/views/collections/CollectionNoteTakingViewColumn.tsx b/src/client/views/collections/CollectionNoteTakingViewColumn.tsx
index 448b11b05..e00fdb50c 100644
--- a/src/client/views/collections/CollectionNoteTakingViewColumn.tsx
+++ b/src/client/views/collections/CollectionNoteTakingViewColumn.tsx
@@ -2,7 +2,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { action, computed, makeObservable, observable } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
-import { lightOrDark, returnEmptyString } from '../../../Utils';
+import { lightOrDark, returnEmptyString } from '../../../ClientUtils';
 import { Doc, DocListCast, Opt } from '../../../fields/Doc';
 import { RichTextField } from '../../../fields/RichTextField';
 import { listSpec } from '../../../fields/Schema';
diff --git a/src/client/views/collections/CollectionNoteTakingViewDivider.tsx b/src/client/views/collections/CollectionNoteTakingViewDivider.tsx
index 50a97b978..67070b24d 100644
--- a/src/client/views/collections/CollectionNoteTakingViewDivider.tsx
+++ b/src/client/views/collections/CollectionNoteTakingViewDivider.tsx
@@ -1,7 +1,8 @@
 import { action, observable } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
-import { emptyFunction, setupMoveUpEvents } from '../../../Utils';
+import { emptyFunction } from '../../../Utils';
+import { setupMoveUpEvents } from '../../../ClientUtils';
 import { UndoManager } from '../../util/UndoManager';
 import { ObservableReactComponent } from '../ObservableReactComponent';
 
diff --git a/src/client/views/collections/CollectionPileView.tsx b/src/client/views/collections/CollectionPileView.tsx
index 9d68c621b..e39f1c700 100644
--- a/src/client/views/collections/CollectionPileView.tsx
+++ b/src/client/views/collections/CollectionPileView.tsx
@@ -1,12 +1,13 @@
 import { action, computed, IReactionDisposer, makeObservable } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
+import { returnFalse, setupMoveUpEvents } from '../../../ClientUtils';
 import { Doc, DocListCast } from '../../../fields/Doc';
 import { ScriptField } from '../../../fields/ScriptField';
 import { NumCast, StrCast } from '../../../fields/Types';
-import { emptyFunction, returnFalse, setupMoveUpEvents } from '../../../Utils';
+import { emptyFunction } from '../../../Utils';
 import { DocUtils } from '../../documents/Documents';
-import { dropActionType } from '../../util/DragManager';
+import { dropActionType } from '../../util/DropActionTypes';
 import { SelectionManager } from '../../util/SelectionManager';
 import { undoBatch, UndoManager } from '../../util/UndoManager';
 import { OpenWhere } from '../nodes/DocumentView';
diff --git a/src/client/views/collections/CollectionStackedTimeline.tsx b/src/client/views/collections/CollectionStackedTimeline.tsx
index 656f850b3..3f6638b25 100644
--- a/src/client/views/collections/CollectionStackedTimeline.tsx
+++ b/src/client/views/collections/CollectionStackedTimeline.tsx
@@ -2,6 +2,7 @@ import { action, computed, IReactionDisposer, makeObservable, observable, reacti
 import { observer } from 'mobx-react';
 import { computedFn } from 'mobx-utils';
 import * as React from 'react';
+import { returnEmptyDoclist, returnEmptyFilter, returnFalse, returnNone, returnTrue, returnZero, setupMoveUpEvents, smoothScrollHorizontal, StopEvent } from '../../../ClientUtils';
 import { Doc, Opt } from '../../../fields/Doc';
 import { DocData } from '../../../fields/DocSymbols';
 import { Id } from '../../../fields/FieldSymbols';
@@ -10,7 +11,7 @@ import { listSpec } from '../../../fields/Schema';
 import { ComputedField, ScriptField } from '../../../fields/ScriptField';
 import { Cast, NumCast } from '../../../fields/Types';
 import { ImageField } from '../../../fields/URLField';
-import { emptyFunction, formatTime, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnNone, returnTrue, returnZero, setupMoveUpEvents, smoothScrollHorizontal, StopEvent } from '../../../Utils';
+import { emptyFunction, formatTime } from '../../../Utils';
 import { Docs } from '../../documents/Documents';
 import { DocumentType } from '../../documents/DocumentTypes';
 import { DocumentManager } from '../../util/DocumentManager';
diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx
index bf0393883..b79d660c6 100644
--- a/src/client/views/collections/CollectionStackingView.tsx
+++ b/src/client/views/collections/CollectionStackingView.tsx
@@ -1,8 +1,10 @@
+/* eslint-disable react/jsx-props-no-spreading */
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import * as CSS from 'csstype';
 import { action, computed, IReactionDisposer, makeObservable, observable, ObservableMap, reaction, runInAction } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
+import { ClientUtils, DivHeight, returnEmptyDoclist, returnNone, returnZero, setupMoveUpEvents, smoothScroll } from '../../../ClientUtils';
 import { Doc, Opt } from '../../../fields/Doc';
 import { DocData } from '../../../fields/DocSymbols';
 import { Id } from '../../../fields/FieldSymbols';
@@ -11,10 +13,11 @@ import { listSpec } from '../../../fields/Schema';
 import { SchemaHeaderField } from '../../../fields/SchemaHeaderField';
 import { BoolCast, Cast, DocCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
 import { TraceMobx } from '../../../fields/util';
-import { DivHeight, emptyFunction, returnEmptyDoclist, returnNone, returnZero, setupMoveUpEvents, smoothScroll, Utils } from '../../../Utils';
+import { emptyFunction } from '../../../Utils';
 import { Docs, DocUtils } from '../../documents/Documents';
 import { CollectionViewType } from '../../documents/DocumentTypes';
-import { DragManager, dropActionType } from '../../util/DragManager';
+import { DragManager } from '../../util/DragManager';
+import { dropActionType } from '../../util/DropActionTypes';
 import { SettingsManager } from '../../util/SettingsManager';
 import { Transform } from '../../util/Transform';
 import { undoBatch, UndoManager } from '../../util/UndoManager';
@@ -31,6 +34,7 @@ import { CollectionMasonryViewFieldRow } from './CollectionMasonryViewFieldRow';
 import './CollectionStackingView.scss';
 import { CollectionStackingViewFieldColumn } from './CollectionStackingViewFieldColumn';
 import { CollectionSubView } from './CollectionSubView';
+
 const _global = (window /* browser */ || global) /* node */ as any;
 
 export type collectionStackingViewProps = {
@@ -125,11 +129,16 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
 
     // TODO: plj - these are the children
     children = (docs: Doc[]) => {
-        //TODO: can somebody explain me to what exactly TraceMobX is?
+        // TODO: can somebody explain me to what exactly TraceMobX is?
         TraceMobx();
         // appears that we are going to reset the _docXfs. TODO: what is Xfs?
         this._docXfs.length = 0;
-        this._renderCount < docs.length && setTimeout(action(() => (this._renderCount = Math.min(docs.length, this._renderCount + 5))));
+        this._renderCount < docs.length &&
+            setTimeout(
+                action(() => {
+                    this._renderCount = Math.min(docs.length, this._renderCount + 5);
+                })
+            );
         return docs.map((d, i) => {
             const height = () => this.getDocHeight(d);
             const width = () => this.getDocWidth(d);
@@ -152,19 +161,21 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
     };
 
     // is sections that all collections inherit? I think this is how we show the masonry/columns
-    //TODO: this seems important
+    // TODO: this seems important
     get Sections() {
         // appears that pivot field IS actually for sorting
         if (!this.pivotField || this.colHeaderData instanceof Promise) return new Map<SchemaHeaderField, Doc[]>();
 
         if (this.colHeaderData === undefined) {
-            setTimeout(() => (this.dataDoc['_' + this.fieldKey + '_columnHeaders'] = new List<SchemaHeaderField>()), 0);
+            setTimeout(() => {
+                this.dataDoc['_' + this.fieldKey + '_columnHeaders'] = new List<SchemaHeaderField>();
+            });
             return new Map<SchemaHeaderField, Doc[]>();
         }
         const colHeaderData = Array.from(this.colHeaderData);
         const fields = new Map<SchemaHeaderField, Doc[]>(colHeaderData.map(sh => [sh, []] as [SchemaHeaderField, []]));
         let changed = false;
-        this.filteredChildren.map(d => {
+        this.filteredChildren.forEach(d => {
             const sectionValue = (d[this.pivotField] ? d[this.pivotField] : `NO ${this.pivotField.toUpperCase()} VALUE`) as object;
             // the next five lines ensures that floating point rounding errors don't create more than one section -syip
             const parsed = parseInt(sectionValue.toString());
@@ -186,7 +197,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
         if (this.layoutDoc._columnsHideIfEmpty) {
             Array.from(fields.keys())
                 .filter(key => !fields.get(key)!.length)
-                .map(header => {
+                .forEach(header => {
                     fields.delete(header);
                     colHeaderData.splice(colHeaderData.indexOf(header), 1);
                     changed = true;
@@ -207,11 +218,13 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
         // reset section headers when a new filter is inputted
         this._disposers.pivotField = reaction(
             () => this.pivotField,
-            () => (this.dataDoc['_' + this.fieldKey + '_columnHeaders'] = new List())
+            () => {
+                this.dataDoc['_' + this.fieldKey + '_columnHeaders'] = new List();
+            }
         );
         this._disposers.autoHeight = reaction(
             () => this.layoutDoc._layout_autoHeight,
-            layout_autoHeight => layout_autoHeight && this._props.setHeight?.(this.headerMargin + (this.isStackingView ? Math.max(...this._refList.map(DivHeight)) : this._refList.reduce((p, r) => p + DivHeight(r), 0)))
+            layoutAutoHeight => layoutAutoHeight && this._props.setHeight?.(this.headerMargin + (this.isStackingView ? Math.max(...this._refList.map(DivHeight)) : this._refList.reduce((p, r) => p + DivHeight(r), 0)))
         );
         this._disposers.refList = reaction(
             () => ({ refList: this._refList.slice(), autoHeight: this.layoutDoc._layout_autoHeight && !LightboxView.Contains(this.DocumentView?.()) }),
@@ -231,9 +244,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
 
     isAnyChildContentActive = () => this._props.isAnyChildContentActive();
 
-    moveDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (doc: Doc | Doc[]) => boolean): boolean => {
-        return this._props.removeDocument?.(doc) && addDocument?.(doc) ? true : false;
-    };
+    moveDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (doc: Doc | Doc[]) => boolean): boolean => !!(this._props.removeDocument?.(doc) && addDocument?.(doc));
 
     onChildClickHandler = () => this._props.childClickScript || ScriptCast(this.Document.onChildClick);
     @computed get onChildDoubleClickHandler() {
@@ -250,10 +261,10 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
 
         const found = this._mainCont && Array.from(this._mainCont.getElementsByClassName('documentView-node')).find((node: any) => node.id === doc[Id]);
         if (found) {
-            const top = found.getBoundingClientRect().top;
+            const { top } = found.getBoundingClientRect();
             const localTop = this.ScreenToLocalBoxXf().transformPoint(0, top);
             if (Math.floor(localTop[1]) !== 0) {
-                let focusSpeed = options.zoomTime ?? 500;
+                const focusSpeed = options.zoomTime ?? 500;
                 smoothScroll(focusSpeed, this._mainCont!, localTop[1] + this._mainCont!.scrollTop, options.easeFunc);
                 return focusSpeed;
             }
@@ -276,18 +287,18 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
     onKeyDown = (e: React.KeyboardEvent, fieldProps: FieldViewProps) => {
         if (['Enter'].includes(e.key) && e.ctrlKey) {
             e.stopPropagation?.();
-            const below = !e.altKey && e.key !== 'Tab';
-            const layout_fieldKey = StrCast(fieldProps.fieldKey);
+            const layoutFieldKey = StrCast(fieldProps.fieldKey);
             const newDoc = Doc.MakeCopy(fieldProps.Document, true);
             const dataField = fieldProps.Document[Doc.LayoutFieldKey(newDoc)];
             newDoc[DocData][Doc.LayoutFieldKey(newDoc)] = dataField === undefined || Cast(dataField, listSpec(Doc), null)?.length !== undefined ? new List<Doc>([]) : undefined;
-            if (layout_fieldKey !== 'layout' && fieldProps.Document[layout_fieldKey] instanceof Doc) {
-                newDoc[layout_fieldKey] = fieldProps.Document[layout_fieldKey];
+            if (layoutFieldKey !== 'layout' && fieldProps.Document[layoutFieldKey] instanceof Doc) {
+                newDoc[layoutFieldKey] = fieldProps.Document[layoutFieldKey];
             }
             newDoc[DocData].text = undefined;
             FormattedTextBox.SetSelectOnLoad(newDoc);
             return this.addDocument?.(newDoc);
         }
+        return false;
     };
     isContentActive = () => (this._props.isContentActive() ? true : this._props.isSelected() === false || this._props.isContentActive() === false ? false : undefined);
 
@@ -363,7 +374,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
     getDocTransform(doc: Doc) {
         const dref = this.docRefs.get(doc);
         this._scroll; // must be referenced for document decorations to update when the text box container is scrolled
-        const { translateX, translateY } = Utils.GetScreenTransform(dref?.ContentDiv);
+        const { translateX, translateY } = ClientUtils.GetScreenTransform(dref?.ContentDiv);
         // the document view may center its contents and if so, will prepend that onto the screenToLocalTansform.  so we have to subtract that off
         return new Transform(-translateX + (dref?.centeringX || 0), -translateY + (dref?.centeringY || 0), 1).scale(this.ScreenToLocalBoxXf().Scale);
     }
@@ -399,7 +410,9 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
 
     // This following three functions must be from the view Mehek showed
     columnDividerDown = (e: React.PointerEvent) => {
-        runInAction(() => (this._cursor = 'grabbing'));
+        runInAction(() => {
+            this._cursor = 'grabbing';
+        });
         const batch = UndoManager.StartBatch('stacking width');
         setupMoveUpEvents(
             this,
@@ -425,7 +438,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
                 onPointerDown={this.columnDividerDown}
                 ref={this._draggerRef}
                 style={{ cursor: this._cursor, color: SettingsManager.userColor, left: `${this.columnWidth + this.xMargin}px`, top: `${Math.max(0, this.yMargin - 9)}px` }}>
-                <FontAwesomeIcon icon={'arrows-alt-h'} />
+                <FontAwesomeIcon icon="arrows-alt-h" />
             </div>
         );
     }
@@ -439,7 +452,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
         let dropAfter = 0;
         if (de.complete.docDragData) {
             // going to re-add the docs to the _docXFs based on position of where we just dropped
-            this._docXfs.map((cd, i) => {
+            this._docXfs.forEach((cd, i) => {
                 const pos = cd
                     .stackedDocTransform()
                     .inverse()
@@ -496,7 +509,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
     onExternalDrop = async (e: React.DragEvent): Promise<void> => {
         const where = [e.clientX, e.clientY];
         let targInd = -1;
-        this._docXfs.map((cd, i) => {
+        this._docXfs.forEach((cd, i) => {
             const pos = cd
                 .stackedDocTransform()
                 .inverse()
@@ -521,10 +534,11 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
     // what a section looks like if we're in stacking view
     sectionStacking = (heading: SchemaHeaderField | undefined, docList: Doc[]) => {
         const key = this.pivotField;
-        let type: 'string' | 'number' | 'bigint' | 'boolean' | 'symbol' | 'undefined' | 'object' | 'function' | undefined = undefined;
+        let type: 'string' | 'number' | 'bigint' | 'boolean' | 'symbol' | 'undefined' | 'object' | 'function' | undefined;
         if (this.pivotField) {
             const types = docList.length ? docList.map(d => typeof d[key]) : this.filteredChildren.map(d => typeof d[key]);
             if (types.map((i, idx) => types.indexOf(i) === idx).length === 1) {
+                // eslint-disable-next-line prefer-destructuring
                 type = types[0];
             }
         }
@@ -557,9 +571,10 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
     // what a section looks like if we're in masonry. Shouldn't actually need to use this.
     sectionMasonry = (heading: SchemaHeaderField | undefined, docList: Doc[], first: boolean) => {
         const key = this.pivotField;
-        let type: 'string' | 'number' | 'bigint' | 'boolean' | 'symbol' | 'undefined' | 'object' | 'function' | undefined = undefined;
+        let type: 'string' | 'number' | 'bigint' | 'boolean' | 'symbol' | 'undefined' | 'object' | 'function' | undefined;
         const types = docList.length ? docList.map(d => typeof d[key]) : this.filteredChildren.map(d => typeof d[key]);
         if (types.map((i, idx) => types.indexOf(i) === idx).length === 1) {
+            // eslint-disable-next-line prefer-destructuring
             type = types[0];
         }
         const rows = () => (!this.isStackingView ? 1 : Math.max(1, Math.min(docList.length, Math.floor((this._props.PanelWidth() - 2 * this.xMargin) / (this.columnWidth + this.gridGap)))));
@@ -609,9 +624,9 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
             const cm = ContextMenu.Instance;
             const options = cm.findByDescription('Options...');
             const optionItems: ContextMenuProps[] = options && 'subitems' in options ? options.subitems : [];
-            optionItems.push({ description: `${this.layoutDoc._columnsFill ? 'Variable Size' : 'Autosize'} Column`, event: () => (this.layoutDoc._columnsFill = !this.layoutDoc._columnsFill), icon: 'plus' });
-            optionItems.push({ description: `${this.layoutDoc._layout_autoHeight ? 'Variable Height' : 'Auto Height'}`, event: () => (this.layoutDoc._layout_autoHeight = !this.layoutDoc._layout_autoHeight), icon: 'plus' });
-            optionItems.push({ description: 'Clear All', event: () => (this.dataDoc[this.fieldKey ?? 'data'] = new List([])), icon: 'times' });
+            optionItems.push({ description: `${this.layoutDoc._columnsFill ? 'Variable Size' : 'Autosize'} Column`, event: () => { this.layoutDoc._columnsFill = !this.layoutDoc._columnsFill; }, icon: 'plus' }); // prettier-ignore
+            optionItems.push({ description: `${this.layoutDoc._layout_autoHeight ? 'Variable Height' : 'Auto Height'}`, event: () => { this.layoutDoc._layout_autoHeight = !this.layoutDoc._layout_autoHeight; }, icon: 'plus' }); // prettier-ignore
+            optionItems.push({ description: 'Clear All', event: () => { this.dataDoc[this.fieldKey ?? 'data'] = new List([]); } , icon: 'times' }); // prettier-ignore
             !options && cm.addItem({ description: 'Options...', subitems: optionItems, icon: 'compass' });
         }
     };
@@ -700,7 +715,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
                         className={this.isStackingView ? 'collectionStackingView' : 'collectionMasonryView'}
                         ref={ele => {
                             this._masonryGridRef = ele;
-                            this.createDashEventsTarget(ele); //so the whole grid is the drop target?
+                            this.createDashEventsTarget(ele); // so the whole grid is the drop target?
                             this._oldWheel?.removeEventListener('wheel', this.onPassiveWheel);
                             this._oldWheel = ele;
                             // prevent wheel events from passively propagating up through containers and prevents containers from preventDefault which would block scrolling
@@ -711,7 +726,9 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
                             background: this._props.styleProvider?.(this.Document, this._props, StyleProp.BackgroundColor),
                             pointerEvents: (this._props.pointerEvents?.() as any) ?? this.backgroundEvents,
                         }}
-                        onScroll={action(e => (this._scroll = e.currentTarget.scrollTop))}
+                        onScroll={action(e => {
+                            this._scroll = e.currentTarget.scrollTop;
+                        })}
                         onDrop={this.onExternalDrop.bind(this)}
                         onContextMenu={this.onContextMenu}
                         onWheel={e => this.isContentActive() && e.stopPropagation()}>
diff --git a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
index c5292f880..35f330b44 100644
--- a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
+++ b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
@@ -1,7 +1,11 @@
+/* eslint-disable jsx-a11y/control-has-associated-label */
+/* eslint-disable jsx-a11y/no-static-element-interactions */
+/* eslint-disable jsx-a11y/click-events-have-key-events */
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { action, computed, IReactionDisposer, makeObservable, observable, reaction } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
+import { DivHeight, DivWidth, returnEmptyString, setupMoveUpEvents } from '../../../ClientUtils';
 import { Doc, DocListCast, Opt } from '../../../fields/Doc';
 import { RichTextField } from '../../../fields/RichTextField';
 import { PastelSchemaPalette, SchemaHeaderField } from '../../../fields/SchemaHeaderField';
@@ -9,10 +13,11 @@ import { ScriptField } from '../../../fields/ScriptField';
 import { BoolCast, NumCast } from '../../../fields/Types';
 import { ImageField } from '../../../fields/URLField';
 import { TraceMobx } from '../../../fields/util';
-import { DivHeight, DivWidth, emptyFunction, returnEmptyString, setupMoveUpEvents } from '../../../Utils';
+import { emptyFunction } from '../../../Utils';
 import { Docs, DocUtils } from '../../documents/Documents';
 import { DocumentType } from '../../documents/DocumentTypes';
-import { DragManager, dropActionType } from '../../util/DragManager';
+import { DragManager } from '../../util/DragManager';
+import { dropActionType } from '../../util/DropActionTypes';
 import { SnappingManager } from '../../util/SnappingManager';
 import { Transform } from '../../util/Transform';
 import { undoBatch } from '../../util/UndoManager';
@@ -95,7 +100,7 @@ export class CollectionStackingViewFieldColumn extends ObservableReactComponent<
         this._ele && this.props.refList.push(this._ele);
         this._disposers.collapser = reaction(
             () => this._props.headingObject?.collapsed,
-            collapsed => (this.collapsed = collapsed !== undefined ? BoolCast(collapsed) : false),
+            collapsed => { this.collapsed = collapsed !== undefined ? BoolCast(collapsed) : false; }, // prettier-ignore
             { fireImmediately: true }
         );
     }
@@ -105,7 +110,6 @@ export class CollectionStackingViewFieldColumn extends ObservableReactComponent<
         this._ele = null;
     }
 
-    //TODO: what is scripting? I found it in SetInPlace def but don't know what that is
     @undoBatch
     columnDrop = action((e: Event, de: DragManager.DropEvent) => {
         const drop = { docs: de.complete.docDragData?.droppedDocuments, val: this.getValue(this._heading) };
@@ -121,13 +125,13 @@ export class CollectionStackingViewFieldColumn extends ObservableReactComponent<
     };
 
     @action
-    headingChanged = (value: string, shiftDown?: boolean) => {
+    headingChanged = (value: string /* , shiftDown?: boolean */) => {
         const castedValue = this.getValue(value);
         if (castedValue) {
             if (this._props.colHeaderData?.map(i => i.heading).indexOf(castedValue.toString()) !== -1) {
                 return false;
             }
-            this._props.pivotField && this._props.docList.forEach(d => (d[this._props.pivotField] = castedValue));
+            this._props.pivotField && this._props.docList.forEach(d => { d[this._props.pivotField] = castedValue; }) // prettier-ignore
             if (this._props.headingObject) {
                 this._props.headingObject.setHeading(castedValue.toString());
                 this._heading = this._props.headingObject.heading;
@@ -143,9 +147,9 @@ export class CollectionStackingViewFieldColumn extends ObservableReactComponent<
         this._color = color;
     };
 
-    @action pointerEntered = () => SnappingManager.IsDragging && (this._background = '#b4b4b4');
-    @action pointerLeave = () => (this._background = 'inherit');
-    @undoBatch typedNote = (char: string) => this.addNewTextDoc('-typed text-', false, true);
+    @action pointerEntered = () => { SnappingManager.IsDragging && (this._background = '#b4b4b4'); } // prettier-ignore
+    @action pointerLeave = () => { this._background = 'inherit'}; // prettier-ignore
+    @undoBatch typedNote = () => this.addNewTextDoc('-typed text-', false, true);
 
     @action
     addNewTextDoc = (value: string, shiftDown?: boolean, forceEmptyNote?: boolean) => {
@@ -163,7 +167,10 @@ export class CollectionStackingViewFieldColumn extends ObservableReactComponent<
 
     @action
     deleteColumn = () => {
-        this._props.pivotField && this._props.docList.forEach(d => (d[this._props.pivotField] = undefined));
+        this._props.pivotField &&
+            this._props.docList.forEach(d => {
+                d[this._props.pivotField] = undefined;
+            });
         if (this._props.colHeaderData && this._props.headingObject) {
             const index = this._props.colHeaderData.indexOf(this._props.headingObject);
             this._props.colHeaderData.splice(index, 1);
@@ -178,8 +185,8 @@ export class CollectionStackingViewFieldColumn extends ObservableReactComponent<
 
     headerDown = (e: React.PointerEvent<HTMLDivElement>) => setupMoveUpEvents(this, e, this.startDrag, emptyFunction, emptyFunction);
 
-    //TODO: I think this is where I'm supposed to edit stuff
-    startDrag = (e: PointerEvent, down: number[], delta: number[]) => {
+    // TODO: I think this is where I'm supposed to edit stuff
+    startDrag = (e: PointerEvent) => {
         // is MakeEmbedding a way to make a copy of a doc without rendering it?
         const embedding = Doc.MakeEmbedding(this._props.Document);
         embedding._width = this._props.columnWidth / (this._props.colHeaderData?.length || 1);
@@ -210,23 +217,23 @@ export class CollectionStackingViewFieldColumn extends ObservableReactComponent<
         );
     };
 
-    renderMenu = () => {
-        return (
-            <div className="collectionStackingView-optionPicker">
-                <div className="optionOptions">
-                    <div className={'optionPicker' + (true ? ' active' : '')} onClick={action(() => {})}>
-                        Add options here
-                    </div>
+    renderMenu = () => (
+        <div className="collectionStackingView-optionPicker">
+            <div className="optionOptions">
+                <div className={'optionPicker' + (true ? ' active' : '')} onClick={action(() => {})}>
+                    Add options here
                 </div>
             </div>
-        );
-    };
+        </div>
+    );
 
     @observable private collapsed: boolean = false;
 
-    private toggleVisibility = action(() => (this.collapsed = !this.collapsed));
+    private toggleVisibility = action(() => {
+        this.collapsed = !this.collapsed;
+    });
 
-    menuCallback = (x: number, y: number) => {
+    menuCallback = () => {
         ContextMenu.Instance.clearItems();
         const layoutItems: ContextMenuProps[] = [];
         const docItems: ContextMenuProps[] = [];
@@ -257,6 +264,7 @@ export class CollectionStackingViewFieldColumn extends ObservableReactComponent<
                             }
                             return this._props.addDocument?.(created);
                         }
+                        return false;
                     },
                     icon: 'compress-arrows-alt',
                 })
@@ -276,6 +284,7 @@ export class CollectionStackingViewFieldColumn extends ObservableReactComponent<
                             }
                             return this._props.addDocument?.(created) || false;
                         }
+                        return false;
                     },
                     icon: 'compress-arrows-alt',
                 })
@@ -307,7 +316,7 @@ export class CollectionStackingViewFieldColumn extends ObservableReactComponent<
         const columnYMargin = this._props.headingObject ? 0 : this._props.yMargin;
         const uniqueHeadings = headings.map((i, idx) => headings.indexOf(i) === idx);
         const noValueHeader = `NO ${key.toUpperCase()} VALUE`;
-        const evContents = heading ? heading : this._props?.type === 'number' ? '0' : noValueHeader;
+        const evContents = heading || (this._props?.type === 'number' ? '0' : noValueHeader);
         const headingView = this._props.headingObject ? (
             <div
                 key={heading}
@@ -324,14 +333,19 @@ export class CollectionStackingViewFieldColumn extends ObservableReactComponent<
                     onPointerDown={this.headerDown}
                     title={evContents === noValueHeader ? `Documents that don't have a ${key} value will go here. This column cannot be removed.` : ''}
                     style={{ background: evContents !== noValueHeader ? this._color : 'inherit' }}>
-                    <EditableView GetValue={() => evContents} SetValue={this.headingChanged} contents={evContents} oneLine={true} />
+                    <EditableView GetValue={() => evContents} SetValue={this.headingChanged} contents={evContents} oneLine />
                     <div className="collectionStackingView-sectionColor" style={{ display: evContents === noValueHeader ? 'none' : undefined }}>
-                        <button className="collectionStackingView-sectionColorButton" onClick={action(e => (this._paletteOn = !this._paletteOn))}>
+                        <button
+                            type="button"
+                            className="collectionStackingView-sectionColorButton"
+                            onClick={action(() => {
+                                this._paletteOn = !this._paletteOn;
+                            })}>
                             <FontAwesomeIcon icon="palette" size="lg" />
                         </button>
                         {this._paletteOn ? this.renderColorPicker() : null}
                     </div>
-                    <button className="collectionStackingView-sectionDelete" onClick={this.deleteColumn}>
+                    <button type="button" className="collectionStackingView-sectionDelete" onClick={this.deleteColumn}>
                         <FontAwesomeIcon icon="trash" size="lg" />
                     </button>
                     {/* {evContents === noValueHeader ? null : (
@@ -352,7 +366,7 @@ export class CollectionStackingViewFieldColumn extends ObservableReactComponent<
             </div>
         ) : null;
         const templatecols = `${this._props.columnWidth / this._props.numGroupColumns}px `;
-        const type = this._props.Document.type;
+        const { type } = this._props.Document;
         return (
             <>
                 {this._props.Document._columnsHideIfEmpty ? null : headingView}
@@ -364,7 +378,7 @@ export class CollectionStackingViewFieldColumn extends ObservableReactComponent<
                             style={{
                                 padding: `${columnYMargin}px ${0}px ${this._props.yMargin}px ${0}px`,
                                 margin: 'auto',
-                                width: 'max-content', //singleColumn ? undefined : `${cols * (style.columnWidth + style.gridGap) + 2 * style.xMargin - style.gridGap}px`,
+                                width: 'max-content', // singleColumn ? undefined : `${cols * (style.columnWidth + style.gridGap) + 2 * style.xMargin - style.gridGap}px`,
                                 height: 'max-content',
                                 position: 'relative',
                                 gridGap: this._props.gridGap,
@@ -376,10 +390,10 @@ export class CollectionStackingViewFieldColumn extends ObservableReactComponent<
                         {!this._props.chromeHidden && type !== DocumentType.PRES ? (
                             // TODO: this is the "new" button: see what you can work with here
                             // change cursor to pointer for this, and update dragging cursor
-                            //TODO: there is a bug that occurs when adding a freeform document and trying to move it around
-                            //TODO: would be great if there was additional space beyond the frame, so that you can actually see your
+                            //  TODO: there is a bug that occurs when adding a freeform document and trying to move it around
+                            //  TODO: would be great if there was additional space beyond the frame, so that you can actually see your
                             // bottom note
-                            //TODO: ok, so we are using a single column, and this is it!
+                            // TODO: ok, so we are using a single column, and this is it!
                             <div
                                 key={`${heading}-add-document`}
                                 onKeyDown={e => e.stopPropagation()}
@@ -390,7 +404,7 @@ export class CollectionStackingViewFieldColumn extends ObservableReactComponent<
                                     SetValue={this.addNewTextDoc}
                                     textCallback={this.typedNote}
                                     placeholder={"Type ':' for commands"}
-                                    contents={<FontAwesomeIcon icon={'plus'} />}
+                                    contents={<FontAwesomeIcon icon="plus" />}
                                     menuCallback={this.menuCallback}
                                 />
                             </div>
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index 32198e3a2..cd401058f 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -1,10 +1,10 @@
 import { action, computed, makeObservable, observable } from 'mobx';
 import * as React from 'react';
 import * as rp from 'request-promise';
-import { Utils, returnFalse } from '../../../Utils';
+import { ClientUtils, returnFalse } from '../../../ClientUtils';
 import CursorField from '../../../fields/CursorField';
-import { Doc, DocListCast, Field, Opt, StrListCast } from '../../../fields/Doc';
-import { AclPrivate, DocData } from '../../../fields/DocSymbols';
+import { Doc, DocListCast, Field, FieldType, Opt, StrListCast } from '../../../fields/Doc';
+import { AclPrivate } from '../../../fields/DocSymbols';
 import { Id } from '../../../fields/FieldSymbols';
 import { List } from '../../../fields/List';
 import { listSpec } from '../../../fields/Schema';
@@ -15,16 +15,47 @@ import { GestureUtils } from '../../../pen-gestures/GestureUtils';
 import { DocServer } from '../../DocServer';
 import { Networking } from '../../Network';
 import { CollectionViewType, DocumentType } from '../../documents/DocumentTypes';
-import { ViewBoxBaseComponent } from '../DocComponent';
 import { DocUtils, Docs, DocumentOptions } from '../../documents/Documents';
-import { DragManager, dropActionType } from '../../util/DragManager';
+import { DragManager } from '../../util/DragManager';
+import { dropActionType } from '../../util/DropActionTypes';
 import { ImageUtils } from '../../util/Import & Export/ImageUtils';
 import { SelectionManager } from '../../util/SelectionManager';
 import { SnappingManager } from '../../util/SnappingManager';
 import { UndoManager, undoBatch } from '../../util/UndoManager';
+import { ViewBoxBaseComponent } from '../DocComponent';
+import { FieldViewProps } from '../nodes/FieldView';
 import { LoadingBox } from '../nodes/LoadingBox';
 import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox';
-import { CollectionView, CollectionViewProps } from './CollectionView';
+
+export interface CollectionViewProps extends React.PropsWithChildren<FieldViewProps> {
+    isAnnotationOverlay?: boolean; // is the collection an annotation overlay (eg an overlay on an image/video/etc)
+    isAnnotationOverlayScrollable?: boolean; // whether the annotation overlay can be vertically scrolled (just for tree views, currently)
+    layoutEngine?: () => string;
+    setPreviewCursor?: (func: (x: number, y: number, drag: boolean, hide: boolean, doc: Opt<Doc>) => void) => void;
+    ignoreUnrendered?: boolean;
+
+    // property overrides for child documents
+    childDocuments?: Doc[]; // used to override the documents shown by the sub collection to an explicit list (see LinkBox)
+    childDocumentsActive?: () => boolean | undefined; // whether child documents can be dragged if collection can be dragged (eg., in a when a Pile document is in startburst mode)
+    childContentsActive?: () => boolean | undefined;
+    childLayoutFitWidth?: (child: Doc) => boolean;
+    childlayout_showTitle?: () => string;
+    childOpacity?: () => number;
+    childContextMenuItems?: () => { script: ScriptField; label: string }[];
+    childLayoutTemplate?: () => Doc | undefined; // specify a layout Doc template to use for children of the collection
+    childHideDecorationTitle?: boolean;
+    childHideResizeHandles?: boolean;
+    childDragAction?: dropActionType;
+    childXPadding?: number;
+    childYPadding?: number;
+    childLayoutString?: string;
+    childIgnoreNativeSize?: boolean;
+    childClickScript?: ScriptField;
+    childDoubleClickScript?: ScriptField;
+    AddToMap?: (treeViewDoc: Doc, index: number[]) => void;
+    RemFromMap?: (treeViewDoc: Doc, index: number[]) => void;
+    hierarchyIndex?: number[]; // hierarchical index of a document up to the rendering root (primarily used for tree views)
+}
 
 export interface SubCollectionViewProps extends CollectionViewProps {
     isAnyChildContentActive: () => boolean;
@@ -53,7 +84,7 @@ export function CollectionSubView<X>(moreProps?: X) {
             }
         };
         protected CreateDropTarget(ele: HTMLDivElement) {
-            //used in schema view
+            // used in schema view
             this.createDashEventsTarget(ele);
         }
 
@@ -83,10 +114,11 @@ export function CollectionSubView<X>(moreProps?: X) {
             const { Document, TemplateDataDocument } = this._props;
             const validPairs = this.childDocs
                 .map(doc => Doc.GetLayoutDataDocPair(Document, !this._props.isAnnotationOverlay ? TemplateDataDocument : undefined, doc))
-                .filter(pair => {
-                    // filter out any documents that have a proto that we don't have permissions to
-                    return !pair.layout?.hidden && pair.layout && (!pair.layout.proto || (pair.layout.proto instanceof Doc && GetEffectiveAcl(pair.layout.proto) !== AclPrivate));
-                });
+                .filter(
+                    pair =>
+                        // filter out any documents that have a proto that we don't have permissions to
+                        !pair.layout?.hidden && pair.layout && (!pair.layout.proto || (pair.layout.proto instanceof Doc && GetEffectiveAcl(pair.layout.proto) !== AclPrivate))
+                );
             return validPairs.map(({ data, layout }) => ({ data: data as Doc, layout: layout! })); // this mapping is a bit of a hack to coerce types
         }
         @computed get childDocList() {
@@ -95,9 +127,9 @@ export function CollectionSubView<X>(moreProps?: X) {
         collectionFilters = () => this._focusFilters ?? StrListCast(this.Document._childFilters);
         collectionRangeDocFilters = () => this._focusRangeFilters ?? Cast(this.Document._childFiltersByRanges, listSpec('string'), []);
         // child filters apply to the descendants of the documents in this collection
-        childDocFilters = () => [...(this._props.childFilters?.().filter(f => Utils.IsRecursiveFilter(f)) || []), ...this.collectionFilters()];
+        childDocFilters = () => [...(this._props.childFilters?.().filter(f => ClientUtils.IsRecursiveFilter(f)) || []), ...this.collectionFilters()];
         // unrecursive filters apply to the documents in the collection, but no their children.  See Utils.noRecursionHack
-        unrecursiveDocFilters = () => [...(this._props.childFilters?.().filter(f => !Utils.IsRecursiveFilter(f)) || [])];
+        unrecursiveDocFilters = () => [...(this._props.childFilters?.().filter(f => !ClientUtils.IsRecursiveFilter(f)) || [])];
         childDocRangeFilters = () => [...(this._props.childFiltersByRanges?.() || []), ...this.collectionRangeDocFilters()];
         searchFilterDocs = () => this._props.searchFilterDocs?.() ?? DocListCast(this.Document._searchFilterDocs);
         @computed.struct get childDocs() {
@@ -129,13 +161,13 @@ export function CollectionSubView<X>(moreProps?: X) {
             const docsforFilter: Doc[] = [];
             childDocs.forEach(d => {
                 // dragging facets
-                const dragged = this._props.childFilters?.().some(f => f.includes(Utils.noDragDocsFilter));
-                if (dragged && SnappingManager.CanEmbed && DragManager.docsBeingDragged.includes(d)) return false;
+                const dragged = this._props.childFilters?.().some(f => f.includes(ClientUtils.noDragDocsFilter));
+                if (dragged && SnappingManager.CanEmbed && DragManager.docsBeingDragged.includes(d)) return;
                 let notFiltered = d.z || Doc.IsSystem(d) || DocUtils.FilterDocs([d], this.unrecursiveDocFilters(), childFiltersByRanges, this.Document).length > 0;
                 if (notFiltered) {
                     notFiltered = (!searchDocs.length || searchDocs.includes(d)) && DocUtils.FilterDocs([d], childDocFilters, childFiltersByRanges, this.Document).length > 0;
                     const fieldKey = Doc.LayoutFieldKey(d);
-                    const annos = !Field.toString(Doc.LayoutField(d) as Field).includes(CollectionView.name);
+                    const annos = !Field.toString(Doc.LayoutField(d) as FieldType).includes(CollectionView.name);
                     const data = d[annos ? fieldKey + '_annotations' : fieldKey];
                     const side = annos && d[fieldKey + '_sidebar'];
                     if (data !== undefined || side !== undefined) {
@@ -147,7 +179,7 @@ export function CollectionSubView<X>(moreProps?: X) {
                                 newarray = [];
                                 subDocs.forEach(t => {
                                     const fieldKey = Doc.LayoutFieldKey(t);
-                                    const annos = !Field.toString(Doc.LayoutField(t) as Field).includes(CollectionView.name);
+                                    const annos = !Field.toString(Doc.LayoutField(t) as FieldType).includes(CollectionView.name);
                                     notFiltered =
                                         notFiltered || ((!searchDocs.length || searchDocs.includes(t)) && ((!childDocFilters.length && !childFiltersByRanges.length) || DocUtils.FilterDocs([t], childDocFilters, childFiltersByRanges, d).length));
                                     DocListCast(t[annos ? fieldKey + '_annotations' : fieldKey]).forEach(newdoc => newarray.push(newdoc));
@@ -168,7 +200,7 @@ export function CollectionSubView<X>(moreProps?: X) {
             let ind;
             const doc = this.Document;
             const id = Doc.UserDoc()[Id];
-            const email = Doc.CurrentUserEmail;
+            const email = ClientUtils.CurrentUserEmail;
             const pos = { x: position[0], y: position[1] };
             if (id && email) {
                 const proto = Doc.GetProto(doc);
@@ -184,6 +216,7 @@ export function CollectionSubView<X>(moreProps?: X) {
                 if (!cursors) {
                     proto.cursors = cursors = new List<CursorField>();
                 }
+                // eslint-disable-next-line no-cond-assign
                 if (cursors.length > 0 && (ind = cursors.findIndex(entry => entry.data.metadata.id === id)) > -1) {
                     cursors[ind].setPosition(pos);
                 } else {
@@ -215,9 +248,9 @@ export function CollectionSubView<X>(moreProps?: X) {
         moveDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (doc: Doc | Doc[], annotationKey?: string) => boolean, annotationKey?: string) => this._props.moveDocument?.(doc, targetCollection, addDocument) || false;
 
         protected onInternalDrop(e: Event, de: DragManager.DropEvent): boolean {
-            const docDragData = de.complete.docDragData;
+            const { docDragData } = de.complete;
             if (docDragData) {
-                let added = undefined;
+                let added;
                 const dropAction = docDragData.dropAction || docDragData.userDropAction;
                 const targetDocments = DocListCast(this.dataDoc[this._props.fieldKey]);
                 const someMoved = !dropAction && docDragData.draggedDocuments.some(drag => targetDocments.includes(drag));
@@ -245,8 +278,9 @@ export function CollectionSubView<X>(moreProps?: X) {
                 }
                 added === false && !this._props.isAnnotationOverlay && e.preventDefault();
                 added === true && e.stopPropagation();
-                return added ? true : false;
-            } else if (de.complete.annoDragData) {
+                return !!added;
+            }
+            if (de.complete.annoDragData) {
                 const dropCreator = de.complete.annoDragData.dropDocCreator;
                 de.complete.annoDragData.dropDocCreator = () => {
                     const dropped = dropCreator(this._props.isAnnotationOverlay ? this.Document : undefined);
@@ -316,7 +350,7 @@ export function CollectionSubView<X>(moreProps?: X) {
                             const result = (await Networking.PostToServer('/uploadRemoteImage', { sources: [imgSrc] })).lastElement();
                             const newImgSrc =
                                 result.accessPaths.agnostic.client.indexOf('dashblobstore') === -1 //
-                                    ? Utils.prepend(result.accessPaths.agnostic.client)
+                                    ? ClientUtils.prepend(result.accessPaths.agnostic.client)
                                     : result.accessPaths.agnostic.client;
 
                             addDocument(ImageUtils.AssignImgInfo(Docs.Create.ImageDocument(newImgSrc, imgOpts), result));
@@ -325,39 +359,39 @@ export function CollectionSubView<X>(moreProps?: X) {
                             addDocument(ImageUtils.AssignImgInfo(doc, await ImageUtils.ExtractImgInfo(doc)));
                         }
                         return;
+                    }
+                    const path = window.location.origin + '/doc/';
+                    if (text.startsWith(path)) {
+                        const docId = text.replace(Doc.globalServerPath(), '').split('?')[0];
+                        DocServer.GetRefField(docId).then(f => {
+                            const fDoc = f;
+                            if (fDoc instanceof Doc) {
+                                if (options.x || options.y) {
+                                    fDoc.x = options.x as number;
+                                    fDoc.y = options.y as number;
+                                } // should be in CollectionFreeFormView
+                                addDocument(fDoc);
+                            }
+                        });
                     } else {
-                        const path = window.location.origin + '/doc/';
-                        if (text.startsWith(path)) {
-                            const docId = text.replace(Doc.globalServerPath(), '').split('?')[0];
-                            DocServer.GetRefField(docId).then(f => {
-                                if (f instanceof Doc) {
-                                    if (options.x || options.y) {
-                                        f.x = options.x as number;
-                                        f.y = options.y as number;
-                                    } // should be in CollectionFreeFormView
-                                    f instanceof Doc && addDocument(f);
-                                }
-                            });
-                        } else {
-                            const srcWeb = SelectionManager.Views.lastElement();
-                            const srcUrl = (srcWeb?.Document.data as WebField)?.url?.href?.match(/https?:\/\/[^/]*/)?.[0];
-                            const reg = new RegExp(Utils.prepend(''), 'g');
-                            const modHtml = srcUrl ? html.replace(reg, srcUrl) : html;
-                            const backgroundColor = tags.map(tag => tag.match(/.*(background-color: ?[^;]*)/)?.[1]?.replace(/background-color: ?(.*)/, '$1')).filter(t => t)?.[0];
-                            const htmlDoc = Docs.Create.HtmlDocument(modHtml, { ...options, title: srcUrl ? 'from:' + srcUrl : '-web clip-', _width: 300, _height: 300, backgroundColor });
-                            Doc.GetProto(htmlDoc)['data-text'] = Doc.GetProto(htmlDoc).text = text;
-                            addDocument(htmlDoc);
-                            if (srcWeb) {
-                                const iframe = SelectionManager.Views[0].ContentDiv?.getElementsByTagName('iframe')?.[0];
-                                const focusNode = iframe?.contentDocument?.getSelection()?.focusNode as any;
-                                if (focusNode) {
-                                    const anchor = srcWeb?.ComponentView?.getAnchor?.(true);
-                                    anchor && DocUtils.MakeLink(htmlDoc, anchor, {});
-                                }
+                        const srcWeb = SelectionManager.Views.lastElement();
+                        const srcUrl = (srcWeb?.Document.data as WebField)?.url?.href?.match(/https?:\/\/[^/]*/)?.[0];
+                        const reg = new RegExp(ClientUtils.prepend(''), 'g');
+                        const modHtml = srcUrl ? html.replace(reg, srcUrl) : html;
+                        const backgroundColor = tags.map(tag => tag.match(/.*(background-color: ?[^;]*)/)?.[1]?.replace(/background-color: ?(.*)/, '$1')).filter(t => t)?.[0];
+                        const htmlDoc = Docs.Create.HtmlDocument(modHtml, { ...options, title: srcUrl ? 'from:' + srcUrl : '-web clip-', _width: 300, _height: 300, backgroundColor });
+                        Doc.GetProto(htmlDoc)['data-text'] = Doc.GetProto(htmlDoc).text = text;
+                        addDocument(htmlDoc);
+                        if (srcWeb) {
+                            const iframe = SelectionManager.Views[0].ContentDiv?.getElementsByTagName('iframe')?.[0];
+                            const focusNode = iframe?.contentDocument?.getSelection()?.focusNode as any;
+                            if (focusNode) {
+                                const anchor = srcWeb?.ComponentView?.getAnchor?.(true);
+                                anchor && DocUtils.MakeLink(htmlDoc, anchor, {});
                             }
                         }
-                        return;
                     }
+                    return;
                 }
             }
 
@@ -414,10 +448,15 @@ export function CollectionSubView<X>(moreProps?: X) {
             for (let i = 0; i < length; i++) {
                 const item = e.dataTransfer.items[i];
                 if (item.kind === 'string' && item.type.includes('uri')) {
-                    const stringContents = await new Promise<string>(resolve => item.getAsString(resolve));
-                    const type = (await rp.head(Utils.CorsProxy(stringContents)))['content-type'];
+                    // eslint-disable-next-line no-await-in-loop
+                    const stringContents = await new Promise<string>(resolve => {
+                        item.getAsString(resolve);
+                    });
+                    // eslint-disable-next-line no-await-in-loop
+                    const type = (await rp.head(ClientUtils.CorsProxy(stringContents)))['content-type'];
                     if (type) {
-                        const doc = await DocUtils.DocumentFromType(type, Utils.CorsProxy(stringContents), options);
+                        // eslint-disable-next-line no-await-in-loop
+                        const doc = await DocUtils.DocumentFromType(type, ClientUtils.CorsProxy(stringContents), options);
                         doc && generatedDocuments.push(doc);
                     }
                 }
@@ -426,7 +465,7 @@ export function CollectionSubView<X>(moreProps?: X) {
                     file?.type && files.push(file);
 
                     file?.type === 'application/json' &&
-                        Utils.readUploadedFileAsText(file).then(result => {
+                        ClientUtils.readUploadedFileAsText(file).then(result => {
                             const json = JSON.parse(result as string);
                             addDocument(
                                 Docs.Create.TreeDocument(
@@ -450,7 +489,7 @@ export function CollectionSubView<X>(moreProps?: X) {
             // create placeholder docs
             // inside placeholder docs have some func that
 
-            let pileUpDoc = undefined;
+            let pileUpDoc;
             if (typeof files === 'string') {
                 const loading = Docs.Create.LoadingDocument(files, options);
                 generatedDocuments.push(loading);
@@ -478,20 +517,16 @@ export function CollectionSubView<X>(moreProps?: X) {
                         })
                       : [];
                 if (completed) completed(set);
-                else {
-                    if (isFreeformView && generatedDocuments.length > 1) {
-                        pileUpDoc = DocUtils.pileup(generatedDocuments, options.x as number, options.y as number)!;
-                        addDocument(pileUpDoc);
-                    } else {
-                        generatedDocuments.forEach(addDocument);
-                    }
-                }
-            } else {
-                if (text && !text.includes('https://')) {
-                    addDocument(Docs.Create.TextDocument(text, { ...options, title: text.substring(0, 20), _width: 400, _height: 315 }));
+                else if (isFreeformView && generatedDocuments.length > 1) {
+                    pileUpDoc = DocUtils.pileup(generatedDocuments, options.x as number, options.y as number)!;
+                    addDocument(pileUpDoc);
                 } else {
-                    alert('Document upload failed - possibly an unsupported file type.');
+                    generatedDocuments.forEach(addDocument);
                 }
+            } else if (text && !text.includes('https://')) {
+                addDocument(Docs.Create.TextDocument(text, { ...options, title: text.substring(0, 20), _width: 400, _height: 315 }));
+            } else {
+                alert('Document upload failed - possibly an unsupported file type.');
             }
         };
     }
diff --git a/src/client/views/collections/CollectionTimeView.tsx b/src/client/views/collections/CollectionTimeView.tsx
index 37452ddfb..7f234ffe9 100644
--- a/src/client/views/collections/CollectionTimeView.tsx
+++ b/src/client/views/collections/CollectionTimeView.tsx
@@ -1,7 +1,8 @@
 import { action, computed, makeObservable, observable, runInAction } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
-import { emptyFunction, returnFalse, returnTrue, setupMoveUpEvents } from '../../../Utils';
+import { emptyFunction } from '../../../Utils';
+import { returnFalse, returnTrue, setupMoveUpEvents } from '../../../ClientUtils';
 import { Doc, Opt, StrListCast } from '../../../fields/Doc';
 import { List } from '../../../fields/List';
 import { ObjectField } from '../../../fields/ObjectField';
@@ -195,7 +196,7 @@ export class CollectionTimeView extends CollectionSubView() {
         let nonNumbers = 0;
         this.childDocs.map(doc => {
             const num = NumCast(doc[this.pivotField], Number(StrCast(doc[this.pivotField])));
-            if (Number.isNaN(num)) {
+            if (isNaN(num)) {
                 nonNumbers++;
             }
         });
diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx
index 5741fc29b..32473f20b 100644
--- a/src/client/views/collections/CollectionTreeView.tsx
+++ b/src/client/views/collections/CollectionTreeView.tsx
@@ -1,6 +1,7 @@
 import { action, computed, IReactionDisposer, makeObservable, observable, reaction } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
+import { DivHeight, returnAll, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnNone, returnOne, returnTrue, returnZero } from '../../../ClientUtils';
 import { Doc, DocListCast, Opt, StrListCast } from '../../../fields/Doc';
 import { DocData } from '../../../fields/DocSymbols';
 import { Id } from '../../../fields/FieldSymbols';
@@ -8,10 +9,11 @@ import { listSpec } from '../../../fields/Schema';
 import { ScriptField } from '../../../fields/ScriptField';
 import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
 import { TraceMobx } from '../../../fields/util';
-import { DivHeight, emptyFunction, returnAll, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnNone, returnOne, returnTrue, returnZero, Utils } from '../../../Utils';
+import { emptyFunction, Utils } from '../../../Utils';
 import { Docs, DocUtils } from '../../documents/Documents';
 import { DocumentManager } from '../../util/DocumentManager';
-import { DragManager, dropActionType } from '../../util/DragManager';
+import { DragManager } from '../../util/DragManager';
+import { dropActionType } from '../../util/DropActionTypes';
 import { ScriptingGlobals } from '../../util/ScriptingGlobals';
 import { SelectionManager } from '../../util/SelectionManager';
 import { SnappingManager } from '../../util/SnappingManager';
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index 18eb4dd1f..a0d84ab28 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -1,21 +1,20 @@
+/* eslint-disable react/jsx-props-no-spreading */
 import { IReactionDisposer, makeObservable, observable, reaction } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
-import { returnEmptyString } from '../../../Utils';
-import { Doc, DocListCast, Opt } from '../../../fields/Doc';
+import { returnEmptyString } from '../../../ClientUtils';
+import { Doc, DocListCast } from '../../../fields/Doc';
 import { ObjectField } from '../../../fields/ObjectField';
-import { ScriptField } from '../../../fields/ScriptField';
 import { BoolCast, Cast, ScriptCast, StrCast } from '../../../fields/Types';
 import { TraceMobx } from '../../../fields/util';
 import { CollectionViewType } from '../../documents/DocumentTypes';
 import { DocUtils } from '../../documents/Documents';
-import { dropActionType } from '../../util/DragManager';
 import { ImageUtils } from '../../util/Import & Export/ImageUtils';
 import { ContextMenu } from '../ContextMenu';
 import { ContextMenuProps } from '../ContextMenuItem';
 import { ViewBoxAnnotatableComponent, ViewBoxInterface } from '../DocComponent';
 import { OpenWhere } from '../nodes/DocumentView';
-import { FieldView, FieldViewProps } from '../nodes/FieldView';
+import { FieldView } from '../nodes/FieldView';
 import { CollectionCalendarView } from './CollectionCalendarView';
 import { CollectionCarousel3DView } from './CollectionCarousel3DView';
 import { CollectionCarouselView } from './CollectionCarouselView';
@@ -23,7 +22,7 @@ import { CollectionDockingView } from './CollectionDockingView';
 import { CollectionNoteTakingView } from './CollectionNoteTakingView';
 import { CollectionPileView } from './CollectionPileView';
 import { CollectionStackingView } from './CollectionStackingView';
-import { SubCollectionViewProps } from './CollectionSubView';
+import { CollectionViewProps, SubCollectionViewProps } from './CollectionSubView';
 import { CollectionTimeView } from './CollectionTimeView';
 import { CollectionTreeView } from './CollectionTreeView';
 import './CollectionView.scss';
@@ -33,36 +32,7 @@ import { CollectionLinearView } from './collectionLinear';
 import { CollectionMulticolumnView } from './collectionMulticolumn/CollectionMulticolumnView';
 import { CollectionMultirowView } from './collectionMulticolumn/CollectionMultirowView';
 import { CollectionSchemaView } from './collectionSchema/CollectionSchemaView';
-export interface CollectionViewProps extends React.PropsWithChildren<FieldViewProps> {
-    isAnnotationOverlay?: boolean; // is the collection an annotation overlay (eg an overlay on an image/video/etc)
-    isAnnotationOverlayScrollable?: boolean; // whether the annotation overlay can be vertically scrolled (just for tree views, currently)
-    layoutEngine?: () => string;
-    setPreviewCursor?: (func: (x: number, y: number, drag: boolean, hide: boolean, doc: Opt<Doc>) => void) => void;
-    ignoreUnrendered?: boolean;
 
-    // property overrides for child documents
-    childDocuments?: Doc[]; // used to override the documents shown by the sub collection to an explicit list (see LinkBox)
-    childDocumentsActive?: () => boolean | undefined; // whether child documents can be dragged if collection can be dragged (eg., in a when a Pile document is in startburst mode)
-    childContentsActive?: () => boolean | undefined;
-    childLayoutFitWidth?: (child: Doc) => boolean;
-    childlayout_showTitle?: () => string;
-    childOpacity?: () => number;
-    childContextMenuItems?: () => { script: ScriptField; label: string }[];
-    childLayoutTemplate?: () => Doc | undefined; // specify a layout Doc template to use for children of the collection
-    childHideDecorationTitle?: boolean;
-    childHideResizeHandles?: boolean;
-    childDragAction?: dropActionType;
-    childXPadding?: number;
-    childYPadding?: number;
-    childLayoutString?: string;
-    childIgnoreNativeSize?: boolean;
-    childClickScript?: ScriptField;
-    childDoubleClickScript?: ScriptField;
-    //TODO: [AL] add these fields
-    AddToMap?: (treeViewDoc: Doc, index: number[]) => void;
-    RemFromMap?: (treeViewDoc: Doc, index: number[]) => void;
-    hierarchyIndex?: number[]; // hierarchical index of a document up to the rendering root (primarily used for tree views)
-}
 @observer
 export class CollectionView extends ViewBoxAnnotatableComponent<CollectionViewProps>() implements ViewBoxInterface {
     public static LayoutString(fieldStr: string) {
@@ -89,7 +59,9 @@ export class CollectionView extends ViewBoxAnnotatableComponent<CollectionViewPr
         // this a reaction, downstream invalidations only occur when the reaction value actually changes.
         this.reactionDisposer = reaction(
             () => (this.isAnyChildContentActive() ? true : this._props.isContentActive()),
-            active => (this._isContentActive = active),
+            active => {
+                this._isContentActive = active;
+            },
             { fireImmediately: true }
         );
     }
@@ -100,13 +72,12 @@ export class CollectionView extends ViewBoxAnnotatableComponent<CollectionViewPr
     get collectionViewType(): CollectionViewType | undefined {
         const viewField = StrCast(this.layoutDoc._type_collection);
         if (CollectionView._safeMode) {
-            switch (viewField) {
+            switch (viewField) { 
                 case CollectionViewType.Freeform:
-                case CollectionViewType.Schema:
-                    return CollectionViewType.Tree;
-                case CollectionViewType.Invalid:
-                    return CollectionViewType.Freeform;
-            }
+                case CollectionViewType.Schema:  return CollectionViewType.Tree;
+                case CollectionViewType.Invalid:  return CollectionViewType.Freeform;
+                default:
+            } // prettier-ignore
         }
         return viewField as any as CollectionViewType;
     }
@@ -117,8 +88,6 @@ export class CollectionView extends ViewBoxAnnotatableComponent<CollectionViewPr
         TraceMobx();
         if (type === undefined) return null;
         switch (type) {
-            default:
-            case CollectionViewType.Freeform:     return <CollectionFreeFormView    key="collview" {...props} />;
             case CollectionViewType.Schema:       return <CollectionSchemaView      key="collview" {...props} />;
             case CollectionViewType.Calendar:     return <CollectionCalendarView    key="collview" {...props} />;
             case CollectionViewType.Docking:      return <CollectionDockingView     key="collview" {...props} />;
@@ -134,6 +103,8 @@ export class CollectionView extends ViewBoxAnnotatableComponent<CollectionViewPr
             case CollectionViewType.Masonry:      return <CollectionStackingView    key="collview" {...props} />;
             case CollectionViewType.Time:         return <CollectionTimeView        key="collview" {...props} />;
             case CollectionViewType.Grid:         return <CollectionGridView        key="collview" {...props} />;
+            case CollectionViewType.Freeform:     
+            default:                              return <CollectionFreeFormView    key="collview" {...props} />;
         }
     };
 
@@ -144,9 +115,9 @@ export class CollectionView extends ViewBoxAnnotatableComponent<CollectionViewPr
                 { description: 'Freeform',    event: () => func(CollectionViewType.Freeform), icon: 'signature' },
                 { description: 'Schema',      event: () => func(CollectionViewType.Schema), icon: 'th-list' },
                 { description: 'Tree',        event: () => func(CollectionViewType.Tree), icon: 'tree' },
-                { description: 'Stacking',    event: () => (func(CollectionViewType.Stacking)._layout_autoHeight = true), icon: 'ellipsis-v' },
+                { description: 'Stacking',    event: () => {func(CollectionViewType.Stacking)._layout_autoHeight = true}, icon: 'ellipsis-v' },
                 { description: 'Calendar',    event: () => func(CollectionViewType.Calendar), icon: 'columns'},
-                { description: 'Notetaking',  event: () => (func(CollectionViewType.NoteTaking)._layout_autoHeight = true), icon: 'ellipsis-v' },
+                { description: 'Notetaking',  event: () => {func(CollectionViewType.NoteTaking)._layout_autoHeight = true}, icon: 'ellipsis-v' },
                 { description: 'Multicolumn', event: () => func(CollectionViewType.Multicolumn), icon: 'columns' },
                 { description: 'Multirow',    event: () => func(CollectionViewType.Multirow), icon: 'columns' },
                 { description: 'Masonry',     event: () => func(CollectionViewType.Masonry), icon: 'columns' },
@@ -178,14 +149,14 @@ export class CollectionView extends ViewBoxAnnotatableComponent<CollectionViewPr
 
             const options = cm.findByDescription('Options...');
             const optionItems = options && 'subitems' in options ? options.subitems : [];
-            !Doc.noviceMode ? optionItems.splice(0, 0, { description: `${this.Document.forceActive ? 'Select' : 'Force'} Contents Active`, event: () => (this.Document.forceActive = !this.Document.forceActive), icon: 'project-diagram' }) : null;
+            !Doc.noviceMode ? optionItems.splice(0, 0, { description: `${this.Document.forceActive ? 'Select' : 'Force'} Contents Active`, event: () => {this.Document.forceActive = !this.Document.forceActive}, icon: 'project-diagram' }) : null; // prettier-ignore
             if (this.Document.childLayout instanceof Doc) {
                 optionItems.push({ description: 'View Child Layout', event: () => this._props.addDocTab(this.Document.childLayout as Doc, OpenWhere.addRight), icon: 'project-diagram' });
             }
             if (this.Document.childClickedOpenTemplateView instanceof Doc) {
                 optionItems.push({ description: 'View Child Detailed Layout', event: () => this._props.addDocTab(this.Document.childClickedOpenTemplateView as Doc, OpenWhere.addRight), icon: 'project-diagram' });
             }
-            !Doc.noviceMode && optionItems.push({ description: `${this.layoutDoc._isLightbox ? 'Unset' : 'Set'} is Lightbox`, event: () => (this.layoutDoc._isLightbox = !this.layoutDoc._isLightbox), icon: 'project-diagram' });
+            !Doc.noviceMode && optionItems.push({ description: `${this.layoutDoc._isLightbox ? 'Unset' : 'Set'} is Lightbox`, event: () => { this.layoutDoc._isLightbox = !this.layoutDoc._isLightbox; }, icon: 'project-diagram' }); // prettier-ignore
 
             !options && cm.addItem({ description: 'Options...', subitems: optionItems, icon: 'hand-point-right' });
 
@@ -200,7 +171,7 @@ export class CollectionView extends ViewBoxAnnotatableComponent<CollectionViewPr
                     onClicks.push({
                         description: `Edit ${func.name} script`,
                         icon: 'edit',
-                        event: (obj: any) => {
+                        event: () => {
                             const embedding = Doc.MakeEmbedding(this.Document);
                             DocUtils.makeCustomViewClicked(embedding, undefined, func.key);
                             this._props.addDocTab(embedding, OpenWhere.addRight);
@@ -211,7 +182,9 @@ export class CollectionView extends ViewBoxAnnotatableComponent<CollectionViewPr
                     onClicks.push({
                         description: `Set child ${childClick.title}`,
                         icon: 'edit',
-                        event: () => (this.dataDoc[StrCast(childClick.targetScriptKey)] = ObjectField.MakeCopy(ScriptCast(childClick.data))),
+                        event: () => {
+                            this.dataDoc[StrCast(childClick.targetScriptKey)] = ObjectField.MakeCopy(ScriptCast(childClick.data));
+                        },
                     })
                 );
                 !Doc.IsSystem(this.Document) && !existingOnClick && cm.addItem({ description: 'OnClick...', noexpand: true, subitems: onClicks, icon: 'mouse-pointer' });
@@ -229,7 +202,7 @@ export class CollectionView extends ViewBoxAnnotatableComponent<CollectionViewPr
     bodyPanelWidth = () => this._props.PanelWidth();
 
     childLayoutTemplate = () => this._props.childLayoutTemplate?.() || Cast(this.Document.childLayoutTemplate, Doc, null);
-    isContentActive = (outsideReaction?: boolean) => this._isContentActive;
+    isContentActive = () => this._isContentActive;
 
     pointerEvents = () =>
         this.layoutDoc._lockedPosition && //
diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx
index 5cb7b149b..8a2e83ed9 100644
--- a/src/client/views/collections/TabDocView.tsx
+++ b/src/client/views/collections/TabDocView.tsx
@@ -6,7 +6,8 @@ import { IReactionDisposer, ObservableSet, action, computed, makeObservable, obs
 import { observer } from 'mobx-react';
 import * as React from 'react';
 import * as ReactDOM from 'react-dom/client';
-import { DashColor, Utils, emptyFunction, lightOrDark, returnEmptyDoclist, returnFalse, returnTrue, setupMoveUpEvents, simulateMouseClick } from '../../../Utils';
+import { ClientUtils, DashColor, lightOrDark, returnEmptyDoclist, returnFalse, returnTrue, setupMoveUpEvents, simulateMouseClick } from '../../../ClientUtils';
+import { emptyFunction } from '../../../Utils';
 import { Doc, Opt } from '../../../fields/Doc';
 import { DocData } from '../../../fields/DocSymbols';
 import { Id } from '../../../fields/FieldSymbols';
@@ -18,13 +19,14 @@ import { DocServer } from '../../DocServer';
 import { CollectionViewType, DocumentType } from '../../documents/DocumentTypes';
 import { Docs } from '../../documents/Documents';
 import { DocumentManager } from '../../util/DocumentManager';
-import { DragManager, dropActionType } from '../../util/DragManager';
+import { DragManager } from '../../util/DragManager';
+import { dropActionType } from '../../util/DropActionTypes';
 import { SelectionManager } from '../../util/SelectionManager';
-import { SettingsManager } from '../../util/SettingsManager';
 import { SnappingManager } from '../../util/SnappingManager';
 import { Transform } from '../../util/Transform';
 import { UndoManager, undoable } from '../../util/UndoManager';
 import { DashboardView } from '../DashboardView';
+import { PinProps } from '../DocComponent';
 import { LightboxView } from '../LightboxView';
 import { ObservableReactComponent } from '../ObservableReactComponent';
 import { DefaultStyleProvider, StyleProp } from '../StyleProvider';
@@ -32,12 +34,12 @@ import { Colors } from '../global/globalEnums';
 import { DocumentView, OpenWhere, OpenWhereMod, returnEmptyDocViewList } from '../nodes/DocumentView';
 import { FieldViewProps, FocusViewOptions } from '../nodes/FieldView';
 import { KeyValueBox } from '../nodes/KeyValueBox';
-import { DashFieldView } from '../nodes/formattedText/DashFieldView';
-import { PinProps, PresBox, PresMovement } from '../nodes/trails';
+import { PresBox, PresMovement } from '../nodes/trails';
 import { CollectionDockingView } from './CollectionDockingView';
 import { CollectionView } from './CollectionView';
 import './TabDocView.scss';
 import { CollectionFreeFormView } from './collectionFreeForm/CollectionFreeFormView';
+
 const _global = (window /* browser */ || global) /* node */ as any;
 
 interface TabDocViewProps {
@@ -105,7 +107,6 @@ export class TabDocView extends ObservableReactComponent<TabDocViewProps> {
                 while (child?.children.length) {
                     const next = Array.from(child.children).find(c => c.className?.toString().includes('SVGAnimatedString') || typeof c.className === 'string');
                     if (next?.className?.toString().includes(DocumentView.ROOT_DIV)) break;
-                    if (next?.className?.toString().includes(DashFieldView.name)) break;
                     if (next) child = next;
                     else break;
                 }
@@ -149,17 +150,17 @@ export class TabDocView extends ObservableReactComponent<TabDocViewProps> {
                 };
 
                 const docIcon = <FontAwesomeIcon onPointerDown={dragBtnDown} icon={iconType} />;
-                const closeIcon = <FontAwesomeIcon icon={'eye'} />;
+                const closeIcon = <FontAwesomeIcon icon="eye" />;
                 ReactDOM.createRoot(iconWrap).render(docIcon);
                 ReactDOM.createRoot(closeWrap).render(closeIcon);
                 tab.reactComponents = [iconWrap, closeWrap];
                 tab.element[0].prepend(iconWrap);
                 tab._disposers.color = reaction(
-                    () => ({ variant: SettingsManager.userVariantColor, degree: Doc.GetBrushStatus(doc), highlight: DefaultStyleProvider(this._document, undefined, StyleProp.Highlighting) }),
+                    () => ({ variant: SnappingManager.userVariantColor, degree: Doc.GetBrushStatus(doc), highlight: DefaultStyleProvider(this._document, undefined, StyleProp.Highlighting) }),
                     ({ variant, degree, highlight }) => {
                         const color = highlight?.highlightIndex === Doc.DocBrushStatus.highlighted ? highlight.highlightColor : degree ? ['transparent', variant, variant, 'orange'][degree] : variant;
 
-                        const textColor = color === variant ? SettingsManager.userColor : lightOrDark(color);
+                        const textColor = color === variant ? SnappingManager.userColor : lightOrDark(color);
                         titleEle.style.color = textColor;
                         iconWrap.style.color = textColor;
                         closeWrap.style.color = textColor;
@@ -407,9 +408,7 @@ export class TabDocView extends ObservableReactComponent<TabDocViewProps> {
         return false;
     };
 
-    getCurrentFrame = () => {
-        return NumCast(Cast(PresBox.Instance.activeItem.presentation_targetDoc, Doc, null)._currentFrame);
-    };
+    getCurrentFrame = () => NumCast(Cast(PresBox.Instance.activeItem.presentation_targetDoc, Doc, null)._currentFrame);
     static Activate = (tabDoc: Doc) => {
         const tab = Array.from(CollectionDockingView.Instance?.tabMap!).find(tab => tab.DashDoc === tabDoc && !tab.contentItem.config.props.keyValue);
         tab?.header.parent.setActiveContentItem(tab.contentItem); // glr: Panning does not work when this is set - (this line is for trying to make a tab that is not topmost become topmost)
@@ -426,7 +425,7 @@ export class TabDocView extends ObservableReactComponent<TabDocViewProps> {
     @observable _forceInvalidateScreenToLocal = 0;
     ScreenToLocalTransform = () => {
         this._forceInvalidateScreenToLocal;
-        const { translateX, translateY } = Utils.GetScreenTransform(this._mainCont?.children?.[0] as HTMLElement);
+        const { translateX, translateY } = ClientUtils.GetScreenTransform(this._mainCont?.children?.[0] as HTMLElement);
         return CollectionDockingView.Instance?.ScreenToLocalBoxXf().translate(-translateX, -translateY) ?? Transform.Identity();
     };
     PanelWidth = () => this._panelWidth;
@@ -434,7 +433,9 @@ export class TabDocView extends ObservableReactComponent<TabDocViewProps> {
     miniMapColor = () => Colors.MEDIUM_GRAY;
     tabView = () => this._view;
     disableMinimap = () => !this._document;
-    whenChildContentActiveChanges = (isActive: boolean) => (this._isAnyChildContentActive = isActive);
+    whenChildContentActiveChanges = (isActive: boolean) => {
+        this._isAnyChildContentActive = isActive;
+    };
     isContentActive = () => this._isContentActive;
     waitForDoubleClick = () => (SnappingManager.ExploreMode ? 'never' : undefined);
     @computed get docView() {
@@ -465,9 +466,9 @@ export class TabDocView extends ObservableReactComponent<TabDocViewProps> {
                     addDocument={undefined}
                     removeDocument={this.remDocTab}
                     addDocTab={this.addDocTab}
-                    suppressSetHeight={this._document._layout_fitWidth ? true : false}
+                    suppressSetHeight={!!this._document._layout_fitWidth}
                     ScreenToLocalTransform={this.ScreenToLocalTransform}
-                    dontCenter={'y'}
+                    dontCenter="y"
                     whenChildContentsActiveChanged={this.whenChildContentActiveChanges}
                     focus={this.focusFunc}
                     containerViewPath={returnEmptyDoclist}
@@ -485,12 +486,13 @@ export class TabDocView extends ObservableReactComponent<TabDocViewProps> {
                 style={{
                     fontFamily: Doc.UserDoc().renderStyle === 'comic' ? 'Comic Sans MS' : undefined,
                 }}
-                onPointerOver={action(() => (this._hovering = true))}
-                onPointerLeave={action(() => (this._hovering = false))}
-                onDragOver={action(() => (this._hovering = true))}
-                onDragLeave={action(() => (this._hovering = false))}
+                onPointerOver={action(() =>  { this._hovering = true; })} // prettier-ignore
+                onPointerLeave={action(() => { this._hovering = false; })} // prettier-ignore
+                onDragOver={action(() =>     { this._hovering = true; })} // prettier-ignore
+                onDragLeave={action(() =>    { this._hovering = false; })} // prettier-ignore
                 ref={ref => {
-                    if ((this._mainCont = ref)) {
+                    this._mainCont = ref;
+                    if (this._mainCont) {
                         if (this._lastTab) {
                             this._view && DocumentManager.Instance.RemoveView(this._view);
                         }
@@ -524,7 +526,8 @@ interface TabMiniThumbProps {
 @observer
 class TabMiniThumb extends React.Component<TabMiniThumbProps> {
     render() {
-        return <div className="miniThumb" style={{ width: `${this.props.miniWidth()}% `, height: `${this.props.miniHeight()}% `, left: `${this.props.miniLeft()}% `, top: `${this.props.miniTop()}% ` }} />;
+        const { miniWidth, miniHeight, miniLeft, miniTop } = this.props;
+        return <div className="miniThumb" style={{ width: `${miniWidth()}%`, height: `${miniHeight()}%`, left: `${miniLeft()}%`, top: `${miniTop()}%` }} />;
     }
 }
 @observer
@@ -532,14 +535,10 @@ export class TabMinimapView extends ObservableReactComponent<TabMinimapViewProps
     static miniStyleProvider = (doc: Opt<Doc>, props: Opt<FieldViewProps>, property: string): any => {
         if (doc) {
             switch (property.split(':')[0]) {
-                default:
-                    return DefaultStyleProvider(doc, props, property);
-                case StyleProp.PointerEvents:
-                    return 'none';
-                case StyleProp.DocContents:
-                    const background = ((type: DocumentType) => {
-                        // prettier-ignore
-                        switch (type) {
+                case StyleProp.PointerEvents: return 'none';
+                case StyleProp.DocContents: {
+                    const background = (() => {
+                        switch (doc.type as DocumentType) {
                             case DocumentType.PDF:    return 'pink';
                             case DocumentType.AUDIO:  return 'lightgreen';
                             case DocumentType.WEB:    return 'brown';
@@ -549,10 +548,12 @@ export class TabMinimapView extends ObservableReactComponent<TabMinimapViewProps
                             case DocumentType.RTF:    return 'yellow';
                             case DocumentType.COL:    return undefined;
                             default:                  return 'gray';
-                        }
-                    })(doc.type as DocumentType);
+                        } // prettier-ignore
+                    })();
                     return !background ? undefined : <div style={{ width: NumCast(doc._width), height: NumCast(doc._height), position: 'absolute', display: 'block', background }} />;
-            }
+                }
+                default: return DefaultStyleProvider(doc, props, property);
+            } // prettier-ignore
         }
     };
 
@@ -639,7 +640,7 @@ export class TabMinimapView extends ObservableReactComponent<TabMinimapViewProps
     render() {
         return this._props.document.layout !== CollectionView.LayoutString(Doc.LayoutFieldKey(this._props.document)) || this._props.document?._type_collection !== CollectionViewType.Freeform ? null : (
             <div className="miniMap-hidden">
-                <Popup icon={<FontAwesomeIcon icon="globe-asia" size="lg" />} color={SettingsManager.userVariantColor} type={Type.TERT} onPointerDown={e => e.stopPropagation()} placement="top-end" popup={this.popup} />
+                <Popup icon={<FontAwesomeIcon icon="globe-asia" size="lg" />} color={SnappingManager.userVariantColor} type={Type.TERT} onPointerDown={e => e.stopPropagation()} placement="top-end" popup={this.popup} />
             </div>
         );
     }
diff --git a/src/client/views/collections/TreeView.tsx b/src/client/views/collections/TreeView.tsx
index 4fd49f8fe..e266ccd14 100644
--- a/src/client/views/collections/TreeView.tsx
+++ b/src/client/views/collections/TreeView.tsx
@@ -1,11 +1,15 @@
+/* eslint-disable jsx-a11y/no-static-element-interactions */
+/* eslint-disable jsx-a11y/no-noninteractive-element-interactions */
+/* eslint-disable jsx-a11y/click-events-have-key-events */
 import { IconProp } from '@fortawesome/fontawesome-svg-core';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { IconButton, Size } from 'browndash-components';
 import { IReactionDisposer, action, computed, makeObservable, observable, reaction } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
-import { Utils, emptyFunction, lightOrDark, return18, returnEmptyDoclist, returnEmptyFilter, returnEmptyString, returnFalse, returnTrue, returnZero, setupMoveUpEvents, simulateMouseClick } from '../../../Utils';
-import { Doc, DocListCast, Field, FieldResult, Opt, StrListCast } from '../../../fields/Doc';
+import { ClientUtils, lightOrDark, return18, returnEmptyDoclist, returnEmptyFilter, returnEmptyString, returnFalse, returnTrue, returnZero, setupMoveUpEvents, simulateMouseClick } from '../../../ClientUtils';
+import { emptyFunction } from '../../../Utils';
+import { Doc, DocListCast, Field, FieldType, FieldResult, Opt, StrListCast } from '../../../fields/Doc';
 import { DocData } from '../../../fields/DocSymbols';
 import { Id } from '../../../fields/FieldSymbols';
 import { List } from '../../../fields/List';
@@ -17,7 +21,8 @@ import { TraceMobx } from '../../../fields/util';
 import { CollectionViewType, DocumentType } from '../../documents/DocumentTypes';
 import { DocUtils, Docs } from '../../documents/Documents';
 import { DocumentManager } from '../../util/DocumentManager';
-import { DragManager, dropActionType } from '../../util/DragManager';
+import { DragManager } from '../../util/DragManager';
+import { dropActionType } from '../../util/DropActionTypes';
 import { LinkManager } from '../../util/LinkManager';
 import { SettingsManager } from '../../util/SettingsManager';
 import { SnappingManager } from '../../util/SnappingManager';
@@ -35,10 +40,12 @@ import { CollectionTreeView, TreeViewType } from './CollectionTreeView';
 import { CollectionView } from './CollectionView';
 import { TreeSort } from './TreeSort';
 import './TreeView.scss';
+
 const { TREE_BULLET_WIDTH } = require('../global/globalCssVariables.module.scss'); // prettier-ignore
 
 export interface TreeViewProps {
     treeView: CollectionTreeView;
+    // eslint-disable-next-line no-use-before-define
     parentTreeView: TreeView | CollectionTreeView | undefined;
     observeHeight: (ref: any) => void;
     unobserveHeight: (ref: any) => void;
@@ -87,6 +94,7 @@ const treeBulletWidth = function () {
  */
 @observer
 export class TreeView extends ObservableReactComponent<TreeViewProps> {
+    // eslint-disable-next-line no-use-before-define
     static _editTitleOnLoad: Opt<{ id: string; parent: TreeView | CollectionTreeView | undefined }>;
     static _openTitleScript: Opt<ScriptField | undefined>;
     static _openLevelScript: Opt<ScriptField | undefined>;
@@ -101,6 +109,9 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> {
     get treeViewOpenIsTransient() {
         return this.treeView.Document.treeView_OpenIsTransient || Doc.IsDataProto(this.Document);
     }
+    @computed get treeViewOpen() {
+        return (!this.treeViewOpenIsTransient && Doc.GetT(this.Document, 'treeView_Open', 'boolean', true)) || this._transientOpenState;
+    }
     set treeViewOpen(c: boolean) {
         if (this.treeViewOpenIsTransient) this._transientOpenState = c;
         else {
@@ -137,9 +148,6 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> {
     @computed get Document() {
         return this._props.Document;
     }
-    @computed get treeViewOpen() {
-        return (!this.treeViewOpenIsTransient && Doc.GetT(this.Document, 'treeView_Open', 'boolean', true)) || this._transientOpenState;
-    }
     @computed get treeViewExpandedView() {
         return this.validExpandViewTypes.includes(StrCast(this.Document.treeView_ExpandedView)) ? StrCast(this.Document.treeView_ExpandedView) : this.defaultExpandedView;
     }
@@ -221,7 +229,7 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> {
             this.treeViewOpen = !this.treeViewOpen;
         } else {
             // choose an appropriate embedding or make one. --- choose the first embedding that (1) user owns,  (2) has no context field ... otherwise make a new embedding
-            const bestEmbedding = docView.Document.author === Doc.CurrentUserEmail && !Doc.IsDataProto(docView.Document) ? docView.Document : Doc.BestEmbedding(docView.Document);
+            const bestEmbedding = docView.Document.author === ClientUtils.CurrentUserEmail && !Doc.IsDataProto(docView.Document) ? docView.Document : Doc.BestEmbedding(docView.Document);
             this._props.addDocTab(bestEmbedding, OpenWhere.lightbox);
         }
     };
@@ -230,7 +238,7 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> {
     recurToggle = (childList: Doc[]) => {
         if (childList.length > 0) {
             childList.forEach(child => {
-                child.runProcess = !!!child.runProcess;
+                child.runProcess = !child.runProcess;
                 TreeView.ToggleChildrenRun.get(child)?.();
             });
         }
@@ -273,9 +281,7 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> {
             this.recurToggle(this.childDocs);
         });
 
-        TreeView.GetRunningChildren.set(this.Document, () => {
-            return this.getRunningChildren(this.childDocs);
-        });
+        TreeView.GetRunningChildren.set(this.Document, () => this.getRunningChildren(this.childDocs));
     }
 
     _treeEle: any;
@@ -301,7 +307,9 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> {
         super.componentDidUpdate(prevProps);
         this._disposers.opening = reaction(
             () => this.treeViewOpen,
-            open => !open && (this._renderCount = 20)
+            open => {
+                !open && (this._renderCount = 20);
+            }
         );
         this._props.hierarchyIndex !== undefined && this._props.AddToMap?.(this.Document, this._props.hierarchyIndex);
     }
@@ -324,7 +332,7 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> {
             document.addEventListener('pointerup', this.onDragUp, true);
         }
     };
-    onPointerLeave = (e: React.PointerEvent): void => {
+    onPointerLeave = (): void => {
         Doc.UnBrushDoc(this.dataDoc);
         if (this._header.current?.className !== 'treeView-header-editing') {
             this._header.current!.className = 'treeView-header';
@@ -385,7 +393,7 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> {
         return this.localAdd(folder);
     };
 
-    preTreeDrop = (e: Event, de: DragManager.DropEvent, docDropAction: dropActionType) => {
+    preTreeDrop = () => {
         // fall through and let the CollectionTreeView handle this since treeView items have no special properties of their own
     };
 
@@ -403,7 +411,7 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> {
             e.stopPropagation();
             return true;
         }
-        const docDragData = de.complete.docDragData;
+        const { docDragData } = de.complete;
         if (docDragData && pt[0] < rect.left + rect.width) {
             if (docDragData.draggedDocuments[0] === this.Document) return true;
             const added = this.dropDocuments(
@@ -462,8 +470,8 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> {
 
     refTransform = (ref: HTMLDivElement | undefined | null) => {
         if (!ref) return this.ScreenToLocalTransform();
-        const { scale, translateX, translateY } = Utils.GetScreenTransform(ref);
-        const outerXf = Utils.GetScreenTransform(this.treeView.MainEle());
+        const { translateX, translateY } = ClientUtils.GetScreenTransform(ref);
+        const outerXf = ClientUtils.GetScreenTransform(this.treeView.MainEle());
         const offset = this.ScreenToLocalTransform().transformDirection(outerXf.translateX - translateX, outerXf.translateY - translateY);
         return this.ScreenToLocalTransform().translate(offset[0], offset[1]);
     };
@@ -490,14 +498,19 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> {
         const ids: { [key: string]: string } = {};
         const rows: JSX.Element[] = [];
         const doc = this.Document;
-        doc && Object.keys(doc).forEach(key => !(key in ids) && doc[key] !== ComputedField.undefined && (ids[key] = key));
+        doc &&
+            Object.keys(doc).forEach(key => {
+                !(key in ids) && doc[key] !== ComputedField.undefined && (ids[key] = key);
+            });
 
+        // eslint-disable-next-line no-restricted-syntax
         for (const key of Object.keys(ids).slice().sort()) {
+            // eslint-disable-next-line no-continue
             if (this._props.skipFields?.includes(key) || key === 'title' || key === 'treeView_Open') continue;
             const contents = doc[key];
             let contentElement: (JSX.Element | null)[] | JSX.Element = [];
 
-            let leftOffset = observable({ width: 0 });
+            const leftOffset = observable({ width: 0 });
             const expandedWidth = () => this._props.panelWidth() - leftOffset.width;
             if (contents instanceof Doc || (Cast(contents, listSpec(Doc)) && Cast(contents, listSpec(Doc))!.length && Cast(contents, listSpec(Doc))![0] instanceof Doc)) {
                 const remDoc = (doc: Doc | Doc[]) => this.remove(doc, key);
@@ -549,7 +562,7 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> {
                 contentElement = (
                     <EditableView
                         key="editableView"
-                        contents={contents !== undefined ? Field.toString(contents as Field) : 'null'}
+                        contents={contents !== undefined ? Field.toString(contents as FieldType) : 'null'}
                         height={13}
                         fontSize={12}
                         GetValue={() => Field.toKeyValueString(doc, key)}
@@ -572,15 +585,15 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> {
             );
         }
         rows.push(
-            <div style={{ display: 'flex', overflow: 'auto' }} key={'newKeyValue'}>
+            <div style={{ display: 'flex', overflow: 'auto' }} key="newKeyValue">
                 <EditableView
                     key="editableView"
-                    contents={'+key=value'}
+                    contents="+key=value"
                     height={13}
                     fontSize={12}
                     GetValue={returnEmptyString}
                     SetValue={input => {
-                        const match = input.match(/([a-zA-Z0-9_-]+)(=|=:=)([a-zA-Z,_@\?\+\-\*\/\ 0-9\(\)]+)/);
+                        const match = input.match(/([a-zA-Z0-9_-]+)(=|=:=)([a-zA-Z,_@?+\-*/ 0-9()]+)/);
                         if (match) {
                             const key = match[1];
                             const assign = match[2];
@@ -620,7 +633,9 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> {
                     const docs = TreeView.sortDocs(this.childDocs || ([] as Doc[]), ordering);
                     doc.zIndex = addBefore ? NumCast(addBefore.zIndex) + (before ? -0.5 : 0.5) : 1000;
                     docs.push(doc);
-                    docs.sort((a, b) => (NumCast(a.zIndex) > NumCast(b.zIndex) ? 1 : -1)).forEach((d, i) => (d.zIndex = i));
+                    docs.sort((a, b) => (NumCast(a.zIndex) > NumCast(b.zIndex) ? 1 : -1)).forEach((d, i) => {
+                        d.zIndex = i;
+                    });
                 }
                 const dataIsComputed = ComputedField.WithoutComputed(() => FieldValue(this.dataDoc[key])) instanceof ComputedField;
                 const added = (!dataIsComputed || (this.dropping && this.moving)) && Doc.AddDocToList(this.dataDoc, key, doc, addBefore, before, false);
@@ -630,8 +645,8 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> {
             };
             const addDoc = (doc: Doc | Doc[], addBefore?: Doc, before?: boolean) => (doc instanceof Doc ? [doc] : doc).reduce((flg, doc) => flg && localAdd(doc, addBefore, before), true);
             const docs = expandKey === 'embeddings' ? this.childEmbeddings : expandKey === 'links' ? this.childLinks : expandKey === 'annotations' ? this.childAnnos : this.childDocs;
-            let downX = 0,
-                downY = 0;
+            let downX = 0;
+            let downY = 0;
             if (docs?.length && this._renderCount < docs?.length) {
                 this._renderTimer && clearTimeout(this._renderTimer);
                 this._renderTimer = setTimeout(
@@ -667,7 +682,7 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> {
                         style={{ cursor: 'inherit' }}
                         key={expandKey + 'more'}
                         title={`Sorted by : ${this.Document.treeView_SortCriterion}. click to cycle`}
-                        className="" //this.doc.treeView_HideTitle ? 'no-indent' : ''}
+                        className="" // this.doc.treeView_HideTitle ? 'no-indent' : ''}
                         onPointerDown={e => {
                             downX = e.clientX;
                             downY = e.clientY;
@@ -719,7 +734,8 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> {
                     </ul>
                 </div>
             );
-        } else if (this.treeViewExpandedView === 'fields') {
+        }
+        if (this.treeViewExpandedView === 'fields') {
             return (
                 <ul key={this.Document[Id] + this.Document.title} style={{ cursor: 'inherit' }}>
                     <div>{this.expandedField}</div>
@@ -891,14 +907,15 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> {
     titleStyleProvider = (doc: Doc | undefined, props: Opt<FieldViewProps>, property: string): any => {
         if (!doc || doc !== this.Document) return this._props?.treeView?._props.styleProvider?.(doc, props, property); // properties are inherited from the CollectionTreeView, not the hierarchical parent in the treeView
 
-        const treeView = this.treeView;
+        const { treeView } = this;
         // prettier-ignore
         switch (property.split(':')[0]) {
             case StyleProp.Opacity:         return this.treeView.outlineMode ? undefined : 1;
             case StyleProp.BackgroundColor: return this.selected ? '#7089bb' : undefined;//StrCast(doc._backgroundColor, StrCast(doc.backgroundColor));
             case StyleProp.Highlighting:    if (this.treeView.outlineMode) return undefined;
+                                            break;
             case StyleProp.BoxShadow:       return undefined;
-            case StyleProp.DocContents:
+            case StyleProp.DocContents: {
                 const highlightIndex = this.treeView.outlineMode ? Doc.DocBrushStatus.unbrushed : Doc.GetBrushHighlightStatus(doc);
                 const highlightColor = ['transparent', 'rgb(68, 118, 247)', 'rgb(68, 118, 247)', 'orange', 'lightBlue'][highlightIndex];
                 return treeView.outlineMode ? null : (
@@ -917,6 +934,8 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> {
                         {StrCast(doc?.title)}
                     </div>
                 );
+            }
+            default:
         }
         return treeView._props.styleProvider?.(doc, props, property);
     };
@@ -924,7 +943,7 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> {
         if (property.startsWith(StyleProp.Decorations)) return null;
         return this._props?.treeView?._props.styleProvider?.(doc, props, property); // properties are inherited from the CollectionTreeView, not the hierarchical parent in the treeView
     };
-    onKeyDown = (e: React.KeyboardEvent, fieldProps: FieldViewProps) => {
+    onKeyDown = (e: React.KeyboardEvent) => {
         if (this.Document.treeView_HideHeader || (this.Document.treeView_HideHeaderIfTemplate && this.treeView._props.childLayoutTemplate?.()) || this.treeView.outlineMode) {
             switch (e.key) {
                 case 'Tab':
@@ -944,6 +963,7 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> {
                     e.stopPropagation?.();
                     e.preventDefault?.();
                     return UndoManager.RunInBatch(this.makeTextCollection, 'bullet');
+                default:
             }
         }
         return false;
@@ -959,22 +979,24 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> {
         const view = this._editTitle ? (
             <EditableView
                 key="_editTitle"
-                oneLine={true}
-                display={'inline-block'}
+                oneLine
+                display="inline-block"
                 editing={this._editTitle}
-                background={'#7089bb'}
+                background="#7089bb"
                 contents={StrCast(this.Document.title)}
                 height={12}
-                sizeToContent={true}
+                sizeToContent
                 fontSize={12}
-                isEditingCallback={action(e => (this._editTitle = e))}
+                isEditingCallback={action(e => {
+                    this._editTitle = e;
+                })}
                 GetValue={() => StrCast(this.Document.title)}
                 OnTab={undoBatch((shift?: boolean) => {
                     if (!shift) this._props.indentDocument?.(true);
                     else this._props.outdentDocument?.(true);
                 })}
                 OnEmpty={undoBatch(() => this.treeView.outlineMode && this._props.removeDoc?.(this.Document))}
-                OnFillDown={val => this.treeView.fileSysMode && this.makeFolder()}
+                OnFillDown={() => this.treeView.fileSysMode && this.makeFolder()}
                 SetValue={undoBatch((value: string, shiftKey: boolean, enterKey: boolean) => {
                     Doc.SetInPlace(this.Document, 'title', value, false);
                     this.treeView.outlineMode && enterKey && this.makeTextCollection();
@@ -984,7 +1006,7 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> {
             <DocumentView
                 key="title"
                 ref={action((r: any) => {
-                    this._docRef = r ? r : undefined;
+                    this._docRef = r || undefined;
                     if (this._docRef && TreeView._editTitleOnLoad?.id === this.Document[Id] && TreeView._editTitleOnLoad.parent === this._props.parentTreeView) {
                         this._docRef.select(false);
                         this.setEditTitle(this._docRef);
@@ -994,8 +1016,8 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> {
                 Document={this.Document}
                 layout_fitWidth={returnTrue}
                 scriptContext={this}
-                hideDecorations={true}
-                hideClickBehaviors={true}
+                hideDecorations
+                hideClickBehaviors
                 styleProvider={this.titleStyleProvider}
                 onClickScriptDisable="never" // tree docViews have a script to show fields, etc.
                 containerViewPath={this.treeView.childContainerViewPath}
@@ -1015,7 +1037,7 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> {
                 PanelHeight={return18}
                 contextMenuItems={this.contextMenuItems}
                 renderDepth={1}
-                isContentActive={emptyFunction} //this._props.isContentActive}
+                isContentActive={emptyFunction} // this._props.isContentActive}
                 isDocumentActive={this._props.isContentActive}
                 focus={this.refocus}
                 whenChildContentsActiveChanged={this._props.whenChildContentsActiveChanged}
@@ -1045,99 +1067,101 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> {
                     }}>
                     {view}
                 </div>
-                <div className="treeView-rightButtons" ref={action((r: any) => r && (this.headerEleWidth = r.getBoundingClientRect().width))}>
+                <div
+                    className="treeView-rightButtons"
+                    ref={action((r: any) => {
+                        r && (this.headerEleWidth = r.getBoundingClientRect().width);
+                    })}>
                     {this.titleButtons}
                 </div>
             </>
         );
     }
 
-    renderBulletHeader = (contents: JSX.Element, editing: boolean) => {
-        return (
-            <>
+    renderBulletHeader = (contents: JSX.Element, editing: boolean) => (
+        <>
+            <div
+                className={`treeView-header` + (editing ? '-editing' : '')}
+                key="titleheader"
+                ref={this._header}
+                onClick={this.ignoreEvent}
+                onPointerDown={e => {
+                    this.treeView.isContentActive() &&
+                        setupMoveUpEvents(
+                            this,
+                            e,
+                            () => {
+                                (this._dref ?? this._docRef)?.startDragging(e.clientX, e.clientY, '' as any);
+                                return true;
+                            },
+                            returnFalse,
+                            emptyFunction
+                        );
+                }}
+                onPointerEnter={this.onPointerEnter}
+                onPointerLeave={this.onPointerLeave}>
                 <div
-                    className={`treeView-header` + (editing ? '-editing' : '')}
-                    key="titleheader"
-                    ref={this._header}
-                    onClick={this.ignoreEvent}
-                    onPointerDown={e => {
-                        this.treeView.isContentActive() &&
-                            setupMoveUpEvents(
-                                this,
-                                e,
-                                () => {
-                                    (this._dref ?? this._docRef)?.startDragging(e.clientX, e.clientY, '' as any);
-                                    return true;
-                                },
-                                returnFalse,
-                                emptyFunction
-                            );
+                    className="treeView-background"
+                    style={{
+                        background: SettingsManager.userColor,
                     }}
-                    onPointerEnter={this.onPointerEnter}
-                    onPointerLeave={this.onPointerLeave}>
-                    <div
-                        className="treeView-background"
-                        style={{
-                            background: SettingsManager.userColor,
-                        }}
-                    />
-                    {contents}
-                </div>
-                {this.renderBorder}
-            </>
-        );
-    };
-
-    fitWidthFilter = (doc: Doc) => (doc.type === DocumentType.IMG ? false : undefined);
-    renderEmbeddedDocument = (asText: boolean, isActive: () => boolean | undefined) => {
-        return (
-            <div style={{ height: this.embeddedPanelHeight(), width: this.embeddedPanelWidth() }}>
-                <DocumentView
-                    key={this.Document[Id]}
-                    ref={action((r: DocumentView | null) => (this._dref = r))}
-                    Document={this.Document}
-                    layout_fitWidth={this.fitWidthFilter}
-                    PanelWidth={this.embeddedPanelWidth}
-                    PanelHeight={this.embeddedPanelHeight}
-                    LayoutTemplateString={asText ? FormattedTextBox.LayoutString('text') : undefined}
-                    LayoutTemplate={this.treeView._props.childLayoutTemplate}
-                    isContentActive={isActive}
-                    isDocumentActive={isActive}
-                    styleProvider={asText ? this.titleStyleProvider : this.embeddedStyleProvider}
-                    fitContentsToBox={returnTrue}
-                    hideTitle={asText}
-                    hideDecorations={true}
-                    hideClickBehaviors={true}
-                    hideLinkButton={BoolCast(this.treeView.Document.childHideLinkButton)}
-                    dontRegisterView={BoolCast(this.treeView.Document.childDontRegisterViews, this._props.dontRegisterView)}
-                    ScreenToLocalTransform={this.docTransform}
-                    renderDepth={this._props.renderDepth + 1}
-                    onClickScript={this.onChildClick}
-                    onKey={this.onKeyDown}
-                    containerViewPath={this.treeView.childContainerViewPath}
-                    childFilters={returnEmptyFilter}
-                    childFiltersByRanges={returnEmptyFilter}
-                    searchFilterDocs={returnEmptyDoclist}
-                    focus={this.refocus}
-                    addDocument={this._props.addDocument}
-                    moveDocument={this.move}
-                    removeDocument={this._props.removeDoc}
-                    whenChildContentsActiveChanged={this._props.whenChildContentsActiveChanged}
-                    xPadding={NumCast(this.treeView.Document.childXPadding, this.treeView._props.childXPadding)}
-                    yPadding={NumCast(this.treeView.Document.childYPadding, this.treeView._props.childYPadding)}
-                    addDocTab={this._props.addDocTab}
-                    pinToPres={this.treeView._props.pinToPres}
-                    disableBrushing={this.treeView._props.disableBrushing}
-                    scriptContext={this}
                 />
+                {contents}
             </div>
-        );
-    };
+            {this.renderBorder}
+        </>
+    );
+
+    fitWidthFilter = (doc: Doc) => (doc.type === DocumentType.IMG ? false : undefined);
+    renderEmbeddedDocument = (asText: boolean, isActive: () => boolean | undefined) => (
+        <div style={{ height: this.embeddedPanelHeight(), width: this.embeddedPanelWidth() }}>
+            <DocumentView
+                key={this.Document[Id]}
+                ref={action((r: DocumentView | null) => {
+                    this._dref = r;
+                })}
+                Document={this.Document}
+                layout_fitWidth={this.fitWidthFilter}
+                PanelWidth={this.embeddedPanelWidth}
+                PanelHeight={this.embeddedPanelHeight}
+                LayoutTemplateString={asText ? FormattedTextBox.LayoutString('text') : undefined}
+                LayoutTemplate={this.treeView._props.childLayoutTemplate}
+                isContentActive={isActive}
+                isDocumentActive={isActive}
+                styleProvider={asText ? this.titleStyleProvider : this.embeddedStyleProvider}
+                fitContentsToBox={returnTrue}
+                hideTitle={asText}
+                hideDecorations
+                hideClickBehaviors
+                hideLinkButton={BoolCast(this.treeView.Document.childHideLinkButton)}
+                dontRegisterView={BoolCast(this.treeView.Document.childDontRegisterViews, this._props.dontRegisterView)}
+                ScreenToLocalTransform={this.docTransform}
+                renderDepth={this._props.renderDepth + 1}
+                onClickScript={this.onChildClick}
+                onKey={this.onKeyDown}
+                containerViewPath={this.treeView.childContainerViewPath}
+                childFilters={returnEmptyFilter}
+                childFiltersByRanges={returnEmptyFilter}
+                searchFilterDocs={returnEmptyDoclist}
+                focus={this.refocus}
+                addDocument={this._props.addDocument}
+                moveDocument={this.move}
+                removeDocument={this._props.removeDoc}
+                whenChildContentsActiveChanged={this._props.whenChildContentsActiveChanged}
+                xPadding={NumCast(this.treeView.Document.childXPadding, this.treeView._props.childXPadding)}
+                yPadding={NumCast(this.treeView.Document.childYPadding, this.treeView._props.childYPadding)}
+                addDocTab={this._props.addDocTab}
+                pinToPres={this.treeView._props.pinToPres}
+                disableBrushing={this.treeView._props.disableBrushing}
+                scriptContext={this}
+            />
+        </div>
+    );
 
     // renders the text version of a document as the header.  This is used in the file system mode and in other vanilla tree views.
     @computed get renderTitleAsHeader() {
         return this.treeView.Document.treeView_HideUnrendered && this.Document.layout_unrendered && !this.Document.treeView_FieldKey ? (
-            <div></div>
+            <div />
         ) : (
             <>
                 {this.renderBullet}
@@ -1147,14 +1171,12 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> {
     }
 
     // renders the document in the header field instead of a text proxy.
-    renderDocumentAsHeader = (asText: boolean) => {
-        return (
-            <>
-                {this.renderBullet}
-                {this.renderEmbeddedDocument(asText, this._props.isContentActive)}
-            </>
-        );
-    };
+    renderDocumentAsHeader = (asText: boolean) => (
+        <>
+            {this.renderBullet}
+            {this.renderEmbeddedDocument(asText, this._props.isContentActive)}
+        </>
+    );
 
     @computed get renderBorder() {
         const sorting = StrCast(this.Document.treeView_SortCriterion, TreeSort.WhenAdded);
@@ -1185,7 +1207,7 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> {
                 className={`treeView-container${this._props.isContentActive() ? '-active' : ''}`}
                 ref={this.createTreeDropTarget}
                 onDrop={this.onTreeDrop}
-                //onPointerDown={e => this._props.isContentActive(true) && SelectionManager.DeselectAll()} // bcz: this breaks entering a text filter in a filterBox since it deselects the filter's target document
+                // onPointerDown={e => this._props.isContentActive(true) && SelectionManager.DeselectAll()} // bcz: this breaks entering a text filter in a filterBox since it deselects the filter's target document
                 // onKeyDown={this.onKeyDown}
             >
                 <li className="collection-child">
@@ -1209,11 +1231,10 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> {
                     const aN = parseInt(a.match(reN)![0], 10);
                     const bN = parseInt(b.match(reN)![0], 10);
                     return aN === bN ? 0 : aN > bN ? 1 : -1;
-                } else {
-                    return aA > bA ? 1 : -1;
                 }
+                return aA > bA ? 1 : -1;
             };
-            docs.sort(function (d1, d2): 0 | 1 | -1 {
+            docs.sort((d1, d2): 0 | 1 | -1 => {
                 const a = criterion === TreeSort.AlphaUp ? d2 : d1;
                 const b = criterion === TreeSort.AlphaUp ? d1 : d2;
                 const first = a[criterion === TreeSort.Zindex ? 'zIndex' : 'title'];
@@ -1230,7 +1251,7 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> {
         childDocs: Doc[],
         treeView: CollectionTreeView,
         parentTreeView: CollectionTreeView | TreeView | undefined,
-        treeView_Parent: Doc,
+        treeViewParent: Doc,
         dataDoc: Doc | undefined,
         parentCollectionDoc: Doc | undefined,
         containerPrevSibling: Doc | undefined,
@@ -1244,7 +1265,7 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> {
         isContentActive: (outsideReaction?: boolean) => boolean,
         panelWidth: () => number,
         renderDepth: number,
-        treeView_HideHeaderFields: () => boolean,
+        treeViewHideHeaderFields: () => boolean,
         renderedIds: string[],
         onCheckedClick: undefined | (() => ScriptField),
         onChildClick: undefined | (() => ScriptField),
@@ -1261,19 +1282,19 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> {
         hierarchyIndex?: number[],
         renderCount?: number
     ) {
-        const viewSpecScript = Cast(treeView_Parent.viewSpecScript, ScriptField);
+        const viewSpecScript = Cast(treeViewParent.viewSpecScript, ScriptField);
         if (viewSpecScript) {
             childDocs = childDocs.filter(d => viewSpecScript.script.run({ doc: d }, console.log).result);
         }
 
-        const docs = TreeView.sortDocs(childDocs, StrCast(treeView_Parent.treeView_SortCriterion, TreeSort.WhenAdded));
+        const docs = TreeView.sortDocs(childDocs, StrCast(treeViewParent.treeView_SortCriterion, TreeSort.WhenAdded));
         const rowWidth = () => panelWidth() - treeBulletWidth() * (treeView._props.NativeDimScaling?.() || 1);
-        const treeView_Refs = new Map<Doc, TreeView | undefined>();
+        const treeViewRefs = new Map<Doc, TreeView | undefined>();
         return docs
             .filter(child => child instanceof Doc)
             .map((child, i) => {
                 if (renderCount && i > renderCount) return null;
-                const pair = Doc.GetLayoutDataDocPair(treeView_Parent, dataDoc, child);
+                const pair = Doc.GetLayoutDataDocPair(treeViewParent, dataDoc, child);
                 if (!pair.layout || pair.data instanceof Promise) {
                     return null;
                 }
@@ -1290,7 +1311,7 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> {
                         Doc.SetContainer(child, treeView.Document);
                     }
                 };
-                const indent = i === 0 ? undefined : (editTitle: boolean) => dentDoc(editTitle, docs[i - 1], undefined, treeView_Refs.get(docs[i - 1]));
+                const indent = i === 0 ? undefined : (editTitle: boolean) => dentDoc(editTitle, docs[i - 1], undefined, treeViewRefs.get(docs[i - 1]));
                 const outdent = !parentCollectionDoc ? undefined : (editTitle: boolean) => dentDoc(editTitle, parentCollectionDoc, containerPrevSibling, parentTreeView instanceof TreeView ? parentTreeView._props.parentTreeView : undefined);
                 const addDocument = (doc: Doc | Doc[], annotationKey?: string, relativeTo?: Doc, before?: boolean) => add(doc, relativeTo ?? docs[i], before !== undefined ? before : false);
                 const childLayout = Doc.Layout(pair.layout);
@@ -1301,10 +1322,10 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> {
                 return (
                     <TreeView
                         key={child[Id]}
-                        ref={r => treeView_Refs.set(child, r ? r : undefined)}
+                        ref={r => treeViewRefs.set(child, r || undefined)}
                         Document={pair.layout}
                         dataDoc={pair.data}
-                        treeViewParent={treeView_Parent}
+                        treeViewParent={treeViewParent}
                         prevSibling={docs[i]}
                         // TODO: [AL] add these
                         hierarchyIndex={hierarchyIndex ? [...hierarchyIndex, i + 1] : undefined}
@@ -1316,7 +1337,7 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> {
                         onCheckedClick={onCheckedClick}
                         onChildClick={onChildClick}
                         renderDepth={renderDepth}
-                        removeDoc={StrCast(treeView_Parent.treeView_FreezeChildren).includes('remove') ? undefined : remove}
+                        removeDoc={StrCast(treeViewParent.treeView_FreezeChildren).includes('remove') ? undefined : remove}
                         addDocument={addDocument}
                         styleProvider={styleProvider}
                         panelWidth={rowWidth}
@@ -1327,7 +1348,7 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> {
                         addDocTab={addDocTab}
                         ScreenToLocalTransform={screenToLocalXf}
                         isContentActive={isContentActive}
-                        treeViewHideHeaderFields={treeView_HideHeaderFields}
+                        treeViewHideHeaderFields={treeViewHideHeaderFields}
                         renderedIds={renderedIds}
                         skipFields={skipFields}
                         firstLevel={firstLevel}
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormInfoUI.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormInfoUI.tsx
index cc729decc..29d835b28 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormInfoUI.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormInfoUI.tsx
@@ -1,7 +1,7 @@
 import { makeObservable, observable, runInAction } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
-import { Doc, DocListCast, Field, FieldResult } from '../../../../fields/Doc';
+import { Doc, DocListCast, FieldType, FieldResult } from '../../../../fields/Doc';
 import { InkTool } from '../../../../fields/InkField';
 import { StrCast } from '../../../../fields/Types';
 import { DocumentManager } from '../../../util/DocumentManager';
@@ -10,13 +10,14 @@ import { ObservableReactComponent } from '../../ObservableReactComponent';
 import { DocButtonState, DocumentLinksButton } from '../../nodes/DocumentLinksButton';
 import { TopBar } from '../../topbar/TopBar';
 import { CollectionFreeFormInfoState, InfoState, StateEntryFunc, infoState } from './CollectionFreeFormInfoState';
-import { CollectionFreeFormView } from './CollectionFreeFormView';
 import './CollectionFreeFormView.scss';
+import { DocData } from '../../../../fields/DocSymbols';
 
 export interface CollectionFreeFormInfoUIProps {
     Document: Doc;
-    Freeform: CollectionFreeFormView;
-    close: () => boolean;
+    LayoutDoc: Doc;
+    childDocs: () => Doc[];
+    close: () => void;
 }
 
 @observer
@@ -32,10 +33,10 @@ export class CollectionFreeFormInfoUI extends ObservableReactComponent<Collectio
 
     @observable _currState: infoState | undefined = undefined;
     get currState() { return this._currState; } // prettier-ignore
-    set currState(val) { runInAction(() => (this._currState = val)); } // prettier-ignore
+    set currState(val) { runInAction(() => {this._currState = val;}); } // prettier-ignore
 
     componentWillUnmount(): void {
-        this._props.Freeform.dataDoc.backgroundColor = this._originalbackground;
+        this._props.Document[DocData].backgroundColor = this._originalbackground;
     }
 
     setCurrState = (state: infoState) => {
@@ -46,16 +47,16 @@ export class CollectionFreeFormInfoUI extends ObservableReactComponent<Collectio
     };
 
     setupStates = () => {
-        this._originalbackground = StrCast(this._props.Freeform.dataDoc.backgroundColor);
+        this._originalbackground = StrCast(this._props.Document[DocData].backgroundColor);
         //  state entry functions
-        const setBackground = (colour: string) => () => (this._props.Freeform.dataDoc.backgroundColor = colour);
-        const setOpacity = (opacity: number) => () => (this._props.Freeform.layoutDoc.opacity = opacity);
+        const setBackground = (colour: string) => () => {this._props.Document[DocData].backgroundColor = colour;} // prettier-ignore
+        const setOpacity = (opacity: number) => () => {this._props.LayoutDoc.opacity = opacity;} // prettier-ignore
         //  arc transition trigger conditions
-        const firstDoc = () => (this._props.Freeform.childDocs.length ? this._props.Freeform.childDocs[0] : undefined);
-        const numDocs = () => this._props.Freeform.childDocs.length;
+        const firstDoc = () => (this._props.childDocs().length ? this._props.childDocs()[0] : undefined);
+        const numDocs = () => this._props.childDocs().length;
 
-        let docX: FieldResult<Field>;
-        let docY: FieldResult<Field>;
+        let docX: FieldResult<FieldType>;
+        let docY: FieldResult<FieldType>;
 
         const docNewX = () => firstDoc()?.x;
         const docNewY = () => firstDoc()?.y;
@@ -244,12 +245,12 @@ export class CollectionFreeFormInfoUI extends ObservableReactComponent<Collectio
             // editPresentation: [() => presentationMode() === 'edit', () => editPresentationMode],
             manualPresentation: [() => presentationMode() === 'manual', () => manualPresentationMode],
             docRemoved: [() => numDocs() < 3, () => viewedLink],
-            docCreated: [() => numDocs() == 4, () => completed],
+            docCreated: [() => numDocs() === 4, () => completed],
         });
 
         const completed = InfoState(
             'Eager to learn more? Click the ? icon in the top right corner to read our full documentation.',
-            { docRemoved: [() => numDocs() == 1, () => oneDoc] },
+            { docRemoved: [() => numDocs() === 1, () => oneDoc] },
             'documentation.png',
             () => TopBar.Instance.FlipDocumentationIcon()
         ); // prettier-ignore
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
index c83c26509..a0b96c75a 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
@@ -1,4 +1,4 @@
-import { Doc, Field, FieldResult } from '../../../../fields/Doc';
+import { Doc, Field, FieldType, FieldResult } from '../../../../fields/Doc';
 import { Id, ToString } from '../../../../fields/FieldSymbols';
 import { ObjectField } from '../../../../fields/ObjectField';
 import { RefField } from '../../../../fields/RefField';
@@ -50,7 +50,7 @@ export interface ViewDefResult {
     bounds?: ViewDefBounds;
     inkMask?: number; //sort elements into either the mask layer (which has a mixedBlendMode appropriate for transparent masks), or the regular documents layer;   -1 = no mask, 0 = mask layer but stroke is transparent (hidden, as in during a presentation when you want to smoothly animate it into being a mask), >0 = mask layer and not hidden
 }
-function toLabel(target: FieldResult<Field>) {
+function toLabel(target: FieldResult<FieldType>) {
     if (typeof target === 'number' || Number(target)) {
         const truncated = Number(Number(target).toFixed(0));
         const precise = Number(Number(target).toFixed(2));
@@ -128,7 +128,7 @@ export function computeStarburstLayout(poolData: Map<string, PoolData>, pivotDoc
 export function computePivotLayout(poolData: Map<string, PoolData>, pivotDoc: Doc, childPairs: { layout: Doc; data?: Doc }[], panelDim: number[], viewDefsToJSX: (views: ViewDefBounds[]) => ViewDefResult[], engineProps: any) {
     const docMap = new Map<string, PoolData>();
     const fieldKey = 'data';
-    const pivotColumnGroups = new Map<FieldResult<Field>, PivotColumn>();
+    const pivotColumnGroups = new Map<FieldResult<FieldType>, PivotColumn>();
 
     let nonNumbers = 0;
     const pivotFieldKey = toLabel(engineProps?.pivotField ?? pivotDoc._pivotField) || 'author';
@@ -136,10 +136,10 @@ export function computePivotLayout(poolData: Map<string, PoolData>, pivotDoc: Do
         const listValue = Cast(pair.layout[pivotFieldKey], listSpec('string'), null);
 
         const num = toNumber(pair.layout[pivotFieldKey]);
-        if (num === undefined || Number.isNaN(num)) {
+        if (num === undefined || isNaN(num)) {
             nonNumbers++;
         }
-        const val = Field.toString(pair.layout[pivotFieldKey] as Field);
+        const val = Field.toString(pair.layout[pivotFieldKey] as FieldType);
         if (listValue) {
             listValue.forEach((val, i) => {
                 !pivotColumnGroups.get(val) && pivotColumnGroups.set(val, { docs: [], filters: [val], replicas: [] });
@@ -265,7 +265,7 @@ export function computePivotLayout(poolData: Map<string, PoolData>, pivotDoc: Do
     return normalizeResults(panelDim, max_text, docMap, poolData, viewDefsToJSX, groupNames, 0, []);
 }
 
-function toNumber(val: FieldResult<Field>) {
+function toNumber(val: FieldResult<FieldType>) {
     return val === undefined ? undefined : NumCast(val, Number(StrCast(val)));
 }
 
@@ -274,7 +274,7 @@ export function computeTimelineLayout(poolData: Map<string, PoolData>, pivotDoc:
     const pivotDateGroups = new Map<number, Doc[]>();
     const docMap = new Map<string, PoolData>();
     const groupNames: ViewDefBounds[] = [];
-    const timelineFieldKey = Field.toString(pivotDoc._pivotField as Field);
+    const timelineFieldKey = Field.toString(pivotDoc._pivotField as FieldType);
     const curTime = toNumber(pivotDoc[fieldKey + '-timelineCur']);
     const curTimeSpan = Cast(pivotDoc[fieldKey + '-timelineSpan'], 'number', null);
     const minTimeReq = curTimeSpan === undefined ? Cast(pivotDoc[fieldKey + '-timelineMinReq'], 'number', null) : curTime && curTime - curTimeSpan;
@@ -290,7 +290,7 @@ export function computeTimelineLayout(poolData: Map<string, PoolData>, pivotDoc:
     let maxTime = maxTimeReq === undefined ? -Number.MAX_VALUE : maxTimeReq;
     childPairs.forEach(pair => {
         const num = NumCast(pair.layout[timelineFieldKey], Number(StrCast(pair.layout[timelineFieldKey])));
-        if (!Number.isNaN(num) && (!minTimeReq || num >= minTimeReq) && (!maxTimeReq || num <= maxTimeReq)) {
+        if (!isNaN(num) && (!minTimeReq || num >= minTimeReq) && (!maxTimeReq || num <= maxTimeReq)) {
             !pivotDateGroups.get(num) && pivotDateGroups.set(num, []);
             pivotDateGroups.get(num)!.push(pair.layout);
             minTime = Math.min(num, minTime);
@@ -400,7 +400,7 @@ function normalizeResults(
     const height = aggBounds.b - aggBounds.y === 0 ? 1 : aggBounds.b - aggBounds.y;
     const wscale = panelDim[0] / width;
     let scale = wscale * height > panelDim[1] ? panelDim[1] / height : wscale;
-    if (Number.isNaN(scale)) scale = 1;
+    if (isNaN(scale)) scale = 1;
 
     Array.from(docMap.entries())
         .filter(ele => ele[1].pair)
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormPannableContents.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormPannableContents.tsx
index 69cbae86f..707f6c198 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormPannableContents.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormPannableContents.tsx
@@ -4,26 +4,28 @@ import * as React from 'react';
 import { Doc } from '../../../../fields/Doc';
 import { ScriptField } from '../../../../fields/ScriptField';
 import { PresBox } from '../../nodes/trails/PresBox';
-import { CollectionFreeFormView } from './CollectionFreeFormView';
 import './CollectionFreeFormView.scss';
+import { ObservableReactComponent } from '../../ObservableReactComponent';
+
 export interface CollectionFreeFormPannableContentsProps {
     Document: Doc;
     viewDefDivClick?: ScriptField;
     children?: React.ReactNode | undefined;
     transition?: string;
     isAnnotationOverlay: boolean | undefined;
+    showPresPaths: () => boolean;
     transform: () => string;
     brushedView: () => { panX: number; panY: number; width: number; height: number } | undefined;
 }
 
 @observer
-export class CollectionFreeFormPannableContents extends React.Component<CollectionFreeFormPannableContentsProps> {
+export class CollectionFreeFormPannableContents extends ObservableReactComponent<CollectionFreeFormPannableContentsProps> {
     constructor(props: CollectionFreeFormPannableContentsProps) {
         super(props);
         makeObservable(this);
     }
     @computed get presPaths() {
-        return CollectionFreeFormView.ShowPresPaths ? PresBox.Instance.pathLines(this.props.Document) : null;
+        return this._props.showPresPaths() ? PresBox.Instance.pathLines(this._props.Document) : null;
     }
     // rectangle highlight used when following trail/link to a region of a collection that isn't a document
     showViewport = (viewport: { panX: number; panY: number; width: number; height: number } | undefined) =>
@@ -42,7 +44,7 @@ export class CollectionFreeFormPannableContents extends React.Component<Collecti
     render() {
         return (
             <div
-                className={'collectionfreeformview' + (this.props.viewDefDivClick ? '-viewDef' : '-none')}
+                className={'collectionfreeformview' + (this._props.viewDefDivClick ? '-viewDef' : '-none')}
                 onScroll={e => {
                     const target = e.target as any;
                     if (getComputedStyle(target)?.overflow === 'visible') {
@@ -50,13 +52,13 @@ export class CollectionFreeFormPannableContents extends React.Component<Collecti
                     }
                 }}
                 style={{
-                    transform: this.props.transform(),
-                    transition: this.props.transition,
-                    width: this.props.isAnnotationOverlay ? undefined : 0, // if not an overlay, then this will be the size of the collection, but panning and zooming will move it outside the visible border of the collection and make it selectable.  This problem shows up after zooming/panning on a background collection -- you can drag the collection by clicking on apparently empty space outside the collection
+                    transform: this._props.transform(),
+                    transition: this._props.transition,
+                    width: this._props.isAnnotationOverlay ? undefined : 0, // if not an overlay, then this will be the size of the collection, but panning and zooming will move it outside the visible border of the collection and make it selectable.  This problem shows up after zooming/panning on a background collection -- you can drag the collection by clicking on apparently empty space outside the collection
                 }}>
                 {this.props.children}
                 {this.presPaths}
-                {this.showViewport(this.props.brushedView())}
+                {this.showViewport(this._props.brushedView())}
             </div>
         );
     }
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.tsx
index fa8218bdd..35394016d 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.tsx
@@ -9,17 +9,17 @@ import { Id } from '../../../../fields/FieldSymbols';
 import { List } from '../../../../fields/List';
 import { listSpec } from '../../../../fields/Schema';
 import { Cast } from '../../../../fields/Types';
-import { CollectionViewProps } from '../CollectionView';
+import { CollectionViewProps } from '../CollectionSubView';
 import './CollectionFreeFormView.scss';
 
 @observer
 export class CollectionFreeFormRemoteCursors extends React.Component<CollectionViewProps> {
     @computed protected get cursors(): CursorField[] {
-        const doc = this.props.Document;
+        const { Document } = this.props;
 
         let cursors: FieldResult<List<CursorField>>;
         const id = Doc.UserDoc()[Id];
-        if (!id || !(cursors = Cast(doc.cursors, listSpec(CursorField)))) {
+        if (!id || !(cursors = Cast(Document.cursors, listSpec(CursorField)))) {
             return [];
         }
         const now = mobxUtils.now();
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 3fab00968..a70713429 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -1,14 +1,17 @@
+/* eslint-disable jsx-a11y/click-events-have-key-events */
+/* eslint-disable jsx-a11y/no-static-element-interactions */
 import { Bezier } from 'bezier-js';
 import { Colors } from 'browndash-components';
 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';
+import { ClientUtils, DashColor, lightOrDark, OmitKeys, returnFalse, returnZero, setupMoveUpEvents } from '../../../../ClientUtils';
 import { DateField } from '../../../../fields/DateField';
-import { Doc, DocListCast, Field, Opt } from '../../../../fields/Doc';
+import { Doc, DocListCast, Field, FieldType, Opt } from '../../../../fields/Doc';
 import { DocData, Height, Width } from '../../../../fields/DocSymbols';
 import { Id } from '../../../../fields/FieldSymbols';
-import { InkData, InkField, InkTool, PointData, Segment } from '../../../../fields/InkField';
+import { InkData, InkField, InkTool, Segment } from '../../../../fields/InkField';
 import { List } from '../../../../fields/List';
 import { RichTextField } from '../../../../fields/RichTextField';
 import { listSpec } from '../../../../fields/Schema';
@@ -16,13 +19,15 @@ import { ScriptField } from '../../../../fields/ScriptField';
 import { BoolCast, Cast, DocCast, NumCast, ScriptCast, StrCast } from '../../../../fields/Types';
 import { ImageField } from '../../../../fields/URLField';
 import { TraceMobx } from '../../../../fields/util';
+import { Gestures, PointData } from '../../../../pen-gestures/GestureTypes';
 import { GestureUtils } from '../../../../pen-gestures/GestureUtils';
-import { aggregateBounds, DashColor, emptyFunction, intersectRect, lightOrDark, OmitKeys, returnFalse, returnZero, setupMoveUpEvents, Utils } from '../../../../Utils';
+import { aggregateBounds, emptyFunction, intersectRect, Utils } from '../../../../Utils';
 import { CognitiveServices } from '../../../cognitive_services/CognitiveServices';
 import { Docs, DocUtils } from '../../../documents/Documents';
 import { CollectionViewType, DocumentType } from '../../../documents/DocumentTypes';
 import { DocumentManager } from '../../../util/DocumentManager';
-import { DragManager, dropActionType } from '../../../util/DragManager';
+import { DragManager } from '../../../util/DragManager';
+import { dropActionType } from '../../../util/DropActionTypes';
 import { ReplayMovements } from '../../../util/ReplayMovements';
 import { CompileScript } from '../../../util/Scripting';
 import { ScriptingGlobals } from '../../../util/ScriptingGlobals';
@@ -33,8 +38,8 @@ import { Transform } from '../../../util/Transform';
 import { undoable, undoBatch, UndoManager } from '../../../util/UndoManager';
 import { Timeline } from '../../animationtimeline/Timeline';
 import { ContextMenu } from '../../ContextMenu';
+import { PinProps } from '../../DocComponent';
 import { GestureOverlay } from '../../GestureOverlay';
-import { CtrlKey } from '../../GlobalKeyHandler';
 import { ActiveInkWidth, InkingStroke, SetActiveInkColor, SetActiveInkWidth } from '../../InkingStroke';
 import { LightboxView } from '../../LightboxView';
 import { CollectionFreeFormDocumentView } from '../../nodes/CollectionFreeFormDocumentView';
@@ -42,7 +47,7 @@ import { SchemaCSVPopUp } from '../../nodes/DataVizBox/SchemaCSVPopUp';
 import { DocumentView, OpenWhere } from '../../nodes/DocumentView';
 import { FieldViewProps, FocusViewOptions } from '../../nodes/FieldView';
 import { FormattedTextBox } from '../../nodes/formattedText/FormattedTextBox';
-import { PinProps, PresBox } from '../../nodes/trails/PresBox';
+import { PresBox } from '../../nodes/trails/PresBox';
 import { CreateImage } from '../../nodes/WebBoxRenderer';
 import { StyleProp } from '../../StyleProvider';
 import { CollectionSubView } from '../CollectionSubView';
@@ -77,7 +82,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     @observable _paintedId = 'id' + Utils.GenerateGuid().replace(/-/g, '');
     @computed get paintFunc() {
         const field = this.dataDoc[this.fieldKey];
-        const paintFunc = StrCast(Field.toJavascriptString(Cast(field, RichTextField, null)?.Text as Field)).trim();
+        const paintFunc = StrCast(Field.toJavascriptString(Cast(field, RichTextField, null)?.Text as FieldType)).trim();
         return !paintFunc
             ? ''
             : paintFunc.includes('dashDiv')
@@ -635,11 +640,11 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     onGesture = (e: Event, ge: GestureUtils.GestureEvent) => {
         switch (ge.gesture) {
             default:
-            case GestureUtils.Gestures.Line:
-            case GestureUtils.Gestures.Circle:
-            case GestureUtils.Gestures.Rectangle:
-            case GestureUtils.Gestures.Triangle:
-            case GestureUtils.Gestures.Stroke:
+            case Gestures.Line:
+            case Gestures.Circle:
+            case Gestures.Rectangle:
+            case Gestures.Triangle:
+            case Gestures.Stroke:
                 const points = ge.points;
                 const B = this.screenToFreeformContentsXf.transformBounds(ge.bounds.left, ge.bounds.top, ge.bounds.width, ge.bounds.height);
                 const inkWidth = ActiveInkWidth() * this.ScreenToLocalBoxXf().Scale;
@@ -660,7 +665,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                 this.addDocument(inkDoc);
                 e.stopPropagation();
                 break;
-            case GestureUtils.Gestures.Rectangle:
+            case Gestures.Rectangle:
                 const strokes = this.getActiveDocuments()
                     .filter(doc => doc.type === DocumentType.INK)
                     .map(i => {
@@ -672,7 +677,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
 
                 CognitiveServices.Inking.Appliers.InterpretStrokes(strokes).then(results => {});
                 break;
-            case GestureUtils.Gestures.Text:
+            case Gestures.Text:
                 if (ge.text) {
                     const B = this.screenToFreeformContentsXf.transformPoint(ge.points[0].X, ge.points[0].Y);
                     this.addDocument(Docs.Create.TextDocument(ge.text, { title: ge.text, x: B[0], y: B[1] }));
@@ -690,7 +695,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     @action
     onClick = (e: React.MouseEvent) => {
         if (this._lightboxDoc) this._lightboxDoc = undefined;
-        if (Utils.isClick(e.pageX, e.pageY, this._downX, this._downY, this._downTime)) {
+        if (ClientUtils.isClick(e.pageX, e.pageY, this._downX, this._downY, this._downTime)) {
             if (this.onBrowseClickHandler()) {
                 this.onBrowseClickHandler().script.run({ documentView: this.DocumentView?.(), clientX: e.clientX, clientY: e.clientY });
                 e.stopPropagation();
@@ -746,7 +751,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                     segments.forEach(segment =>
                         this.forceStrokeGesture(
                             e,
-                            GestureUtils.Gestures.Stroke,
+                            Gestures.Stroke,
                             segment.reduce((data, curve) => [...data, ...curve.points.map(p => intersect.inkView.ComponentView?.ptToScreen?.({ X: p.x, Y: p.y }) ?? { X: 0, Y: 0 })], [] as PointData[])
                         )
                     );
@@ -759,7 +764,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         });
         return false;
     };
-    forceStrokeGesture = (e: PointerEvent, gesture: GestureUtils.Gestures, points: InkData, text?: any) => {
+    forceStrokeGesture = (e: PointerEvent, gesture: Gestures, points: InkData, text?: any) => {
         this.onGesture(e, new GestureUtils.GestureEvent(gesture, points, GestureOverlay.getBounds(points), text));
     };
 
@@ -956,7 +961,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         switch (
             !e.ctrlKey && !e.shiftKey && !e.metaKey && !e.altKey ?//
                 Doc.UserDoc().freeformScrollMode : // no modifiers, do assigned mode
-                    e.ctrlKey && !CtrlKey? // otherwise, if ctrl key (pinch gesture) try to zoom else pan
+                    e.ctrlKey && !SnappingManager.CtrlKey? // otherwise, if ctrl key (pinch gesture) try to zoom else pan
                         freeformScrollMode.Zoom : freeformScrollMode.Pan // prettier-ignore
         ) {
             case freeformScrollMode.Pan:
@@ -1030,9 +1035,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                 (!this._props.getScrollHeight?.() ? fitYscroll : 0); // when not zoomed, scrolling is handled via a scrollbar, not panning
             let newPanY = Math.max(minPanY, Math.min(maxPanY, panY));
             if (false && NumCast(this.layoutDoc.layout_scrollTop) && NumCast(this.layoutDoc._freeform_scale, minScale) !== minScale) {
-                const relTop = NumCast(this.layoutDoc.layout_scrollTop) / maxScrollTop;
-                this.layoutDoc.layout_scrollTop = undefined;
-                newPanY = minPanY + relTop * (maxPanY - minPanY);
             } else if (fitYscroll > 2 && this.layoutDoc.layout_scrollTop === undefined && NumCast(this.layoutDoc._freeform_scale, minScale) === minScale) {
                 const maxPanY = minPanY + fitYscroll;
                 const relTop = (panY - minPanY) / (maxPanY - minPanY);
@@ -1289,8 +1291,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                 : NumCast(this.layoutDoc._rotation_jitter) * random(-1, 1, NumCast(x), NumCast(y)) );
         const childProps = { ...this._props, fieldKey: '', styleProvider: this.clusterStyleProvider };
         return {
-            x: Number.isNaN(NumCast(x)) ? 0 : NumCast(x),
-            y: Number.isNaN(NumCast(y)) ? 0 : NumCast(y),
+            x: isNaN(NumCast(x)) ? 0 : NumCast(x),
+            y: isNaN(NumCast(y)) ? 0 : NumCast(y),
             z: Cast(z, 'number'),
             autoDim,
             rotation,
@@ -1424,8 +1426,9 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         return anchor;
     };
 
-    @action closeInfo = () => (Doc.IsInfoUIDisabled = true);
-    infoUI = () => (Doc.IsInfoUIDisabled || this.Document.annotationOn || this._props.renderDepth ? null : <CollectionFreeFormInfoUI Document={this.Document} Freeform={this} close={this.closeInfo} />);
+    childDocsFunc = () => this.childDocs;
+    @action closeInfo = () => { Doc.IsInfoUIDisabled = true }; // prettier-ignore
+    infoUI = () => (Doc.IsInfoUIDisabled || this.Document.annotationOn || this._props.renderDepth ? null : <CollectionFreeFormInfoUI Document={this.Document} LayoutDoc={this.layoutDoc} childDocs={this.childDocsFunc} close={this.closeInfo} />);
 
     componentDidMount() {
         this._props.setContentViewBox?.(this);
@@ -1483,6 +1486,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                 if (!code.includes('dashDiv')) {
                     const script = CompileScript(code, { params: { docView: 'any' }, typecheck: false, editable: true });
                     if (script.compiled) script.run({ this: this.DocumentView?.() });
+                    // eslint-disable-next-line no-eval
                 } else code && !first && eval?.(code);
             },
             { fireImmediately: true }
@@ -1491,7 +1495,9 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         this._disposers.layoutElements = reaction(
             // layoutElements can't be a computed value because doLayoutComputation() is an action that has side effect of updating clusters
             () => this.doInternalLayoutComputation,
-            computation => (this._layoutElements = this.doLayoutComputation(computation.newPool, computation.computedElementData)),
+            computation => {
+                this._layoutElements = this.doLayoutComputation(computation.newPool, computation.computedElementData);
+            },
             { fireImmediately: true }
         );
     }
@@ -1512,7 +1518,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                 const canvas = oldDiv;
                 const img = document.createElement('img'); // create a Image Element
                 try {
-                    img.src = canvas.toDataURL(); //image source
+                    img.src = canvas.toDataURL(); // image source
                 } catch (e) {
                     console.log(e);
                 }
@@ -1567,14 +1573,12 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         const htmlString = new XMLSerializer().serializeToString(newDiv);
         const nativeWidth = width;
         const nativeHeight = height;
-        return CreateImage(Utils.prepend(''), document.styleSheets, htmlString, nativeWidth, (nativeWidth * panelHeight) / panelWidth, (scrollTop * panelHeight) / realNativeHeight)
-            .then(async (data_url: any) => {
-                const returnedFilename = await Utils.convertDataUri(data_url, filename, noSuffix, replaceRootFilename);
+        return CreateImage(ClientUtils.prepend(''), document.styleSheets, htmlString, nativeWidth, (nativeWidth * panelHeight) / panelWidth, (scrollTop * panelHeight) / realNativeHeight)
+            .then(async (dataUrl: any) => {
+                const returnedFilename = await ClientUtils.convertDataUri(dataUrl, filename, noSuffix, replaceRootFilename);
                 cb(returnedFilename as string, nativeWidth, nativeHeight);
             })
-            .catch(function (error: any) {
-                console.error('oops, something went wrong!', error);
-            });
+            .catch((error: any) => console.error('oops, something went wrong!', error));
     }
 
     componentWillUnmount() {
@@ -1583,14 +1587,15 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     }
 
     @action
-    onCursorMove = (e: React.PointerEvent) => {
+    onCursorMove = () => {
         //  super.setCursorPosition(this.getTransform().transformPoint(e.clientX, e.clientY));
     };
 
     @undoBatch
     promoteCollection = () => {
         const childDocs = this.childDocs.slice();
-        childDocs.forEach(doc => {
+        childDocs.forEach(docIn => {
+            const doc = docIn;
             const scr = this.screenToFreeformContentsXf.inverse().transformPoint(NumCast(doc.x), NumCast(doc.y));
             doc.x = scr?.[0];
             doc.y = scr?.[1];
@@ -1604,7 +1609,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         const width = Math.max(...docs.map(doc => NumCast(doc._width))) + 20;
         const height = Math.max(...docs.map(doc => NumCast(doc._height))) + 20;
         const dim = Math.ceil(Math.sqrt(docs.length));
-        docs.forEach((doc, i) => {
+        docs.forEach((docIn, i) => {
+            const doc = docIn;
             doc.x = NumCast(this.Document[this.panXFieldKey]) + (i % dim) * width - (width * dim) / 2;
             doc.y = NumCast(this.Document[this.panYFieldKey]) + Math.floor(i / dim) * height - (height * dim) / 2;
         });
@@ -1704,14 +1710,14 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
 
         const snappableDocs = activeDocs.filter(doc => doc.z === undefined && isDocInView(doc, selRect)); // first see if there are any foreground docs to snap to
         activeDocs
-            .filter(doc => doc.isGroup && SnappingManager.IsResizing !== doc && !DragManager.docsBeingDragged.includes(doc))
+            .filter(doc => doc.isGroup && SnappingManager.IsResizing !== doc[Id] && !DragManager.docsBeingDragged.includes(doc))
             .forEach(doc => DocumentManager.Instance.getDocumentView(doc)?.ComponentView?.dragStarting?.(snapToDraggedDoc, false, visited));
 
         const horizLines: number[] = [];
         const vertLines: number[] = [];
         const invXf = this.screenToFreeformContentsXf.inverse();
         snappableDocs
-            .filter(doc => !doc.isGroup && (snapToDraggedDoc || (SnappingManager.IsResizing !== doc && !DragManager.docsBeingDragged.includes(doc))))
+            .filter(doc => !doc.isGroup && (snapToDraggedDoc || (SnappingManager.IsResizing !== doc[Id] && !DragManager.docsBeingDragged.includes(doc))))
             .forEach(doc => {
                 const { left, top, width, height } = docDims(doc);
                 const topLeftInScreen = invXf.transformPoint(left, top);
@@ -1727,10 +1733,10 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
 
     incrementalRender = action(() => {
         if (!LightboxView.LightboxDoc || LightboxView.Contains(this.DocumentView?.())) {
-            const layout_unrendered = this.childDocs.filter(doc => !this._renderCutoffData.get(doc[Id]));
+            const layoutUnrendered = this.childDocs.filter(doc => !this._renderCutoffData.get(doc[Id]));
             const loadIncrement = this.Document.isTemplateDoc ? Number.MAX_VALUE : 5;
-            for (var i = 0; i < Math.min(layout_unrendered.length, loadIncrement); i++) {
-                this._renderCutoffData.set(layout_unrendered[i][Id] + '', true);
+            for (let i = 0; i < Math.min(layoutUnrendered.length, loadIncrement); i++) {
+                this._renderCutoffData.set(layoutUnrendered[i][Id] + '', true);
             }
         }
         this.childDocs.some(doc => !this._renderCutoffData.get(doc[Id])) && setTimeout(this.incrementalRender, 1);
@@ -1744,6 +1750,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         );
     }
 
+    showPresPaths = () => CollectionFreeFormView.ShowPresPaths;
     brushedView = () => this._brushedView;
     gridColor = () =>
         DashColor(lightOrDark(this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BackgroundColor)))
@@ -1776,6 +1783,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                 brushedView={this.brushedView}
                 isAnnotationOverlay={this.isAnnotationOverlay}
                 transform={this.PanZoomCenterXf}
+                showPresPaths={this.showPresPaths}
                 transition={this._panZoomTransition ? `transform ${this._panZoomTransition}ms` : Cast(this.layoutDoc._viewTransition, 'string', Cast(this.Document._viewTransition, 'string', null))}
                 viewDefDivClick={this._props.viewDefDivClick}>
                 {this.props.children ?? null} {/* most likely case of children is document content that's being annoated: eg., an image */}
@@ -1828,7 +1836,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         this._brushtimer1 = setTimeout(
             action(() => {
                 this._brushedView = { ...viewport, panX: viewport.panX - viewport.width / 2, panY: viewport.panY - viewport.height / 2 };
-                this._brushtimer = setTimeout(action(() => (this._brushedView = undefined)), holdTime); // prettier-ignore
+                this._brushtimer = setTimeout(action(() => { this._brushedView = undefined; }), holdTime); // prettier-ignore
             }),
             transTime + 1
         );
@@ -1912,6 +1920,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
 @observer
 class CollectionFreeFormOverlayView extends React.Component<{ elements: () => ViewDefResult[] }> {
     render() {
+        // eslint-disable-next-line react/destructuring-assignment
         return this.props.elements().filter(ele => ele.bounds?.z).map(ele => ele.ele); // prettier-ignore
     }
 }
@@ -1923,11 +1932,12 @@ export function CollectionBrowseClick(dv: DocumentView, clientX: number, clientY
         DocumentManager.Instance.showDocument(dv.Document, { zoomScale: 0.8, willZoomCentered: true }, (focused: boolean) => {
             if (!focused) {
                 const selfFfview = !dv.Document.isGroup && dv.ComponentView instanceof CollectionFreeFormView ? dv.ComponentView : undefined;
-                let containers = dv.containerViewPath?.() ?? [];
+                const containers = dv.containerViewPath?.() ?? [];
                 let parFfview = dv.CollectionFreeFormView;
-                for (var cont of containers) {
+                containers.forEach(cont => {
                     parFfview = parFfview ?? cont.CollectionFreeFormView;
-                }
+                });
+
                 while (parFfview?.Document.isGroup) parFfview = parFfview.DocumentView?.().CollectionFreeFormView;
                 const ffview = selfFfview && selfFfview.layoutDoc[selfFfview.scaleFieldKey] !== 0.5 ? selfFfview : parFfview; // if focus doc is a freeform that is not at it's default 0.5 scale, then zoom out on it.  Otherwise, zoom out on the parent ffview
                 ffview?.zoomSmoothlyAboutPt(ffview.screenToFreeformContentsXf.transformPoint(clientX, clientY), ffview?.isAnnotationOverlay ? 1 : 0.5, browseTransitionTime);
@@ -1936,17 +1946,21 @@ export function CollectionBrowseClick(dv: DocumentView, clientX: number, clientY
         });
 }
 ScriptingGlobals.add(CollectionBrowseClick);
+// eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function nextKeyFrame(readOnly: boolean) {
     !readOnly && (SelectionManager.Views[0].ComponentView as CollectionFreeFormView)?.changeKeyFrame();
 });
+// eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function prevKeyFrame(readOnly: boolean) {
     !readOnly && (SelectionManager.Views[0].ComponentView as CollectionFreeFormView)?.changeKeyFrame(true);
 });
+// eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function curKeyFrame(readOnly: boolean) {
     const selView = SelectionManager.Views;
     if (readOnly) return selView[0].ComponentView?.getKeyFrameEditing?.() ? Colors.MEDIUM_BLUE : 'transparent';
     runInAction(() => selView[0].ComponentView?.setKeyFrameEditing?.(!selView[0].ComponentView?.getKeyFrameEditing?.()));
 });
+// eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function pinWithView(pinContent: boolean) {
     SelectionManager.Views.forEach(view =>
         view._props.pinToPres(view.Document, {
@@ -1959,15 +1973,19 @@ ScriptingGlobals.add(function pinWithView(pinContent: boolean) {
         })
     );
 });
+// eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function bringToFront() {
     SelectionManager.Views.forEach(view => view.CollectionFreeFormView?.bringToFront(view.Document));
 });
-ScriptingGlobals.add(function sendToBack(doc: Doc) {
+// eslint-disable-next-line prefer-arrow-callback
+ScriptingGlobals.add(function sendToBack() {
     SelectionManager.Views.forEach(view => view.CollectionFreeFormView?.bringToFront(view.Document, true));
 });
-ScriptingGlobals.add(function datavizFromSchema(doc: Doc) {
+// eslint-disable-next-line prefer-arrow-callback
+ScriptingGlobals.add(function datavizFromSchema() {
     // creating a dataviz doc to represent the schema table
-    SelectionManager.Views.forEach(view => {
+    SelectionManager.Views.forEach(viewIn => {
+        const view = viewIn;
         if (!view.layoutDoc.schema_columnKeys) {
             view.layoutDoc.schema_columnKeys = new List<string>(['title', 'type', 'author', 'author_date']);
         }
@@ -1975,13 +1993,13 @@ ScriptingGlobals.add(function datavizFromSchema(doc: Doc) {
         if (!keys) return;
 
         const children = DocListCast(view.Document[Doc.LayoutFieldKey(view.Document)]);
-        let csvRows = [];
+        const csvRows = [];
         csvRows.push(keys.join(','));
         for (let i = 0; i < children.length; i++) {
-            let eachRow = [];
+            const eachRow = [];
             for (let j = 0; j < keys.length; j++) {
-                var cell = children[i][keys[j]]?.toString();
-                if (cell) cell = cell.toString().replace(/\,/g, '');
+                let cell = children[i][keys[j]]?.toString();
+                if (cell) cell = cell.toString().replace(/,/g, '');
                 eachRow.push(cell);
             }
             csvRows.push(eachRow);
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index 6eca91e9d..b03e435ce 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -1,7 +1,9 @@
+/* eslint-disable jsx-a11y/no-static-element-interactions */
 import { action, computed, makeObservable, observable } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
-import { Utils, intersectRect, lightOrDark, returnFalse } from '../../../../Utils';
+import { ClientUtils, lightOrDark, returnFalse } from '../../../../ClientUtils';
+import { intersectRect } from '../../../../Utils';
 import { Doc, Opt } from '../../../../fields/Doc';
 import { AclAdmin, AclAugment, AclEdit, DocData } from '../../../../fields/DocSymbols';
 import { Id } from '../../../../fields/FieldSymbols';
@@ -20,6 +22,7 @@ import { SnappingManager } from '../../../util/SnappingManager';
 import { Transform } from '../../../util/Transform';
 import { UndoManager, undoBatch } from '../../../util/UndoManager';
 import { ContextMenu } from '../../ContextMenu';
+import { MarqueeViewBounds } from '../../DocComponent';
 import { ObservableReactComponent } from '../../ObservableReactComponent';
 import { PreviewCursor } from '../../PreviewCursor';
 import { OpenWhere } from '../../nodes/DocumentView';
@@ -28,6 +31,7 @@ import { FormattedTextBox } from '../../nodes/formattedText/FormattedTextBox';
 import { SubCollectionViewProps } from '../CollectionSubView';
 import { MarqueeOptionsMenu } from './MarqueeOptionsMenu';
 import './MarqueeView.scss';
+
 interface MarqueeViewProps {
     getContainerTransform: () => Transform;
     getTransform: () => Transform;
@@ -44,13 +48,6 @@ interface MarqueeViewProps {
     slowLoadDocuments: (files: File[] | string, options: DocumentOptions, generatedDocuments: Doc[], text: string, completed: ((doc: Doc[]) => void) | undefined, addDocument: (doc: Doc | Doc[]) => boolean) => Promise<void>;
 }
 
-export interface MarqueeViewBounds {
-    left: number;
-    top: number;
-    width: number;
-    height: number;
-}
-
 @observer
 export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps & MarqueeViewProps> {
     public static CurViewBounds(pinDoc: Doc, panelWidth: number, panelHeight: number) {
@@ -102,7 +99,7 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
 
     @action
     onKeyDown = (e: KeyboardEvent) => {
-        //make textbox and add it to this collection
+        // make textbox and add it to this collection
         // tslint:disable-next-line:prefer-const
         const cm = ContextMenu.Instance;
         const [x, y] = this.Transform.transformPoint(this._downX, this._downY);
@@ -141,7 +138,7 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
                     }
                 }
                 let ypos = y;
-                ns.map(line => {
+                ns.forEach(line => {
                     const indent = line.search(/\S|$/);
                     const newBox = Docs.Create.TextDocument(line, { _width: 200, _height: 35, x: x + (indent / 3) * 10, y: ypos, title: line });
                     this._props.addDocument?.(newBox);
@@ -155,7 +152,7 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
                 pasteImageBitmap((data: any, error: any) => {
                     error && console.log(error);
                     data &&
-                        Utils.convertDataUri(data, this._props.Document[Id] + '-thumb-frozen').then(returnedfilename => {
+                        ClientUtils.convertDataUri(data, this._props.Document[Id] + '-thumb-frozen').then(returnedfilename => {
                             this._props.Document['thumb-frozen'] = new ImageField(returnedfilename);
                         });
                 })
@@ -169,7 +166,7 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
             TreeView._editTitleOnLoad = { id: slide[Id], parent: undefined };
             this._props.addDocument?.(slide);
             e.stopPropagation();
-        }*/ else if (e.key === 'p' && e.ctrlKey) {
+        } */ else if (e.key === 'p' && e.ctrlKey) {
             e.preventDefault();
             (async () => {
                 const text: string = await navigator.clipboard.readText();
@@ -184,7 +181,7 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
             e.stopPropagation();
         }
     };
-    //heuristically converts pasted text into a table.
+    // heuristically converts pasted text into a table.
     // assumes each entry is separated by a tab
     // skips all rows until it gets to a row with more than one entry
     // assumes that 1st row has header entry for each column
@@ -192,7 +189,7 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
     //         any row that has only one column is a section header-- this header is then added as a column to subsequent rows until the next header
     // assumes each cell is a string or a number
     pasteTable(ns: string[], x: number, y: number) {
-        let csvRows = [];
+        const csvRows = [];
         const headers = ns[0].split('\t');
         csvRows.push(headers.join(','));
         ns[0] = '';
@@ -200,7 +197,7 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
         let eachRow = [];
         for (let i = 1; i < eachCell.length; i++) {
             eachRow.push(eachCell[i].replace(/\,/g, ''));
-            if (i % headers.length == 0) {
+            if (i % headers.length === 0) {
                 csvRows.push(eachRow);
                 eachRow = [];
             }
@@ -233,7 +230,7 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
         this._lastY = e.pageY;
         this._lassoPts.push([e.clientX, e.clientY]);
         if (!e.cancelBubble) {
-            if (!Utils.isClick(this._lastX, this._lastY, this._downX, this._downY, Date.now())) {
+            if (!ClientUtils.isClick(this._lastX, this._lastY, this._downX, this._downY, Date.now())) {
                 if (!this._commandExecuted) {
                     this.showMarquee();
                 }
@@ -320,7 +317,7 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
     @action
     onClick = (e: React.MouseEvent): void => {
         if (this._props.pointerEvents?.() === 'none') return;
-        if (Utils.isClick(e.clientX, e.clientY, this._downX, this._downY, Date.now())) {
+        if (ClientUtils.isClick(e.clientX, e.clientY, this._downX, this._downY, Date.now())) {
             if (Doc.ActiveTool === InkTool.None) {
                 if (!this._props.trySelectCluster(e.shiftKey)) {
                     !SnappingManager.ExploreMode && this.setPreviewCursor(e.clientX, e.clientY, false, false, this._props.Document);
@@ -335,15 +332,21 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
     };
 
     @action
-    showMarquee = () => (this._visible = true);
+    showMarquee = () => {
+        this._visible = true;
+    };
     @action
-    hideMarquee = () => (this._visible = false);
+    hideMarquee = () => {
+        this._visible = false;
+    };
 
     @undoBatch
     delete = action((e?: React.PointerEvent<Element> | KeyboardEvent | undefined, hide?: boolean) => {
         const selected = this.marqueeSelect(false);
         SelectionManager.DeselectAll();
-        selected.forEach(doc => (hide ? (doc.hidden = true) : this._props.removeDocument?.(doc)));
+        selected.forEach(doc => {
+            hide ? (doc.hidden = true) : this._props.removeDocument?.(doc);
+        });
 
         this.cleanupInteractions(false);
         MarqueeOptionsMenu.Instance.fadeOut(true);
@@ -540,6 +543,7 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
     };
 
     touchesLine(r1: { left: number; top: number; width: number; height: number }) {
+        // eslint-disable-next-line no-restricted-syntax
         for (const lassoPt of this._lassoPts) {
             const topLeft = this.Transform.transformPoint(lassoPt[0], lassoPt[1]);
             if (r1.left < topLeft[0] && topLeft[0] < r1.left + r1.width && r1.top < topLeft[1] && topLeft[1] < r1.top + r1.height) {
@@ -560,6 +564,7 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
             let hasLeft = false;
             let hasBottom = false;
             let hasRight = false;
+            // eslint-disable-next-line no-restricted-syntax
             for (const lassoPt of this._lassoPts) {
                 const truePoint = this.Transform.transformPoint(lassoPt[0], lassoPt[1]);
                 hasLeft = hasLeft || (truePoint[0] > tl[0] && truePoint[0] < r1.left && truePoint[1] > r1.top && truePoint[1] < r1.top + r1.height);
@@ -662,6 +667,7 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
     };
     render() {
         return (
+            // eslint-disable-next-line jsx-a11y/click-events-have-key-events
             <div
                 className="marqueeView"
                 ref={r => {
@@ -673,7 +679,9 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
                     cursor: [InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool) || this._visible ? 'crosshair' : 'pointer',
                 }}
                 onDragOver={e => e.preventDefault()}
-                onScroll={e => (e.currentTarget.scrollTop = e.currentTarget.scrollLeft = 0)}
+                onScroll={e => {
+                    e.currentTarget.scrollTop = e.currentTarget.scrollLeft = 0;
+                }}
                 onClick={this.onClick}
                 onPointerDown={this.onPointerDown}>
                 {this._visible ? this.marqueeDiv : null}
diff --git a/src/client/views/collections/collectionGrid/CollectionGridView.tsx b/src/client/views/collections/collectionGrid/CollectionGridView.tsx
index f25872c2b..ab6788e6f 100644
--- a/src/client/views/collections/collectionGrid/CollectionGridView.tsx
+++ b/src/client/views/collections/collectionGrid/CollectionGridView.tsx
@@ -4,7 +4,8 @@ import * as React from 'react';
 import { Doc, Opt } from '../../../../fields/Doc';
 import { Id } from '../../../../fields/FieldSymbols';
 import { BoolCast, NumCast, ScriptCast, StrCast } from '../../../../fields/Types';
-import { emptyFunction, returnFalse, returnZero, setupMoveUpEvents } from '../../../../Utils';
+import { emptyFunction } from '../../../../Utils';
+import { returnFalse, returnZero, setupMoveUpEvents } from '../../../../ClientUtils';
 import { Docs } from '../../../documents/Documents';
 import { DragManager } from '../../../util/DragManager';
 import { Transform } from '../../../util/Transform';
diff --git a/src/client/views/collections/collectionLinear/CollectionLinearView.tsx b/src/client/views/collections/collectionLinear/CollectionLinearView.tsx
index 228af78aa..6635ab222 100644
--- a/src/client/views/collections/collectionLinear/CollectionLinearView.tsx
+++ b/src/client/views/collections/collectionLinear/CollectionLinearView.tsx
@@ -1,10 +1,13 @@
+/* eslint-disable jsx-a11y/no-static-element-interactions */
+/* eslint-disable jsx-a11y/click-events-have-key-events */
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { Tooltip } from '@mui/material';
 import { Toggle, ToggleType, Type } from 'browndash-components';
 import { IReactionDisposer, action, makeObservable, reaction } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
-import { Utils, emptyFunction, returnEmptyDoclist, returnTrue } from '../../../../Utils';
+import { ClientUtils, returnTrue } from '../../../../ClientUtils';
+import { emptyFunction } from '../../../../Utils';
 import { Doc, Opt } from '../../../../fields/Doc';
 import { Height, Width } from '../../../../fields/DocSymbols';
 import { Id } from '../../../../fields/FieldSymbols';
@@ -46,13 +49,15 @@ export class CollectionLinearView extends CollectionSubView() {
         this._dropDisposer?.();
         this._widthDisposer?.();
         this._selectedDisposer?.();
-        this.childLayoutPairs.map((pair, ind) => ScriptCast(DocCast(pair.layout.proto)?.onPointerUp)?.script.run({ this: pair.layout.proto }, console.log));
+        this.childLayoutPairs.map(pair => ScriptCast(DocCast(pair.layout.proto)?.onPointerUp)?.script.run({ this: pair.layout.proto }, console.log));
     }
 
     componentDidMount() {
         this._widthDisposer = reaction(
             () => 5 + NumCast(this.dataDoc.linearBtnWidth, this.dimension()) + (this.layoutDoc.linearView_IsOpen ? this.childDocs.filter(doc => !doc.hidden).reduce((tot, doc) => (NumCast(doc._width) || this.dimension()) + tot + 4, 0) : 0),
-            width => this.childDocs.length && (this.layoutDoc._width = width),
+            width => {
+                this.childDocs.length && (this.layoutDoc._width = width);
+            },
             { fireImmediately: true }
         );
     }
@@ -64,14 +69,14 @@ export class CollectionLinearView extends CollectionSubView() {
     dimension = () => NumCast(this.layoutDoc._height);
     getTransform = (ele: Opt<HTMLDivElement>) => {
         if (!ele) return Transform.Identity();
-        const { scale, translateX, translateY } = Utils.GetScreenTransform(ele);
+        const { translateX, translateY } = ClientUtils.GetScreenTransform(ele);
         return new Transform(-translateX, -translateY, 1);
     };
 
     @action
     exitLongLinks = () => {
         if (DocumentLinksButton.StartLink?.Document) {
-            action((e: React.PointerEvent<HTMLDivElement>) => Doc.UnBrushDoc(DocumentLinksButton.StartLink?.Document as Doc));
+            action(() => Doc.UnBrushDoc(DocumentLinksButton.StartLink?.Document as Doc));
         }
         DocumentLinksButton.StartLink = undefined;
         DocumentLinksButton.StartLinkView = undefined;
@@ -97,8 +102,8 @@ export class CollectionLinearView extends CollectionSubView() {
         e.preventDefault();
     };
 
-    getLinkUI = () => {
-        return !DocumentLinksButton.StartLink ? null : (
+    getLinkUI = () =>
+        !DocumentLinksButton.StartLink ? null : (
             <span className="bottomPopup-background" style={{ pointerEvents: 'all' }} onPointerDown={e => e.stopPropagation()}>
                 <span className="bottomPopup-text">
                     Creating link from:{' '}
@@ -108,7 +113,7 @@ export class CollectionLinearView extends CollectionSubView() {
                     </b>
                 </span>
 
-                <Tooltip title={<div className="dash-tooltip">{'Toggle description pop-up'} </div>} placement="top">
+                <Tooltip title={<div className="dash-tooltip">Toggle description pop-up </div>} placement="top">
                     <span className="bottomPopup-descriptions" onClick={this.changeDescriptionSetting}>
                         Labels: {LinkDescriptionPopup.Instance.showDescriptions ? LinkDescriptionPopup.Instance.showDescriptions : 'ON'}
                     </span>
@@ -121,9 +126,8 @@ export class CollectionLinearView extends CollectionSubView() {
                 </Tooltip>
             </span>
         );
-    };
-    getCurrentlyPlayingUI = () => {
-        return !CollectionStackedTimeline.CurrentlyPlaying?.length ? null : (
+    getCurrentlyPlayingUI = () =>
+        !CollectionStackedTimeline.CurrentlyPlaying?.length ? null : (
             <span className="bottomPopup-background">
                 <span className="bottomPopup-text">
                     Currently playing:
@@ -139,7 +143,6 @@ export class CollectionLinearView extends CollectionSubView() {
                 </span>
             </span>
         );
-    };
 
     getDisplayDoc = (doc: Doc, preview: boolean = false) => {
         // hack to avoid overhead of making UndoStack,etc into DocumentView style Boxes. If the UndoStack is ever intended to become part of the persisten state of the dashboard, then this would have to change.
@@ -149,6 +152,7 @@ export class CollectionLinearView extends CollectionSubView() {
             case '<CurrentlyPlayingUI>': return this.getCurrentlyPlayingUI();
             case '<UndoStack>':          return <UndoStack key={doc[Id]}/>;
             case '<Branching>':          return Doc.UserDoc().isBranchingMode  ? <BranchingTrailManager key={doc[Id]} /> : null;
+            default: 
         }
 
         const nested = doc._type_collection === CollectionViewType.Linear;
@@ -161,7 +165,9 @@ export class CollectionLinearView extends CollectionSubView() {
             <div
                 className={preview ? 'preview' : `collectionLinearView-docBtn`}
                 key={doc[Id]}
-                ref={r => (dref = r || undefined)}
+                ref={r => {
+                    dref = r || undefined;
+                }}
                 style={{
                     pointerEvents: 'all',
                     width: NumCast(doc._width),
@@ -194,7 +200,7 @@ export class CollectionLinearView extends CollectionSubView() {
                     childFilters={this._props.childFilters}
                     childFiltersByRanges={this._props.childFiltersByRanges}
                     searchFilterDocs={this._props.searchFilterDocs}
-                    hideResizeHandles={true}
+                    hideResizeHandles
                 />
             </div>
         );
@@ -220,30 +226,26 @@ export class CollectionLinearView extends CollectionSubView() {
                     ScriptCast(this.Document.onClick)?.script.run({ this: this.Document }, console.log);
                 }}
                 tooltip={isExpanded ? 'Close' : 'Open'}
-                fillWidth={true}
-                align={'center'}
+                fillWidth
+                align="center"
             />
         );
 
         return (
             <div className={`collectionLinearView-outer ${this.layoutDoc.linearView_SubMenu}`} style={{ backgroundColor: this.layoutDoc.linearView_IsOpen ? undefined : 'transparent' }}>
                 <div className="collectionLinearView" ref={this.createDashEventsTarget} onContextMenu={this.myContextMenu} style={{ minHeight: this.dimension(), pointerEvents: 'all' }}>
-                    {
-                        <>
-                            {!this.layoutDoc.linearView_Expandable ? null : menuOpener}
-                            {!this.layoutDoc.linearView_IsOpen ? null : (
-                                <div
-                                    className="collectionLinearView-content"
-                                    style={{
-                                        height: this.dimension(),
-                                        flexDirection: flexDir as any,
-                                        gap: flexGap,
-                                    }}>
-                                    {this.childLayoutPairs.map(pair => this.getDisplayDoc(pair.layout))}
-                                </div>
-                            )}
-                        </>
-                    }
+                    {!this.layoutDoc.linearView_Expandable ? null : menuOpener}
+                    {!this.layoutDoc.linearView_IsOpen ? null : (
+                        <div
+                            className="collectionLinearView-content"
+                            style={{
+                                height: this.dimension(),
+                                flexDirection: flexDir as any,
+                                gap: flexGap,
+                            }}>
+                            {this.childLayoutPairs.map(pair => this.getDisplayDoc(pair.layout))}
+                        </div>
+                    )}
                 </div>
             </div>
         );
diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx
index 6a956f2ac..46bf56dc8 100644
--- a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx
+++ b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx
@@ -3,16 +3,19 @@ import { Popup, PopupTrigger, Type } from 'browndash-components';
 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 { returnEmptyDoclist, returnEmptyString, returnFalse, returnIgnore, returnNever, returnTrue, setupMoveUpEvents, smoothScroll } from '../../../../ClientUtils';
+import { emptyFunction } from '../../../../Utils';
+import { Doc, DocListCast, Field, FieldType, 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 { ColumnType } from '../../../../fields/SchemaHeaderField';
 import { BoolCast, Cast, DocCast, NumCast, StrCast } from '../../../../fields/Types';
 import { DocUtils, Docs, DocumentOptions, FInfo } from '../../../documents/Documents';
 import { DocumentManager } from '../../../util/DocumentManager';
-import { DragManager, dropActionType } from '../../../util/DragManager';
+import { DragManager } from '../../../util/DragManager';
+import { dropActionType } from '../../../util/DropActionTypes';
 import { SelectionManager } from '../../../util/SelectionManager';
 import { SettingsManager } from '../../../util/SettingsManager';
 import { undoBatch, undoable } from '../../../util/UndoManager';
@@ -30,17 +33,6 @@ import { SchemaColumnHeader } from './SchemaColumnHeader';
 import { SchemaRowBox } from './SchemaRowBox';
 const { SCHEMA_NEW_NODE_HEIGHT } = require('../../global/globalCssVariables.module.scss'); // prettier-ignore
 
-export enum ColumnType {
-    Number,
-    String,
-    Boolean,
-    Date,
-    Image,
-    RTF,
-    Enumeration,
-    Any,
-}
-
 export const FInfotoColType: { [key: string]: ColumnType } = {
     string: ColumnType.String,
     number: ColumnType.Number,
@@ -823,8 +815,8 @@ export class CollectionSchemaView extends CollectionSubView() {
         const docs = !field
             ? this.childDocs
             : [...this.childDocs].sort((docA, docB) => {
-                  const aStr = Field.toString(docA[field] as Field);
-                  const bStr = Field.toString(docB[field] as Field);
+                  const aStr = Field.toString(docA[field] as FieldType);
+                  const bStr = Field.toString(docB[field] as FieldType);
                   var out = 0;
                   if (aStr < bStr) out = -1;
                   if (aStr > bStr) out = 1;
diff --git a/src/client/views/collections/collectionSchema/SchemaColumnHeader.tsx b/src/client/views/collections/collectionSchema/SchemaColumnHeader.tsx
index 5f8b412be..2823e1936 100644
--- a/src/client/views/collections/collectionSchema/SchemaColumnHeader.tsx
+++ b/src/client/views/collections/collectionSchema/SchemaColumnHeader.tsx
@@ -2,7 +2,8 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { action, observable } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
-import { emptyFunction, setupMoveUpEvents } from '../../../../Utils';
+import { setupMoveUpEvents } from '../../../../ClientUtils';
+import { emptyFunction } from '../../../../Utils';
 import { Colors } from '../../global/globalEnums';
 import './CollectionSchemaView.scss';
 
diff --git a/src/client/views/collections/collectionSchema/SchemaRowBox.tsx b/src/client/views/collections/collectionSchema/SchemaRowBox.tsx
index 39fea2d2e..27a4493cb 100644
--- a/src/client/views/collections/collectionSchema/SchemaRowBox.tsx
+++ b/src/client/views/collections/collectionSchema/SchemaRowBox.tsx
@@ -5,7 +5,8 @@ import { computedFn } from 'mobx-utils';
 import * as React from 'react';
 import { CgClose, CgLock, CgLockUnlock } from 'react-icons/cg';
 import { FaExternalLinkAlt } from 'react-icons/fa';
-import { emptyFunction, returnFalse, setupMoveUpEvents } from '../../../../Utils';
+import { emptyFunction } from '../../../../Utils';
+import { returnFalse, setupMoveUpEvents } from '../../../../ClientUtils';
 import { Doc } from '../../../../fields/Doc';
 import { BoolCast } from '../../../../fields/Types';
 import { DragManager } from '../../../util/DragManager';
diff --git a/src/client/views/collections/collectionSchema/SchemaTableCell.tsx b/src/client/views/collections/collectionSchema/SchemaTableCell.tsx
index bf36b2668..08eb35586 100644
--- a/src/client/views/collections/collectionSchema/SchemaTableCell.tsx
+++ b/src/client/views/collections/collectionSchema/SchemaTableCell.tsx
@@ -7,15 +7,17 @@ import * as React from 'react';
 import DatePicker from 'react-datepicker';
 import 'react-datepicker/dist/react-datepicker.css';
 import Select from 'react-select';
-import { Utils, emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnZero } from '../../../../Utils';
+import { ClientUtils, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnZero } from '../../../../ClientUtils';
+import { emptyFunction } from '../../../../Utils';
 import { DateField } from '../../../../fields/DateField';
 import { Doc, DocListCast, Field } from '../../../../fields/Doc';
 import { RichTextField } from '../../../../fields/RichTextField';
+import { ColumnType } from '../../../../fields/SchemaHeaderField';
 import { BoolCast, Cast, DateCast, DocCast, FieldValue, StrCast } from '../../../../fields/Types';
 import { ImageField } from '../../../../fields/URLField';
 import { FInfo, FInfoFieldType } from '../../../documents/Documents';
 import { DocFocusOrOpen } from '../../../util/DocumentManager';
-import { dropActionType } from '../../../util/DragManager';
+import { dropActionType } from '../../../util/DropActionTypes';
 import { SettingsManager } from '../../../util/SettingsManager';
 import { SnappingManager } from '../../../util/SnappingManager';
 import { Transform } from '../../../util/Transform';
@@ -28,7 +30,7 @@ import { OpenWhere, returnEmptyDocViewList } from '../../nodes/DocumentView';
 import { FieldViewProps } from '../../nodes/FieldView';
 import { KeyValueBox } from '../../nodes/KeyValueBox';
 import { FormattedTextBox } from '../../nodes/formattedText/FormattedTextBox';
-import { ColumnType, FInfotoColType } from './CollectionSchemaView';
+import { FInfotoColType } from './CollectionSchemaView';
 import './CollectionSchemaView.scss';
 
 export interface SchemaTableCellProps {
@@ -204,7 +206,7 @@ export class SchemaImageCell extends ObservableReactComponent<SchemaTableCellPro
 
     choosePath(url: URL) {
         if (url.protocol === 'data') return url.href; // if the url ises the data protocol, just return the href
-        if (url.href.indexOf(window.location.origin) === -1) return Utils.CorsProxy(url.href); // otherwise, put it through the cors proxy erver
+        if (url.href.indexOf(window.location.origin) === -1) return ClientUtils.CorsProxy(url.href); // otherwise, put it through the cors proxy erver
         if (!/\.(png|jpg|jpeg|gif|webp)$/.test(url.href.toLowerCase())) return url.href; //Why is this here — good question
 
         const ext = extname(url.href);
@@ -220,7 +222,7 @@ export class SchemaImageCell extends ObservableReactComponent<SchemaTableCellPro
             .map(url => this.choosePath(url)); // access the primary layout data of the alternate documents
         const paths = field ? [this.choosePath(field.url), ...altpaths] : altpaths;
         // If there is a path, follow it; otherwise, follow a link to a default image icon
-        const url = paths.length ? paths : [Utils.CorsProxy('http://www.cs.brown.edu/~bcz/noImage.png')];
+        const url = paths.length ? paths : [ClientUtils.CorsProxy('http://www.cs.brown.edu/~bcz/noImage.png')];
         return url[0];
     }
 
diff --git a/src/client/views/global/globalScripts.ts b/src/client/views/global/globalScripts.ts
index 2a5732708..50cf26cdb 100644
--- a/src/client/views/global/globalScripts.ts
+++ b/src/client/views/global/globalScripts.ts
@@ -2,10 +2,11 @@ import { Colors } from 'browndash-components';
 import { action, runInAction } from 'mobx';
 import { aggregateBounds } from '../../../Utils';
 import { Doc, Opt } from '../../../fields/Doc';
+import { DocData } from '../../../fields/DocSymbols';
 import { InkTool } from '../../../fields/InkField';
 import { BoolCast, Cast, NumCast, StrCast } from '../../../fields/Types';
 import { WebField } from '../../../fields/URLField';
-import { GestureUtils } from '../../../pen-gestures/GestureUtils';
+import { Gestures } from '../../../pen-gestures/GestureTypes';
 import { DocumentType } from '../../documents/DocumentTypes';
 import { LinkManager } from '../../util/LinkManager';
 import { ScriptingGlobals } from '../../util/ScriptingGlobals';
@@ -14,26 +15,28 @@ import { UndoManager, undoable } from '../../util/UndoManager';
 import { GestureOverlay } from '../GestureOverlay';
 import { ActiveFillColor, ActiveInkColor, ActiveInkHideTextLabels, ActiveInkWidth, ActiveIsInkMask, InkingStroke, SetActiveFillColor, SetActiveInkColor, SetActiveInkHideTextLabels, SetActiveInkWidth, SetActiveIsInkMask } from '../InkingStroke';
 import { CollectionFreeFormView } from '../collections/collectionFreeForm';
-// import { InkTranscription } from '../InkTranscription';
-import { DocData } from '../../../fields/DocSymbols';
 import { CollectionFreeFormDocumentView } from '../nodes/CollectionFreeFormDocumentView';
 import { DocumentView } from '../nodes/DocumentView';
+import { ImageBox } from '../nodes/ImageBox';
 import { VideoBox } from '../nodes/VideoBox';
 import { WebBox } from '../nodes/WebBox';
 import { RichTextMenu } from '../nodes/formattedText/RichTextMenu';
-import { ImageBox } from '../nodes/ImageBox';
+// import { InkTranscription } from '../InkTranscription';
 
+// eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function IsNoneSelected() {
     return SelectionManager.Views.length <= 0;
 }, 'are no document selected');
 
 // toggle: Set overlay status of selected document
+// eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function setView(view: string) {
     const selected = SelectionManager.Docs.lastElement();
     selected ? (selected._type_collection = view) : console.log('[FontIconBox.tsx] changeView failed');
 });
 
 // toggle: Set overlay status of selected document
+// eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function setBackgroundColor(color?: string, checkResult?: boolean) {
     const selectedViews = SelectionManager.Views;
     if (Doc.ActiveTool !== InkTool.None) {
@@ -70,15 +73,18 @@ ScriptingGlobals.add(function setBackgroundColor(color?: string, checkResult?: b
             return selected.lastElement()?._backgroundColor ?? 'transparent';
         }
         SetActiveFillColor(color ?? 'transparent');
-        selected.forEach(doc => (doc[DocData].backgroundColor = color));
+        selected.forEach(doc => { doc[DocData].backgroundColor = color; }); // prettier-ignore
     }
+    return '';
 });
 
 // toggle: Set overlay status of selected document
+// eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function setDefaultTemplate(checkResult?: boolean) {
     return DocumentView.setDefaultTemplate(checkResult);
 });
 // toggle: Set overlay status of selected document
+// eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function setHeaderColor(color?: string, checkResult?: boolean) {
     if (checkResult) {
         return SelectionManager.Views.length ? StrCast(SelectionManager.Docs.lastElement().layout_headingColor) : Doc.SharingDoc().headingColor;
@@ -93,9 +99,11 @@ ScriptingGlobals.add(function setHeaderColor(color?: string, checkResult?: boole
         Doc.GetProto(Doc.SharingDoc()).headingColor = color === 'transparent' ? undefined : color;
         Doc.UserDoc().layout_showTitle = color === 'transparent' ? undefined : StrCast(Doc.UserDoc().layout_showTitle, 'title');
     }
+    return undefined;
 });
 
 // toggle: Set overlay status of selected document
+// eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function toggleOverlay(checkResult?: boolean) {
     const selected = SelectionManager.Views.length ? SelectionManager.Views[0] : undefined;
     if (checkResult) {
@@ -103,19 +111,21 @@ ScriptingGlobals.add(function toggleOverlay(checkResult?: boolean) {
         return false;
     }
     selected ? selected.CollectionFreeFormDocumentView?.float() : console.log('[FontIconBox.tsx] toggleOverlay failed');
+    return undefined;
 });
 
+// eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function showFreeform(attr: 'center' | 'grid' | 'snaplines' | 'clusters' | 'viewAll' | 'fitOnce', checkResult?: boolean, persist?: boolean) {
     const selected = SelectionManager.Docs.lastElement();
     // prettier-ignore
     const map: Map<'center' |'grid' | 'snaplines' | 'clusters' | 'viewAll' | 'fitOnce', { waitForRender?: boolean, checkResult: (doc:Doc) => any; setDoc: (doc:Doc, dv:DocumentView) => void;}> = new Map([
         ['grid', {
             checkResult: (doc:Doc) => BoolCast(doc?._freeform_backgroundGrid, false),
-            setDoc: (doc:Doc,dv:DocumentView) => doc._freeform_backgroundGrid = !doc._freeform_backgroundGrid,
+            setDoc: (doc:Doc) => { doc._freeform_backgroundGrid = !doc._freeform_backgroundGrid; },
         }],
         ['snaplines', {
             checkResult: (doc:Doc) => BoolCast(doc?._freeform_snapLines, false),
-            setDoc: (doc:Doc, dv:DocumentView) => doc._freeform_snapLines = !doc._freeform_snapLines,
+            setDoc: (doc:Doc) => { doc._freeform_snapLines = !doc._freeform_snapLines; },
         }],
         ['viewAll', {
             checkResult: (doc:Doc) => BoolCast(doc?._freeform_fitContentsToBox, false),
@@ -127,12 +137,12 @@ ScriptingGlobals.add(function showFreeform(attr: 'center' | 'grid' | 'snaplines'
         }],
         ['center', {
             checkResult: (doc:Doc) => BoolCast(doc?._stacking_alignCenter, false),
-            setDoc: (doc:Doc,dv:DocumentView) => doc._stacking_alignCenter = !doc._stacking_alignCenter,
+            setDoc: (doc:Doc) => { doc._stacking_alignCenter = !doc._stacking_alignCenter; },
         }],
         ['clusters', {
             waitForRender: true, // flags that undo batch should terminate after a re-render giving the script the chance to fire
             checkResult: (doc:Doc) => BoolCast(doc?._freeform_useClusters, false),
-            setDoc: (doc:Doc,dv:DocumentView) => doc._freeform_useClusters = !doc._freeform_useClusters,
+            setDoc: (doc:Doc) => { doc._freeform_useClusters = !doc._freeform_useClusters; },
         }],
    ]);
 
@@ -142,11 +152,12 @@ ScriptingGlobals.add(function showFreeform(attr: 'center' | 'grid' | 'snaplines'
     const batch = map.get(attr)?.waitForRender ? UndoManager.StartBatch('set freeform attribute') : { end: () => {} };
     SelectionManager.Views.map(dv => map.get(attr)?.setDoc(dv.layoutDoc, dv));
     setTimeout(() => batch.end(), 100);
+    return undefined;
 });
 
+// eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function setFontAttr(attr: 'font' | 'fontColor' | 'highlight' | 'fontSize' | 'alignment', value: any, checkResult?: boolean) {
     const editorView = RichTextMenu.Instance?.TextView?.EditorView;
-    const selected = SelectionManager.Docs.lastElement();
     // prettier-ignore
     const map: Map<'font'|'fontColor'|'highlight'|'fontSize'|'alignment', { checkResult: () => any; setDoc: () => void;}> = new Map([
         ['font', {
@@ -163,14 +174,15 @@ ScriptingGlobals.add(function setFontAttr(attr: 'font' | 'fontColor' | 'highligh
         }],
         ['alignment', {
             checkResult: () => RichTextMenu.Instance?.textAlign,
-            setDoc: () => value && editorView?.state ? RichTextMenu.Instance?.align(editorView, editorView.dispatch, value):(Doc.UserDoc().textAlign = value),
+            setDoc: () => { value && editorView?.state ? RichTextMenu.Instance?.align(editorView, editorView.dispatch, value):(Doc.UserDoc().textAlign = value); },
         }],
         ['fontSize', {
             checkResult: () => RichTextMenu.Instance?.fontSize.replace('px', ''),
             setDoc: () => {
-                if (typeof value === 'number') value = value.toString();
-                if (value && Number(value).toString() === value) value += 'px';
-                RichTextMenu.Instance?.setFontSize(value);
+                let fsize = value;
+                if (typeof fsize === 'number') fsize = fsize.toString();
+                if (fsize && Number(fsize).toString() === fsize) fsize += 'px';
+                RichTextMenu.Instance?.setFontSize(fsize);
             },
         }],
     ]);
@@ -179,63 +191,55 @@ ScriptingGlobals.add(function setFontAttr(attr: 'font' | 'fontColor' | 'highligh
         return map.get(attr)?.checkResult();
     }
     map.get(attr)?.setDoc?.();
+    return undefined;
 });
 
 type attrname = 'noAutoLink' | 'dictation' | 'bold' | 'italics' | 'elide' | 'underline' | 'left' | 'center' | 'right' | 'vcent' | 'bullet' | 'decimal';
 type attrfuncs = [attrname, { checkResult: () => boolean; toggle?: () => any }];
 
+// eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function toggleCharStyle(charStyle: attrname, checkResult?: boolean) {
     const textView = RichTextMenu.Instance?.TextView;
     const editorView = textView?.EditorView;
     // prettier-ignore
     const alignments:attrfuncs[] = (['left','right','center','vcent'] as ("left"|"center"|"right"|"vcent")[]).map((where) =>
-        [ where, {      checkResult: () =>(editorView ? (where === 'vcent' ?  RichTextMenu.Instance?.textVcenter ?? false:
-                                                                              (RichTextMenu.Instance?.textAlign === where)): 
-                                                         where === 'vcent' ? BoolCast(Doc.UserDoc()._layout_centered):
-                                                                              (Doc.UserDoc().textAlign ===where) ? true:false),
-                        toggle: () => (editorView?.state ? (where === 'vcent' ? RichTextMenu.Instance?.vcenterToggle(editorView, editorView.dispatch):
-                                                                              RichTextMenu.Instance?.align(editorView, editorView.dispatch, where)):
+        [ where, {      checkResult: () => editorView ? (where === 'vcent' ?  RichTextMenu.Instance?.textVcenter ?? false:
+                                                                             (RichTextMenu.Instance?.textAlign === where)): 
+                                                         where === 'vcent' ?  BoolCast(Doc.UserDoc()._layout_centered):
+                                                                            (Doc.UserDoc().textAlign === where),
+                        toggle: () => { editorView?.state ? (where === 'vcent' ? RichTextMenu.Instance?.vcenterToggle(editorView, editorView.dispatch):
+                                                                                 RichTextMenu.Instance?.align(editorView, editorView.dispatch, where)):
                                                             where === 'vcent' ?  Doc.UserDoc()._layout_centered = !Doc.UserDoc()._layout_centered:
-                                                                              (Doc.UserDoc().textAlign = where))}]); // prettier-ignore
+                                                                                (Doc.UserDoc().textAlign = where); }
+        }]); // prettier-ignore
     // prettier-ignore
     const listings:attrfuncs[] = (['bullet','decimal'] as attrname[]).map(list =>
         [ list, {       checkResult: () => (editorView ? RichTextMenu.Instance?.listStyle === list:false),
                         toggle: () => editorView?.state && RichTextMenu.Instance?.changeListType(list) }]);
     // prettier-ignore
     const attrs:attrfuncs[] = [
-        ['dictation', { checkResult: () => textView?._recordingDictation ? true:false,
-                        toggle: () => textView && runInAction(() => (textView._recordingDictation = !textView._recordingDictation)) }],
+        ['dictation', { checkResult: () => !!textView?._recordingDictation,
+                        toggle: () => textView && runInAction(() => { textView._recordingDictation = !textView._recordingDictation;} ) }],
         ['elide',      { checkResult: () => false,
                         toggle: () => editorView ? RichTextMenu.Instance?.elideSelection(): 0}],
         ['noAutoLink',{ checkResult: () => ((editorView && RichTextMenu.Instance?.noAutoLink) ?? false),
                         toggle: () => editorView && RichTextMenu.Instance?.toggleNoAutoLinkAnchor()}],
-        ['bold',      { checkResult: () => (editorView ? RichTextMenu.Instance?.bold??false : (Doc.UserDoc().fontWeight === 'bold') ? true:false),
-                        toggle: editorView ? RichTextMenu.Instance?.toggleBold : () => (Doc.UserDoc().fontWeight = Doc.UserDoc().fontWeight === 'bold' ? undefined : 'bold')}],
-        ['italics',   { checkResult: () => (editorView ? RichTextMenu.Instance?.italics ?? false : (Doc.UserDoc().fontStyle === 'italics') ? true:false),
-                        toggle: editorView ? RichTextMenu.Instance?.toggleItalics : () => (Doc.UserDoc().fontStyle = Doc.UserDoc().fontStyle === 'italics' ? undefined : 'italics')}],
-        ['underline', { checkResult: () => (editorView ? RichTextMenu.Instance?.underline ?? false: (Doc.UserDoc().textDecoration === 'underline') ? true:false),
-                        toggle: editorView ? RichTextMenu.Instance?.toggleUnderline : () => (Doc.UserDoc().textDecoration = Doc.UserDoc().textDecoration === 'underline' ? undefined : 'underline') }]]
+        ['bold',      { checkResult: () => (editorView ? RichTextMenu.Instance?.bold??false : (Doc.UserDoc().fontWeight === 'bold')),
+                        toggle: editorView ? RichTextMenu.Instance?.toggleBold : () => { Doc.UserDoc().fontWeight = Doc.UserDoc().fontWeight === 'bold' ? undefined : 'bold'; }}],
+        ['italics',   { checkResult: () => (editorView ? RichTextMenu.Instance?.italics ?? false : (Doc.UserDoc().fontStyle === 'italics')),
+                        toggle: editorView ? RichTextMenu.Instance?.toggleItalics : () => { Doc.UserDoc().fontStyle = Doc.UserDoc().fontStyle === 'italics' ? undefined : 'italics'; }}],
+        ['underline', { checkResult: () => (editorView ? RichTextMenu.Instance?.underline ?? false: (Doc.UserDoc().textDecoration === 'underline')),
+                        toggle: editorView ? RichTextMenu.Instance?.toggleUnderline : () => { Doc.UserDoc().textDecoration = Doc.UserDoc().textDecoration === 'underline' ? undefined : 'underline'; } }]]
 
     const map = new Map(attrs.concat(alignments).concat(listings));
     if (checkResult) {
         return map.get(charStyle)?.checkResult();
     }
     undoable(() => map.get(charStyle)?.toggle?.(), 'toggle ' + charStyle)();
+    return undefined;
 });
 
-export function checkInksToGroup() {
-    if (Doc.ActiveTool === InkTool.Write) {
-        CollectionFreeFormView.collectionsWithUnprocessedInk.forEach(ffView => {
-            // TODO: nda - will probably want to go through ffView unprocessed docs and then see if any of the inksToGroup docs are in it and only use those
-            // find all inkDocs in ffView.unprocessedDocs that are within 200 pixels of each other
-            const inksToGroup = ffView.unprocessedDocs.filter(inkDoc => {
-                // console.log(inkDoc.x, inkDoc.y);
-            });
-        });
-    }
-}
-
-export function createInkGroup(inksToGroup?: Doc[], isSubGroup?: boolean) {
+export function createInkGroup(/* inksToGroup?: Doc[], isSubGroup?: boolean */) {
     // TODO nda - if document being added to is a inkGrouping then we can just add to that group
     if (Doc.ActiveTool === InkTool.Write) {
         CollectionFreeFormView.collectionsWithUnprocessedInk.forEach(ffView => {
@@ -301,26 +305,24 @@ export function createInkGroup(inksToGroup?: Doc[], isSubGroup?: boolean) {
     CollectionFreeFormView.collectionsWithUnprocessedInk.clear();
 }
 
-function setActiveTool(tool: InkTool | GestureUtils.Gestures, keepPrim: boolean, checkResult?: boolean) {
+function setActiveTool(tool: InkTool | Gestures, keepPrim: boolean, checkResult?: boolean) {
     // InkTranscription.Instance?.createInkGroup();
     if (checkResult) {
         return (Doc.ActiveTool === tool && !GestureOverlay.Instance?.InkShape) || GestureOverlay.Instance?.InkShape === tool
-            ? GestureOverlay.Instance?.KeepPrimitiveMode || ![GestureUtils.Gestures.Circle, GestureUtils.Gestures.Line, GestureUtils.Gestures.Rectangle].includes(tool as GestureUtils.Gestures)
-                ? true
-                : true
+            ? GestureOverlay.Instance?.KeepPrimitiveMode || ![Gestures.Circle, Gestures.Line, Gestures.Rectangle].includes(tool as Gestures)
             : false;
     }
     runInAction(() => {
         if (GestureOverlay.Instance) {
             GestureOverlay.Instance.KeepPrimitiveMode = keepPrim;
         }
-        if (Object.values(GestureUtils.Gestures).includes(tool as any)) {
+        if (Object.values(Gestures).includes(tool as any)) {
             if (GestureOverlay.Instance.InkShape === tool && !keepPrim) {
                 Doc.ActiveTool = InkTool.None;
                 GestureOverlay.Instance.InkShape = undefined;
             } else {
                 Doc.ActiveTool = InkTool.Pen;
-                GestureOverlay.Instance.InkShape = tool as GestureUtils.Gestures;
+                GestureOverlay.Instance.InkShape = tool as Gestures;
             }
         } else if (tool) {
             // pen or eraser
@@ -334,38 +336,40 @@ function setActiveTool(tool: InkTool | GestureUtils.Gestures, keepPrim: boolean,
             Doc.ActiveTool = InkTool.None;
         }
     });
+    return undefined;
 }
 
 ScriptingGlobals.add(setActiveTool, 'sets the active ink tool mode');
 
 // toggle: Set overlay status of selected document
+// eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function setInkProperty(option: 'inkMask' | 'labels' | 'fillColor' | 'strokeWidth' | 'strokeColor', value: any, checkResult?: boolean) {
     const selected = SelectionManager.Docs.lastElement() ?? Doc.UserDoc();
     // prettier-ignore
     const map: Map<'inkMask' | 'labels' | 'fillColor' | 'strokeWidth' | 'strokeColor', { checkResult: () => any; setInk: (doc: Doc) => void; setMode: () => void }> = new Map([
         ['inkMask', {
             checkResult: () => ((selected?._layout_isSvg ? BoolCast(selected[DocData].stroke_isInkMask) : ActiveIsInkMask())),
-            setInk: (doc: Doc) => (doc[DocData].stroke_isInkMask = !doc.stroke_isInkMask),
+            setInk: (doc: Doc) => { doc[DocData].stroke_isInkMask = !doc.stroke_isInkMask; },
             setMode: () => selected?.type !== DocumentType.INK && SetActiveIsInkMask(!ActiveIsInkMask()),
         }],
         ['labels', {
             checkResult: () => ((selected?._stroke_showLabel ? BoolCast(selected[DocData].stroke_showLabel) : ActiveInkHideTextLabels())),
-            setInk: (doc: Doc) => (doc[DocData].stroke_showLabel = !doc.stroke_showLabel),
+            setInk: (doc: Doc) => { doc[DocData].stroke_showLabel = !doc.stroke_showLabel; },
             setMode: () => selected?.type !== DocumentType.INK && SetActiveInkHideTextLabels(!ActiveInkHideTextLabels()),
         }],
         ['fillColor', {
             checkResult: () => (selected?._layout_isSvg ? StrCast(selected[DocData].fillColor) : ActiveFillColor() ?? "transparent"),
-            setInk: (doc: Doc) => (doc[DocData].fillColor = StrCast(value)),
+            setInk: (doc: Doc) => { doc[DocData].fillColor = StrCast(value); },
             setMode: () => SetActiveFillColor(StrCast(value)),
         }],
         [ 'strokeWidth', {
             checkResult: () => (selected?._layout_isSvg ? NumCast(selected[DocData].stroke_width) : ActiveInkWidth()),
-            setInk: (doc: Doc) => (doc[DocData].stroke_width = NumCast(value)),
+            setInk: (doc: Doc) => { doc[DocData].stroke_width = NumCast(value); },
             setMode: () => { SetActiveInkWidth(value.toString()); selected?.type === DocumentType.INK && setActiveTool( GestureOverlay.Instance.InkShape ?? InkTool.Pen, true, false);},
         }],
         ['strokeColor', {
             checkResult: () => (selected?._layout_isSvg? StrCast(selected[DocData].color) : ActiveInkColor()),
-            setInk: (doc: Doc) => (doc[DocData].color = String(value)),
+            setInk: (doc: Doc) => { doc[DocData].color = String(value); },
             setMode: () => { SetActiveInkColor(StrCast(value)); selected?.type === DocumentType.INK &&  setActiveTool(GestureOverlay.Instance.InkShape ?? InkTool.Pen, true, false);},
         }],
     ]);
@@ -375,11 +379,13 @@ ScriptingGlobals.add(function setInkProperty(option: 'inkMask' | 'labels' | 'fil
     }
     map.get(option)?.setMode();
     SelectionManager.Docs.filter(doc => doc._layout_isSvg).map(doc => map.get(option)?.setInk(doc));
+    return undefined;
 });
 
 /** WEB
  *      webSetURL
- **/
+ * */
+// eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function webSetURL(url: string, checkResult?: boolean) {
     const selected = SelectionManager.Views.lastElement();
     if (selected?.Document.type === DocumentType.WEB) {
@@ -387,30 +393,36 @@ ScriptingGlobals.add(function webSetURL(url: string, checkResult?: boolean) {
             return StrCast(selected.Document.data, Cast(selected.Document.data, WebField, null)?.url?.href);
         }
         selected.ComponentView?.setData?.(url);
-        //selected.Document.data = new WebField(url);
     }
+    return '';
 });
+// eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function webForward(checkResult?: boolean) {
     const selected = SelectionManager.Views.lastElement()?.ComponentView as WebBox;
     if (checkResult) {
         return selected?.forward(checkResult) ? undefined : 'lightGray';
     }
     selected?.forward();
+    return undefined;
 });
+// eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function webBack() {
     const selected = SelectionManager.Views.lastElement()?.ComponentView as WebBox;
     selected?.back();
 });
 
+// eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function videoSnapshot() {
     const selected = SelectionManager.Views.lastElement()?.ComponentView as VideoBox;
     selected?.Snapshot();
 });
 
+// eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function imageSetPixelSize() {
     const selected = SelectionManager.Views.lastElement()?.ComponentView as ImageBox;
     selected?.setNativeSize();
 });
+// eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function imageRotate90() {
     const selected = SelectionManager.Views.lastElement()?.ComponentView as ImageBox;
     selected?.rotate();
@@ -418,21 +430,25 @@ ScriptingGlobals.add(function imageRotate90() {
 
 /** Schema
  *      toggleSchemaPreview
- **/
+ * */
+// eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function toggleSchemaPreview(checkResult?: boolean) {
     const selected = SelectionManager.Docs.lastElement();
     if (checkResult && selected) {
         const result: boolean = NumCast(selected.schema_previewWidth) > 0;
         if (result) return Colors.MEDIUM_BLUE;
-        else return 'transparent';
-    } else if (selected) {
+        return 'transparent';
+    }
+    if (selected) {
         if (NumCast(selected.schema_previewWidth) > 0) {
             selected.schema_previewWidth = 0;
         } else {
             selected.schema_previewWidth = 200;
         }
     }
+    return '';
 });
+// eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function toggleSingleLineSchema(checkResult?: boolean) {
     const selected = SelectionManager.Docs.lastElement();
     if (checkResult && selected) {
@@ -441,17 +457,20 @@ ScriptingGlobals.add(function toggleSingleLineSchema(checkResult?: boolean) {
     if (selected) {
         selected._schema_singleLine = !selected._schema_singleLine;
     }
+    return undefined;
 });
 
 /** STACK
  *      groupBy
  */
+// eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function setGroupBy(key: string, checkResult?: boolean) {
-    SelectionManager.Docs.map(doc => (doc._text_fontFamily = key));
+    SelectionManager.Docs.forEach(doc => { doc._text_fontFamily = key; }); // prettier-ignore
     const editorView = RichTextMenu.Instance?.TextView?.EditorView;
     if (checkResult) {
         return StrCast((editorView ? RichTextMenu.Instance : Doc.UserDoc())?.fontFamily);
     }
     if (editorView) RichTextMenu.Instance?.setFontFamily(key);
     else Doc.UserDoc().fontFamily = key;
+    return undefined;
 });
diff --git a/src/client/views/linking/LinkMenuItem.tsx b/src/client/views/linking/LinkMenuItem.tsx
index a2c9d10b6..38ef08ef9 100644
--- a/src/client/views/linking/LinkMenuItem.tsx
+++ b/src/client/views/linking/LinkMenuItem.tsx
@@ -4,13 +4,15 @@ import { Tooltip } from '@mui/material';
 import { action, computed, makeObservable, observable } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
-import { emptyFunction, returnFalse, setupMoveUpEvents } from '../../../Utils';
+import { returnFalse, setupMoveUpEvents } from '../../../ClientUtils';
+import { emptyFunction } from '../../../Utils';
 import { Doc } from '../../../fields/Doc';
 import { Cast, DocCast, StrCast } from '../../../fields/Types';
 import { WebField } from '../../../fields/URLField';
 import { DocumentType } from '../../documents/DocumentTypes';
 import { DocumentManager } from '../../util/DocumentManager';
-import { DragManager, dropActionType } from '../../util/DragManager';
+import { DragManager } from '../../util/DragManager';
+import { dropActionType } from '../../util/DropActionTypes';
 import { LinkFollower } from '../../util/LinkFollower';
 import { LinkManager } from '../../util/LinkManager';
 import { SelectionManager } from '../../util/SelectionManager';
diff --git a/src/client/views/linking/LinkPopup.tsx b/src/client/views/linking/LinkPopup.tsx
index c9e3c203d..f2681bf2e 100644
--- a/src/client/views/linking/LinkPopup.tsx
+++ b/src/client/views/linking/LinkPopup.tsx
@@ -2,7 +2,8 @@ import { action, observable } from 'mobx';
 import { observer } from 'mobx-react';
 import { EditorView } from 'prosemirror-view';
 import * as React from 'react';
-import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue } from '../../../Utils';
+import { emptyFunction } from '../../../Utils';
+import { returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue } from '../../../ClientUtils';
 import { Doc } from '../../../fields/Doc';
 import { Transform } from '../../util/Transform';
 import { undoBatch } from '../../util/UndoManager';
diff --git a/src/client/views/newlightbox/NewLightboxView.tsx b/src/client/views/newlightbox/NewLightboxView.tsx
index 12b9870ca..dcbc9fc50 100644
--- a/src/client/views/newlightbox/NewLightboxView.tsx
+++ b/src/client/views/newlightbox/NewLightboxView.tsx
@@ -2,7 +2,8 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { action, computed, observable } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
-import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnTrue } from '../../../Utils';
+import { returnEmptyDoclist, returnEmptyFilter, returnTrue } from '../../../ClientUtils';
+import { emptyFunction } from '../../../Utils';
 import { Doc, DocListCast, Opt } from '../../../fields/Doc';
 import { InkTool } from '../../../fields/InkField';
 import { Cast, NumCast, StrCast } from '../../../fields/Types';
diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx
index c685ec66f..c37466914 100644
--- a/src/client/views/nodes/AudioBox.tsx
+++ b/src/client/views/nodes/AudioBox.tsx
@@ -1,15 +1,18 @@
+/* eslint-disable jsx-a11y/no-static-element-interactions */
+/* eslint-disable jsx-a11y/click-events-have-key-events */
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { Tooltip } from '@mui/material';
 import { action, computed, IReactionDisposer, makeObservable, observable, runInAction } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
+import { returnFalse, setupMoveUpEvents } from '../../../ClientUtils';
 import { DateField } from '../../../fields/DateField';
 import { Doc } from '../../../fields/Doc';
 import { DocData } from '../../../fields/DocSymbols';
 import { ComputedField } from '../../../fields/ScriptField';
 import { Cast, DateCast, NumCast } from '../../../fields/Types';
 import { AudioField, nullAudio } from '../../../fields/URLField';
-import { emptyFunction, formatTime, returnFalse, setupMoveUpEvents } from '../../../Utils';
+import { formatTime } from '../../../Utils';
 import { Docs, DocUtils } from '../../documents/Documents';
 import { Networking } from '../../Network';
 import { DragManager } from '../../util/DragManager';
@@ -18,11 +21,11 @@ import { undoBatch } from '../../util/UndoManager';
 import { CollectionStackedTimeline, TrimScope } from '../collections/CollectionStackedTimeline';
 import { ContextMenu } from '../ContextMenu';
 import { ContextMenuProps } from '../ContextMenuItem';
-import { ViewBoxAnnotatableComponent } from '../DocComponent';
+import { PinProps, ViewBoxAnnotatableComponent } from '../DocComponent';
 import './AudioBox.scss';
-import { FocusViewOptions, FieldView, FieldViewProps } from './FieldView';
-import { PinProps, PresBox } from './trails';
 import { OpenWhere } from './DocumentView';
+import { FieldView, FieldViewProps } from './FieldView';
+import { PresBox } from './trails';
 
 /**
  * AudioBox
@@ -42,7 +45,7 @@ declare class MediaRecorder {
     constructor(e: any); // whatever MediaRecorder has
 }
 
-export enum media_state {
+export enum mediaState {
     PendingRecording = 'pendingRecording',
     Recording = 'recording',
     Paused = 'paused',
@@ -97,16 +100,16 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
         return LinkManager.Links(this.dataDoc);
     }
     @computed get mediaState() {
-        return this.dataDoc.mediaState as media_state;
+        return this.dataDoc.mediaState as mediaState;
+    }
+    set mediaState(value) {
+        this.dataDoc.mediaState = value;
     }
     @computed get path() {
         // returns the path of the audio file
         const path = Cast(this.Document[this.fieldKey], AudioField, null)?.url.href || '';
         return path === nullAudio ? '' : path;
     }
-    set mediaState(value) {
-        this.dataDoc.mediaState = value;
-    }
 
     @computed get timeline() {
         return this._stackedTimeline;
@@ -117,17 +120,17 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
         this._dropDisposer?.();
         Object.values(this._disposers).forEach(disposer => disposer?.());
 
-        this.mediaState === media_state.Recording && this.stopRecording();
+        this.mediaState === mediaState.Recording && this.stopRecording();
     }
 
     @action
     componentDidMount() {
         this._props.setContentViewBox?.(this);
         if (this.path) {
-            this.mediaState = media_state.Paused;
+            this.mediaState = mediaState.Paused;
             this.setPlayheadTime(NumCast(this.layoutDoc.clipStart));
         } else {
-            this.mediaState = undefined as any as media_state;
+            this.mediaState = undefined as any as mediaState;
         }
     }
 
@@ -149,7 +152,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                   this.Document,
                   this.dataDoc,
                   this.annotationKey,
-                  this._ele?.currentTime || Cast(this.Document._layout_currentTimecode, 'number', null) || (this.mediaState === media_state.Recording ? (Date.now() - (this.recordingStart || 0)) / 1000 : undefined),
+                  this._ele?.currentTime || Cast(this.Document._layout_currentTimecode, 'number', null) || (this.mediaState === mediaState.Recording ? (Date.now() - (this.recordingStart || 0)) / 1000 : undefined),
                   undefined,
                   undefined,
                   addAsAnnotation
@@ -163,10 +166,10 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
     // updates timecode and shows it in timeline, follows links at time
     @action
     timecodeChanged = () => {
-        if (this.mediaState !== media_state.Recording && this._ele) {
+        if (this.mediaState !== mediaState.Recording && this._ele) {
             this.links
                 .map(l => this.getLinkData(l))
-                .forEach(({ la1, la2, linkTime }) => {
+                .forEach(({ la1, linkTime }) => {
                     if (linkTime > NumCast(this.layoutDoc._layout_currentTimecode) && linkTime < this._ele!.currentTime) {
                         Doc.linkFollowHighlight(la1);
                     }
@@ -180,7 +183,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
     @action
     playFrom = (seekTimeInSeconds: number, endTime?: number, fullPlay: boolean = false) => {
         clearTimeout(this._play); // abort any previous clip ending
-        if (Number.isNaN(this._ele?.duration)) {
+        if (isNaN(this._ele?.duration ?? Number.NaN)) {
             // audio element isn't loaded yet... wait 1/2 second and try again
             setTimeout(() => this.playFrom(seekTimeInSeconds, endTime), 500);
         } else if (this.timeline && this._ele && AudioBox.Enabled) {
@@ -191,7 +194,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
             if (seekTimeInSeconds >= 0 && this.timeline.trimStart <= end && seekTimeInSeconds <= this.timeline.trimEnd) {
                 this._ele.currentTime = start;
                 this._ele.play();
-                this.mediaState = media_state.Playing;
+                this.mediaState = mediaState.Playing;
                 this.addCurrentlyPlaying();
                 this._play = setTimeout(
                     () => {
@@ -233,7 +236,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
 
     // update the recording time
     updateRecordTime = () => {
-        if (this.mediaState === media_state.Recording) {
+        if (this.mediaState === mediaState.Recording) {
             setTimeout(this.updateRecordTime, 30);
             if (!this._paused) {
                 this.layoutDoc._layout_currentTimecode = (new Date().getTime() - this._recordStart - this._pausedTime) / 1000;
@@ -254,7 +257,9 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
             }
         };
         this._recordStart = new Date().getTime();
-        runInAction(() => (this.mediaState = media_state.Recording));
+        runInAction(() => {
+            this.mediaState = mediaState.Recording;
+        });
         setTimeout(this.updateRecordTime);
         this._recorder.start();
         setTimeout(this.stopRecording, 60 * 60 * 1000); // stop after an hour
@@ -269,7 +274,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
             const now = new Date().getTime();
             this._paused && (this._pausedTime += now - this._pauseStart);
             this.dataDoc[this.fieldKey + '_duration'] = (now - this._recordStart - this._pausedTime) / 1000;
-            this.mediaState = media_state.Paused;
+            this.mediaState = mediaState.Paused;
             this._stream?.getAudioTracks()[0].stop();
             const ind = DocUtils.ActiveRecordings.indexOf(this);
             ind !== -1 && DocUtils.ActiveRecordings.splice(ind, 1);
@@ -277,26 +282,26 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
     };
 
     // context menu
-    specificContextMenu = (e: React.MouseEvent): void => {
+    specificContextMenu = (): void => {
         const funcs: ContextMenuProps[] = [];
         funcs.push({
             description: (this.layoutDoc.hideAnchors ? "Don't hide" : 'Hide') + ' anchors',
-            event: e => (this.layoutDoc.hideAnchors = !this.layoutDoc.hideAnchors),
+            event: () => { this.layoutDoc.hideAnchors = !this.layoutDoc.hideAnchors; }, // prettier-ignore
             icon: 'expand-arrows-alt',
         });
         funcs.push({
             description: (this.layoutDoc.dontAutoFollowLinks ? '' : "Don't") + ' follow links when encountered',
-            event: e => (this.layoutDoc.dontAutoFollowLinks = !this.layoutDoc.dontAutoFollowLinks),
+            event: () => { this.layoutDoc.dontAutoFollowLinks = !this.layoutDoc.dontAutoFollowLinks}, // prettier-ignore
             icon: 'expand-arrows-alt',
         });
         funcs.push({
             description: (this.layoutDoc.dontAutoPlayFollowedLinks ? '' : "Don't") + ' play when link is selected',
-            event: e => (this.layoutDoc.dontAutoPlayFollowedLinks = !this.layoutDoc.dontAutoPlayFollowedLinks),
+            event: () => { this.layoutDoc.dontAutoPlayFollowedLinks = !this.layoutDoc.dontAutoPlayFollowedLinks; }, // prettier-ignore
             icon: 'expand-arrows-alt',
         });
         funcs.push({
             description: (this.layoutDoc.autoPlayAnchors ? "Don't auto" : 'Auto') + ' play anchors onClick',
-            event: e => (this.layoutDoc.autoPlayAnchors = !this.layoutDoc.autoPlayAnchors),
+            event: () => { this.layoutDoc.autoPlayAnchors = !this.layoutDoc.autoPlayAnchors; }, // prettier-ignore
             icon: 'expand-arrows-alt',
         });
         ContextMenu.Instance?.addItem({
@@ -342,9 +347,9 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
         }
     };
 
-    IsPlaying = () => this.mediaState === media_state.Playing;
+    IsPlaying = () => this.mediaState === mediaState.Playing;
     TogglePause = () => {
-        if (this.mediaState === media_state.Paused) this.Play();
+        if (this.mediaState === mediaState.Paused) this.Play();
         else this.pause();
     };
     // pause playback without removing from the playback list to allow user to play it again.
@@ -352,7 +357,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
     pause = () => {
         if (this._ele) {
             this._ele.pause();
-            this.mediaState = media_state.Paused;
+            this.mediaState = mediaState.Paused;
 
             // if paused in the middle of playback, prevents restart on next play
             if (!this._finished) clearTimeout(this._play);
@@ -434,7 +439,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
     };
 
     // plays link
-    playLink = (link: Doc, options: FocusViewOptions) => {
+    playLink = (link: Doc /* , options: FocusViewOptions */) => {
         if (link.annotationOn === this.Document) {
             if (!this.layoutDoc.dontAutoPlayFollowedLinks) {
                 this.playFrom(this.timeline?.anchorStart(link) || 0, this.timeline?.anchorEnd(link));
@@ -460,13 +465,17 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
     };
 
     @action
-    timelineWhenChildContentsActiveChanged = (isActive: boolean) => this._props.whenChildContentsActiveChanged((this._isAnyChildContentActive = isActive));
+    timelineWhenChildContentsActiveChanged = (isActive: boolean) => {
+        this._props.whenChildContentsActiveChanged((this._isAnyChildContentActive = isActive));
+    };
 
     timelineScreenToLocal = () => this.ScreenToLocalBoxXf().translate(0, -AudioBox.topControlsHeight);
 
-    setPlayheadTime = (time: number) => (this._ele!.currentTime /*= this.layoutDoc._layout_currentTimecode*/ = time);
+    setPlayheadTime = (time: number) => {
+        this._ele!.currentTime /* = this.layoutDoc._layout_currentTimecode */ = time;
+    };
 
-    playing = () => this.mediaState === media_state.Playing;
+    playing = () => this.mediaState === mediaState.Playing;
 
     isActiveChild = () => this._isAnyChildContentActive;
 
@@ -497,7 +506,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                 e,
                 returnFalse,
                 returnFalse,
-                action(e => {
+                action(() => {
                     if (this.timeline?.IsTrimming !== TrimScope.None) {
                         this.timeline?.CancelTrimming();
                     } else {
@@ -566,7 +575,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
             this._dropDisposer = DragManager.MakeDropTarget(
                 r,
                 (e, de) => {
-                    const [xp, yp] = this.ScreenToLocalBoxXf().transformPoint(de.x, de.y);
+                    const [xp] = this.ScreenToLocalBoxXf().transformPoint(de.x, de.y);
                     de.complete.docDragData && this.timeline?.internalDocDrop(e, de, de.complete.docDragData, xp);
                 },
                 this.layoutDoc
@@ -581,7 +590,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                 <div className="audiobox-dictation" onPointerDown={this.onFile}>
                     <FontAwesomeIcon size="2x" icon="file-alt" />
                 </div>
-                {[media_state.Recording, media_state.Playing].includes(this.mediaState) ? (
+                {[mediaState.Recording, mediaState.Playing].includes(this.mediaState) ? (
                     <div className="recording-controls" onClick={e => e.stopPropagation()}>
                         <div className="record-button" onPointerDown={this.Record}>
                             <FontAwesomeIcon size="2x" icon="stop" />
@@ -614,31 +623,29 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                     <div className="controls-left">
                         <div
                             className="audiobox-button"
-                            title={this.mediaState === media_state.Paused ? 'play' : 'pause'}
+                            title={this.mediaState === mediaState.Paused ? 'play' : 'pause'}
                             onPointerDown={
-                                this.mediaState === media_state.Paused
+                                this.mediaState === mediaState.Paused
                                     ? this.Play
                                     : e => {
                                           e.stopPropagation();
                                           this.Pause();
                                       }
                             }>
-                            <FontAwesomeIcon icon={this.mediaState === media_state.Paused ? 'play' : 'pause'} size={'1x'} />
+                            <FontAwesomeIcon icon={this.mediaState === mediaState.Paused ? 'play' : 'pause'} size="1x" />
                         </div>
 
                         {!this.miniPlayer && (
                             <>
                                 <Tooltip title={<>trim audio</>}>
                                     <div className="audiobox-button" onPointerDown={this.onClipPointerDown}>
-                                        <FontAwesomeIcon icon={this.timeline?.IsTrimming !== TrimScope.None ? 'check' : 'cut'} size={'1x'} />
+                                        <FontAwesomeIcon icon={this.timeline?.IsTrimming !== TrimScope.None ? 'check' : 'cut'} size="1x" />
                                     </div>
                                 </Tooltip>
-                                {this.timeline?.IsTrimming == TrimScope.None && !NumCast(this.layoutDoc.clipStart) && NumCast(this.layoutDoc.clipEnd) === this.rawDuration ? (
-                                    <></>
-                                ) : (
-                                    <Tooltip title={<>{this.timeline?.IsTrimming !== TrimScope.None ? 'Cancel trimming' : 'Edit original timeline'}</>}>
+                                {this.timeline?.IsTrimming === TrimScope.None && !NumCast(this.layoutDoc.clipStart) && NumCast(this.layoutDoc.clipEnd) === this.rawDuration ? null : (
+                                    <Tooltip title={this.timeline?.IsTrimming !== TrimScope.None ? 'Cancel trimming' : 'Edit original timeline'}>
                                         <div className="audiobox-button" onPointerDown={this.onResetPointerDown}>
-                                            <FontAwesomeIcon icon={this.timeline?.IsTrimming !== TrimScope.None ? 'cancel' : 'arrows-left-right'} size={'1x'} />
+                                            <FontAwesomeIcon icon={this.timeline?.IsTrimming !== TrimScope.None ? 'cancel' : 'arrows-left-right'} size="1x" />
                                         </div>
                                     </Tooltip>
                                 )}
@@ -705,9 +712,11 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
     @computed get renderTimeline() {
         return (
             <CollectionStackedTimeline
-                ref={action((r: CollectionStackedTimeline | null) => (this._stackedTimeline = r))}
+                ref={action((r: CollectionStackedTimeline | null) => {
+                    this._stackedTimeline = r;
+                })}
+                // eslint-disable-next-line react/jsx-props-no-spreading
                 {...this._props}
-                CollectionFreeFormDocumentView={undefined}
                 dataFieldKey={this.fieldKey}
                 fieldKey={this.annotationKey}
                 dictationKey={this.fieldKey + '_dictation'}
@@ -738,10 +747,13 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
     // returns the html audio element
     @computed get audio() {
         return (
+            // eslint-disable-next-line jsx-a11y/media-has-caption
             <audio
                 ref={this.setRef}
                 className={`audiobox-control${this._props.isContentActive() ? '-interactive' : ''}`}
-                onLoadedData={action(e => this._ele?.duration && this._ele?.duration !== Infinity && (this.dataDoc[this.fieldKey + '_duration'] = this._ele.duration))}>
+                onLoadedData={action(() => {
+                    this._ele?.duration && this._ele?.duration !== Infinity && (this.dataDoc[this.fieldKey + '_duration'] = this._ele.duration);
+                })}>
                 <source src={this.path} type="audio/mpeg" />
                 Not supported.
             </audio>
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
index 0d0a7c623..958d63267 100644
--- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
@@ -1,8 +1,10 @@
-import { action, makeObservable, observable, trace } from 'mobx';
+import { action, makeObservable, observable } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
-import { OmitKeys, numberRange } from '../../../Utils';
+import { OmitKeys } from '../../../ClientUtils';
+import { numberRange } from '../../../Utils';
 import { Doc, DocListCast, Opt } from '../../../fields/Doc';
+import { TransitionTimer } from '../../../fields/DocSymbols';
 import { List } from '../../../fields/List';
 import { listSpec } from '../../../fields/Schema';
 import { ComputedField } from '../../../fields/ScriptField';
@@ -17,7 +19,6 @@ 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
@@ -239,6 +240,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
         const isGroup = this.dataDoc.isGroup && (!backColor || backColor === 'transparent');
         return isGroup ? (this._props.isDocumentActive?.() ? 'group' : this._props.isGroupActive?.() ? 'child' : 'inactive') : this._props.isGroupActive?.() ? 'child' : undefined;
     };
+    localRotation = () => this._props.rotation;
     render() {
         TraceMobx();
 
@@ -259,6 +261,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
                     <DocumentView
                         {...OmitKeys(this._props,this.WrapperKeys.map(val => val.lower)).omit} // prettier-ignore
                         DataTransition={this.DataTransition}
+                        LocalRotation={this.localRotation}
                         CollectionFreeFormDocumentView={this.returnThis}
                         styleProvider={this.styleProvider}
                         ScreenToLocalTransform={this.screenToLocalTransform}
diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx
index 9ffdc350d..99f9c03bf 100644
--- a/src/client/views/nodes/ComparisonBox.tsx
+++ b/src/client/views/nodes/ComparisonBox.tsx
@@ -1,22 +1,24 @@
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { action, computed, makeObservable, observable } from 'mobx';
+import { action, computed, makeObservable, observable, trace } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
-import { emptyFunction, returnFalse, returnNone, returnZero, setupMoveUpEvents } from '../../../Utils';
+import { returnFalse, returnNone, returnZero, setupMoveUpEvents } from '../../../ClientUtils';
+import { emptyFunction } from '../../../Utils';
 import { Doc, Opt } from '../../../fields/Doc';
 import { RichTextField } from '../../../fields/RichTextField';
 import { DocCast, NumCast, RTFCast, StrCast } from '../../../fields/Types';
 import { DocUtils, Docs } from '../../documents/Documents';
-import { DragManager, dropActionType } from '../../util/DragManager';
+import { DragManager } from '../../util/DragManager';
+import { dropActionType } from '../../util/DropActionTypes';
 import { undoBatch } from '../../util/UndoManager';
-import { ViewBoxAnnotatableComponent, ViewBoxInterface } from '../DocComponent';
+import { PinProps, ViewBoxAnnotatableComponent, ViewBoxInterface } from '../DocComponent';
 import { StyleProp } from '../StyleProvider';
 import './ComparisonBox.scss';
 import { DocumentView } from './DocumentView';
 import { FieldView, FieldViewProps } from './FieldView';
 import { KeyValueBox } from './KeyValueBox';
 import { FormattedTextBox } from './formattedText/FormattedTextBox';
-import { PinProps, PresBox } from './trails';
+import { PresBox } from './trails';
 
 @observer
 export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implements ViewBoxInterface {
@@ -164,7 +166,8 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
      * @returns a JSX layout string if a text field is found, othwerise undefined
      */
     testForTextFields = (whichSlot: string) => {
-        const slotHasText = Doc.Get(this.dataDoc, whichSlot, true) instanceof RichTextField || typeof Doc.Get(this.dataDoc, whichSlot, true) === 'string';
+        const slotData = Doc.Get(this.dataDoc, whichSlot, true);
+        const slotHasText = slotData instanceof RichTextField || typeof slotData === 'string';
         const subjectText = RTFCast(this.Document[this.fieldKey])?.Text.trim();
         const altText = RTFCast(this.Document[this.fieldKey + '_alternate'])?.Text.trim();
         const layoutTemplateString =
@@ -180,8 +183,8 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
         //     where (this) is replaced by the text in the fieldKey slot abd this.excludeWords is repalced by the conetnts of the excludeWords field
         // The GPT call will put the "answer"  in the second slot of the comparison (eg., text_2)
         if (whichSlot.endsWith('2') && !layoutTemplateString?.includes(whichSlot)) {
-            var queryText = altText?.replace('(this)', subjectText); // TODO: this should be done in KeyValueBox.setField but it doesn't know about the fieldKey ...
-            if (queryText && queryText.match(/\(\(.*\)\)/)) {
+            const queryText = altText?.replace('(this)', subjectText); // TODO: this should be done in KeyValueBox.setField but it doesn't know about the fieldKey ...
+            if (queryText?.match(/\(\(.*\)\)/)) {
                 KeyValueBox.SetField(this.Document, whichSlot, ':=' + queryText, false); // make the second slot be a computed field on the data doc that calls ChatGpt
             }
         }
@@ -190,17 +193,16 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
 
     _closeRef = React.createRef<HTMLDivElement>();
     render() {
-        const clearButton = (which: string) => {
-            return (
-                <div
-                    ref={this._closeRef}
-                    className={`clear-button ${which}`}
-                    onPointerDown={e => this.closeDown(e, which)} // prevent triggering slider movement in registerSliding
-                >
-                    <FontAwesomeIcon className={`clear-button ${which}`} icon="times" size="sm" />
-                </div>
-            );
-        };
+        trace();
+        const clearButton = (which: string) => (
+            <div
+                ref={this._closeRef}
+                className={`clear-button ${which}`}
+                onPointerDown={e => this.closeDown(e, which)} // prevent triggering slider movement in registerSliding
+            >
+                <FontAwesomeIcon className={`clear-button ${which}`} icon="times" size="sm" />
+            </div>
+        );
 
         /**
          * Display the Docs in the before/after fields of the comparison.  This also supports a GPT flash card use case
@@ -229,7 +231,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
                         isDocumentActive={returnFalse}
                         whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
                         styleProvider={this._isAnyChildContentActive ? this._props.styleProvider : this.docStyleProvider}
-                        hideLinkButton={true}
+                        hideLinkButton
                         pointerEvents={this._isAnyChildContentActive ? undefined : returnNone}
                     />
                     {layoutTemplateString ? null : clearButton(whichSlot)}
@@ -240,13 +242,11 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
                 </div>
             );
         };
-        const displayBox = (which: string, index: number, cover: number) => {
-            return (
-                <div className={`${index === 0 ? 'before' : 'after'}Box-cont`} key={which} style={{ width: this._props.PanelWidth() }} onPointerDown={e => this.registerSliding(e, cover)} ref={ele => this.createDropTarget(ele, which, index)}>
-                    {displayDoc(which)}
-                </div>
-            );
-        };
+        const displayBox = (which: string, index: number, cover: number) => (
+            <div className={`${index === 0 ? 'before' : 'after'}Box-cont`} key={which} style={{ width: this._props.PanelWidth() }} onPointerDown={e => this.registerSliding(e, cover)} ref={ele => this.createDropTarget(ele, which, index)}>
+                {displayDoc(which)}
+            </div>
+        );
 
         return (
             <div className={`comparisonBox${this._props.isContentActive() ? '-interactive' : ''}` /* change className to easily disable/enable pointer events in CSS */}>
diff --git a/src/client/views/nodes/DataVizBox/DataVizBox.tsx b/src/client/views/nodes/DataVizBox/DataVizBox.tsx
index 22f1f7b79..2a0bc42ee 100644
--- a/src/client/views/nodes/DataVizBox/DataVizBox.tsx
+++ b/src/client/views/nodes/DataVizBox/DataVizBox.tsx
@@ -1,9 +1,11 @@
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { Checkbox } from '@mui/material';
 import { Colors, Toggle, ToggleType, Type } from 'browndash-components';
 import { IReactionDisposer, ObservableMap, action, computed, makeObservable, observable, reaction, runInAction } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
-import { emptyFunction, returnEmptyString, returnFalse, returnOne, setupMoveUpEvents } from '../../../../Utils';
+import { returnEmptyString, returnFalse, returnOne, setupMoveUpEvents } from '../../../../ClientUtils';
+import { emptyFunction } from '../../../../Utils';
 import { Doc, DocListCast, Field, Opt, StrListCast } from '../../../../fields/Doc';
 import { InkTool } from '../../../../fields/InkField';
 import { List } from '../../../../fields/List';
@@ -14,20 +16,18 @@ import { TraceMobx } from '../../../../fields/util';
 import { DocUtils, Docs } from '../../../documents/Documents';
 import { DocumentManager } from '../../../util/DocumentManager';
 import { UndoManager, undoable } from '../../../util/UndoManager';
-import { ViewBoxAnnotatableComponent, ViewBoxInterface } from '../../DocComponent';
+import { PinProps, ViewBoxAnnotatableComponent, ViewBoxInterface } from '../../DocComponent';
 import { MarqueeAnnotator } from '../../MarqueeAnnotator';
 import { SidebarAnnos } from '../../SidebarAnnos';
 import { AnchorMenu } from '../../pdf/AnchorMenu';
 import { GPTPopup } from '../../pdf/GPTPopup/GPTPopup';
 import { DocumentView } from '../DocumentView';
-import { FocusViewOptions, FieldView, FieldViewProps } from '../FieldView';
-import { PinProps } from '../trails';
+import { FieldView, FieldViewProps, FocusViewOptions } from '../FieldView';
 import './DataVizBox.scss';
 import { Histogram } from './components/Histogram';
 import { LineChart } from './components/LineChart';
 import { PieChart } from './components/PieChart';
 import { TableBox } from './components/TableBox';
-import { Checkbox } from '@mui/material';
 
 export enum DataVizView {
     TABLE = 'table',
diff --git a/src/client/views/nodes/DataVizBox/SchemaCSVPopUp.tsx b/src/client/views/nodes/DataVizBox/SchemaCSVPopUp.tsx
index 24023077f..0084d7394 100644
--- a/src/client/views/nodes/DataVizBox/SchemaCSVPopUp.tsx
+++ b/src/client/views/nodes/DataVizBox/SchemaCSVPopUp.tsx
@@ -3,7 +3,8 @@ import { action, makeObservable, observable } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
 import { CgClose } from 'react-icons/cg';
-import { Utils, emptyFunction, setupMoveUpEvents } from '../../../../Utils';
+import { ClientUtils, setupMoveUpEvents } from '../../../../ClientUtils';
+import { emptyFunction } from '../../../../Utils';
 import { Doc } from '../../../../fields/Doc';
 import { StrCast } from '../../../../fields/Types';
 import { DragManager } from '../../../util/DragManager';
@@ -84,7 +85,7 @@ export class SchemaCSVPopUp extends React.Component<SchemaCSVPopUpProps> {
                     const embedding = Doc.MakeEmbedding(this.dataVizDoc!);
                     return embedding;
                 };
-                if (this.view && sourceAnchorCreator && !Utils.isClick(e.clientX, e.clientY, downX, downY, Date.now())) {
+                if (this.view && sourceAnchorCreator && !ClientUtils.isClick(e.clientX, e.clientY, downX, downY, Date.now())) {
                     DragManager.StartAnchorAnnoDrag(e.target instanceof HTMLElement ? [e.target] : [], new DragManager.AnchorAnnoDragData(this.view, sourceAnchorCreator, targetCreator), downX, downY, {
                         dragComplete: e => {
                             this.setVisible(false);
diff --git a/src/client/views/nodes/DataVizBox/components/Histogram.tsx b/src/client/views/nodes/DataVizBox/components/Histogram.tsx
index 6672603f3..58cacef76 100644
--- a/src/client/views/nodes/DataVizBox/components/Histogram.tsx
+++ b/src/client/views/nodes/DataVizBox/components/Histogram.tsx
@@ -11,8 +11,9 @@ import { listSpec } from '../../../../../fields/Schema';
 import { Cast, DocCast, StrCast } from '../../../../../fields/Types';
 import { Docs } from '../../../../documents/Documents';
 import { undoable } from '../../../../util/UndoManager';
+import { PinProps } from '../../../DocComponent';
 import { ObservableReactComponent } from '../../../ObservableReactComponent';
-import { PinProps, PresBox } from '../../trails';
+import { PresBox } from '../../trails';
 import { scaleCreatorNumerical, yAxisCreator } from '../utils/D3Utils';
 import './Chart.scss';
 
@@ -64,7 +65,7 @@ export class Histogram extends ObservableReactComponent<HistogramProps> {
         if (this._props.axes.length < 1) return [];
         if (this._props.axes.length < 2) {
             var ax0 = this._props.axes[0];
-            if (!/[A-Za-z-:]/.test(this._props.records[0][ax0])){
+            if (!/[A-Za-z-:]/.test(this._props.records[0][ax0])) {
                 this.numericalXData = true;
             }
             return this._tableData.map(record => ({ [ax0]: record[this._props.axes[0]] }));
@@ -132,7 +133,7 @@ export class Histogram extends ObservableReactComponent<HistogramProps> {
 
     // cleans data by converting numerical data to numbers and taking out empty cells
     data = (dataSet: any) => {
-        var validData = dataSet.filter((d: { [x: string]: unknown }) => !Object.keys(dataSet[0]).some(key => !d[key] || Number.isNaN(d[key])));
+        var validData = dataSet.filter((d: { [x: string]: unknown }) => !Object.keys(dataSet[0]).some(key => !d[key] || isNaN(d[key])));
         const field = dataSet[0] ? Object.keys(dataSet[0])[0] : undefined;
         return !field
             ? []
@@ -191,7 +192,7 @@ export class Histogram extends ObservableReactComponent<HistogramProps> {
         const endingPoint = this.numericalXData ? this.rangeVals.xMax! : numBins;
 
         // converts data into Objects
-        var histDataSet = dataSet.filter((d: { [x: string]: unknown }) => !Object.keys(dataSet[0]).some(key => !d[key] || Number.isNaN(d[key])));
+        var histDataSet = dataSet.filter((d: { [x: string]: unknown }) => !Object.keys(dataSet[0]).some(key => !d[key] || isNaN(d[key])));
         if (!this.numericalXData) {
             var histStringDataSet: { [x: string]: unknown }[] = [];
             if (this.numericalYData) {
@@ -452,17 +453,16 @@ export class Histogram extends ObservableReactComponent<HistogramProps> {
                     : ''
             );
             selected = selected.substring(0, selected.length - 2) + ' }';
-            if (this._props.titleCol!="" && (!this._currSelected["frequency"] || this._currSelected["frequency"]<10)){
-                selected+= "\n" + this._props.titleCol + ": "
+            if (this._props.titleCol != '' && (!this._currSelected['frequency'] || this._currSelected['frequency'] < 10)) {
+                selected += '\n' + this._props.titleCol + ': ';
                 this._tableData.forEach(each => {
-                    if (this._currSelected[this._props.axes[0]]==each[this._props.axes[0]]) {
-                        if (this._props.axes[1]){
-                            if (this._currSelected[this._props.axes[1]]==each[this._props.axes[1]]) selected+= each[this._props.titleCol] + ", ";
-                        }
-                        else selected+= each[this._props.titleCol] + ", ";
+                    if (this._currSelected[this._props.axes[0]] == each[this._props.axes[0]]) {
+                        if (this._props.axes[1]) {
+                            if (this._currSelected[this._props.axes[1]] == each[this._props.axes[1]]) selected += each[this._props.titleCol] + ', ';
+                        } else selected += each[this._props.titleCol] + ', ';
                     }
-                })
-                selected = selected.slice(0,-1).slice(0,-1);
+                });
+                selected = selected.slice(0, -1).slice(0, -1);
             }
         }
         var selectedBarColor;
diff --git a/src/client/views/nodes/DataVizBox/components/LineChart.tsx b/src/client/views/nodes/DataVizBox/components/LineChart.tsx
index e093ec648..c667a15de 100644
--- a/src/client/views/nodes/DataVizBox/components/LineChart.tsx
+++ b/src/client/views/nodes/DataVizBox/components/LineChart.tsx
@@ -11,10 +11,11 @@ import { Docs } from '../../../../documents/Documents';
 import { DocumentManager } from '../../../../util/DocumentManager';
 import { undoable } from '../../../../util/UndoManager';
 import { ObservableReactComponent } from '../../../ObservableReactComponent';
-import { PinProps, PresBox } from '../../trails';
+import { PresBox } from '../../trails';
 import { DataVizBox } from '../DataVizBox';
 import { createLineGenerator, drawLine, minMaxRange, scaleCreatorNumerical, xAxisCreator, xGrid, yAxisCreator, yGrid } from '../utils/D3Utils';
 import './Chart.scss';
+import { PinProps } from '../../../DocComponent';
 
 export interface DataPoint {
     x: number;
@@ -258,17 +259,18 @@ export class LineChart extends ObservableReactComponent<LineChartProps> {
             .attr('transform', `translate(${margin.left}, ${margin.top})`));
 
         var validSecondData;
-        if (this._props.axes.length>2){ // for when there are 2 lines on the chart
+        if (this._props.axes.length > 2) {
+            // for when there are 2 lines on the chart
             var next = this._tableData.map(record => ({ x: Number(record[this._props.axes[0]]), y: Number(record[this._props.axes[2]]) })).sort((a, b) => (a.x < b.x ? -1 : 1));
             validSecondData = next.filter(d => {
-                if (!d.x || Number.isNaN(d.x) || !d.y || Number.isNaN(d.y)) return false;
+                if (!d.x || isNaN(d.x) || !d.y || isNaN(d.y)) return false;
                 return true;
             });
             var secondDataRange = minMaxRange([validSecondData]);
-            if (secondDataRange.xMax!>xMax) xMax = secondDataRange.xMax;
-            if (secondDataRange.yMax!>yMax) yMax = secondDataRange.yMax;
-            if (secondDataRange.xMin!<xMin) xMin = secondDataRange.xMin;
-            if (secondDataRange.yMin!<yMin) yMin = secondDataRange.yMin;
+            if (secondDataRange.xMax! > xMax) xMax = secondDataRange.xMax;
+            if (secondDataRange.yMax! > yMax) yMax = secondDataRange.yMax;
+            if (secondDataRange.xMin! < xMin) xMin = secondDataRange.xMin;
+            if (secondDataRange.yMin! < yMin) yMin = secondDataRange.yMin;
         }
 
         // creating the x and y scales
@@ -285,37 +287,45 @@ export class LineChart extends ObservableReactComponent<LineChartProps> {
         if (validSecondData) {
             drawLine(svg.append('path'), validSecondData, lineGen, true);
             this.drawDataPoints(validSecondData, 0, xScale, yScale);
-            svg.append('path').attr("stroke", "red");
+            svg.append('path').attr('stroke', 'red');
 
             // legend
-            var color = d3.scaleOrdinal()
-                .range(["black", "blue"])
-                .domain([this._props.axes[1], this._props.axes[2]])
-            svg.selectAll("mydots")
+            var color = d3.scaleOrdinal().range(['black', 'blue']).domain([this._props.axes[1], this._props.axes[2]]);
+            svg.selectAll('mydots')
                 .data([this._props.axes[1], this._props.axes[2]])
                 .enter()
-                .append("circle")
-                    .attr("cx", 5)
-                    .attr("cy", function(d,i){ return -30 + i*15})
-                    .attr("r", 7)
-                    .style("fill", function(d){ return color(d)})
-            svg.selectAll("mylabels")
+                .append('circle')
+                .attr('cx', 5)
+                .attr('cy', function (d, i) {
+                    return -30 + i * 15;
+                })
+                .attr('r', 7)
+                .style('fill', function (d) {
+                    return color(d);
+                });
+            svg.selectAll('mylabels')
                 .data([this._props.axes[1], this._props.axes[2]])
                 .enter()
-                .append("text")
-                    .attr("x", 25)
-                    .attr("y", function(d,i){ return -30 + i*15})
-                    .style("fill", function(d){ return color(d)})
-                    .text(function(d){ return d})
-                    .attr("text-anchor", "left")
-                    .style("alignment-baseline", "middle")
+                .append('text')
+                .attr('x', 25)
+                .attr('y', function (d, i) {
+                    return -30 + i * 15;
+                })
+                .style('fill', function (d) {
+                    return color(d);
+                })
+                .text(function (d) {
+                    return d;
+                })
+                .attr('text-anchor', 'left')
+                .style('alignment-baseline', 'middle');
         }
 
         // get valid data points
         const data = dataSet[0];
         var validData = data.filter(d => {
             Object.keys(data[0]).map(key => {
-                if (!d[key] || Number.isNaN(d[key])) return false;
+                if (!d[key] || isNaN(d[key])) return false;
             });
             return true;
         });
@@ -399,16 +409,16 @@ export class LineChart extends ObservableReactComponent<LineChartProps> {
         else if (this._props.axes.length > 0) titleAccessor = titleAccessor + this._props.axes[0];
         if (!this._props.layoutDoc[titleAccessor]) this._props.layoutDoc[titleAccessor] = this.defaultGraphTitle;
         const selectedPt = this._currSelected ? `{ ${this._props.axes[0]}: ${this._currSelected.x} ${this._props.axes[1]}: ${this._currSelected.y} }` : 'none';
-        var selectedTitle = "";
-        if (this._currSelected && this._props.titleCol){
-            selectedTitle+= "\n" + this._props.titleCol + ": "
+        var selectedTitle = '';
+        if (this._currSelected && this._props.titleCol) {
+            selectedTitle += '\n' + this._props.titleCol + ': ';
             this._tableData.forEach(each => {
-                 var mapThisEntry = false;
-                if (this._currSelected.x==each[this._props.axes[0]] && this._currSelected.y==each[this._props.axes[1]]) mapThisEntry = true;
-                else if (this._currSelected.y==each[this._props.axes[0]] && this._currSelected.x==each[this._props.axes[1]]) mapThisEntry = true;
-                if (mapThisEntry) selectedTitle += each[this._props.titleCol] + ", ";
-            })
-            selectedTitle = selectedTitle.slice(0,-1).slice(0,-1);
+                var mapThisEntry = false;
+                if (this._currSelected.x == each[this._props.axes[0]] && this._currSelected.y == each[this._props.axes[1]]) mapThisEntry = true;
+                else if (this._currSelected.y == each[this._props.axes[0]] && this._currSelected.x == each[this._props.axes[1]]) mapThisEntry = true;
+                if (mapThisEntry) selectedTitle += each[this._props.titleCol] + ', ';
+            });
+            selectedTitle = selectedTitle.slice(0, -1).slice(0, -1);
         }
         if (this._lineChartData.length > 0 || !this.parentViz || this.parentViz.length == 0) {
             return this._props.axes.length >= 2 && /\d/.test(this._props.records[0][this._props.axes[0]]) && /\d/.test(this._props.records[0][this._props.axes[1]]) ? (
diff --git a/src/client/views/nodes/DataVizBox/components/PieChart.tsx b/src/client/views/nodes/DataVizBox/components/PieChart.tsx
index fc23f47de..2735a40d5 100644
--- a/src/client/views/nodes/DataVizBox/components/PieChart.tsx
+++ b/src/client/views/nodes/DataVizBox/components/PieChart.tsx
@@ -12,8 +12,9 @@ import { Cast, DocCast, StrCast } from '../../../../../fields/Types';
 import { Docs } from '../../../../documents/Documents';
 import { undoable } from '../../../../util/UndoManager';
 import { ObservableReactComponent } from '../../../ObservableReactComponent';
-import { PinProps, PresBox } from '../../trails';
+import { PresBox } from '../../trails';
 import './Chart.scss';
+import { PinProps } from '../../../DocComponent';
 
 export interface PieChartProps {
     Document: Doc;
@@ -122,7 +123,7 @@ export class PieChart extends ObservableReactComponent<PieChartProps> {
 
     // cleans data by converting numerical data to numbers and taking out empty cells
     data = (dataSet: any) => {
-        const validData = dataSet.filter((d: { [x: string]: unknown }) => !Object.keys(dataSet[0]).some(key => !d[key] || Number.isNaN(d[key])));
+        const validData = dataSet.filter((d: { [x: string]: unknown }) => !Object.keys(dataSet[0]).some(key => !d[key] || isNaN(d[key])));
         const field = dataSet[0] ? Object.keys(dataSet[0])[0] : undefined;
         return !field
             ? undefined
@@ -192,7 +193,7 @@ export class PieChart extends ObservableReactComponent<PieChartProps> {
 
         // converts data into Objects
         var data = this.data(dataSet);
-        var pieDataSet = dataSet.filter((d: { [x: string]: unknown }) => !Object.keys(dataSet[0]).some(key => !d[key] || Number.isNaN(d[key])));
+        var pieDataSet = dataSet.filter((d: { [x: string]: unknown }) => !Object.keys(dataSet[0]).some(key => !d[key] || isNaN(d[key])));
         if (this.byCategory) {
             let uniqueCategories = [...new Set(data)];
             var pieStringDataSet: { frequency: number }[] = [];
@@ -348,17 +349,16 @@ export class PieChart extends ObservableReactComponent<PieChartProps> {
             });
             selected = selected.substring(0, selected.length - 2);
             selected += ' }';
-            if (this._props.titleCol!="" && (!this._currSelected["frequency"] || this._currSelected["frequency"]<10)){
-                selected+= "\n" + this._props.titleCol + ": "
+            if (this._props.titleCol != '' && (!this._currSelected['frequency'] || this._currSelected['frequency'] < 10)) {
+                selected += '\n' + this._props.titleCol + ': ';
                 this._tableData.forEach(each => {
-                    if (this._currSelected[this._props.axes[0]]==each[this._props.axes[0]]) {
-                        if (this._props.axes[1]){
-                            if (this._currSelected[this._props.axes[1]]==each[this._props.axes[1]]) selected+= each[this._props.titleCol] + ", ";
-                        }
-                        else selected+= each[this._props.titleCol] + ", ";
+                    if (this._currSelected[this._props.axes[0]] == each[this._props.axes[0]]) {
+                        if (this._props.axes[1]) {
+                            if (this._currSelected[this._props.axes[1]] == each[this._props.axes[1]]) selected += each[this._props.titleCol] + ', ';
+                        } else selected += each[this._props.titleCol] + ', ';
                     }
-                })
-                selected = selected.slice(0,-1).slice(0,-1);
+                });
+                selected = selected.slice(0, -1).slice(0, -1);
             }
         } else selected = 'none';
         var selectedSliceColor;
diff --git a/src/client/views/nodes/DataVizBox/components/TableBox.tsx b/src/client/views/nodes/DataVizBox/components/TableBox.tsx
index 53d1869d9..15959c61d 100644
--- a/src/client/views/nodes/DataVizBox/components/TableBox.tsx
+++ b/src/client/views/nodes/DataVizBox/components/TableBox.tsx
@@ -2,7 +2,8 @@ import { Button, Type } from 'browndash-components';
 import { IReactionDisposer, action, computed, makeObservable, observable, reaction } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
-import { Utils, emptyFunction, setupMoveUpEvents } from '../../../../../Utils';
+import { ClientUtils, setupMoveUpEvents } from '../../../../../ClientUtils';
+import { emptyFunction } from '../../../../../Utils';
 import { Doc, Field, NumListCast } from '../../../../../fields/Doc';
 import { List } from '../../../../../fields/List';
 import { listSpec } from '../../../../../fields/Schema';
@@ -137,7 +138,7 @@ export class TableBox extends ObservableReactComponent<TableBoxProps> {
                     embedding.pieSliceColors = Field.Copy(this._props.layoutDoc.pieSliceColors);
                     return embedding;
                 };
-                if (this._props.docView?.() && !Utils.isClick(e.clientX, e.clientY, downX, downY, Date.now())) {
+                if (this._props.docView?.() && !ClientUtils.isClick(e.clientX, e.clientY, downX, downY, Date.now())) {
                     DragManager.StartAnchorAnnoDrag(e.target instanceof HTMLElement ? [e.target] : [], new DragManager.AnchorAnnoDragData(this._props.docView()!, sourceAnchorCreator, targetCreator), downX, downY, {
                         dragComplete: e => {
                             if (!e.aborted && e.annoDragData && e.annoDragData.linkSourceDoc && e.annoDragData.dropDocument && e.linkDocument) {
diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx
index e729e2fa2..518158a7f 100644
--- a/src/client/views/nodes/DocumentContentsView.tsx
+++ b/src/client/views/nodes/DocumentContentsView.tsx
@@ -3,7 +3,8 @@ import { observer } from 'mobx-react';
 import * as React from 'react';
 import JsxParser from 'react-jsx-parser';
 import * as XRegExp from 'xregexp';
-import { OmitKeys, Without, emptyPath } from '../../../Utils';
+import { OmitKeys } from '../../../ClientUtils';
+import { Without, emptyPath } from '../../../Utils';
 import { Doc, Opt } from '../../../fields/Doc';
 import { AclPrivate, DocData } from '../../../fields/DocSymbols';
 import { ScriptField } from '../../../fields/ScriptField';
@@ -20,7 +21,6 @@ import { SchemaRowBox } from '../collections/collectionSchema/SchemaRowBox';
 import { PresElementBox } from '../nodes/trails/PresElementBox';
 import { SearchBox } from '../search/SearchBox';
 import { DashWebRTCVideo } from '../webcam/DashWebRTCVideo';
-import { YoutubeBox } from './../../apis/youtube/YoutubeBox';
 import { AudioBox } from './AudioBox';
 import { ComparisonBox } from './ComparisonBox';
 import { DataVizBox } from './DataVizBox/DataVizBox';
@@ -250,7 +250,6 @@ export class DocumentContentsView extends ObservableReactComponent<DocumentConte
                     AudioBox,
                     RecordingBox,
                     PresBox,
-                    YoutubeBox,
                     PresElementBox,
                     SearchBox,
                     FunctionPlotBox,
diff --git a/src/client/views/nodes/DocumentIcon.tsx b/src/client/views/nodes/DocumentIcon.tsx
index 4a22766cc..364406197 100644
--- a/src/client/views/nodes/DocumentIcon.tsx
+++ b/src/client/views/nodes/DocumentIcon.tsx
@@ -3,11 +3,12 @@ import { action, makeObservable, observable } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
 import { factory } from 'typescript';
-import { Field } from '../../../fields/Doc';
+import { FieldType } from '../../../fields/Doc';
 import { Id } from '../../../fields/FieldSymbols';
+import { StrCast } from '../../../fields/Types';
 import { DocumentManager } from '../../util/DocumentManager';
 import { Transformer, ts } from '../../util/Scripting';
-import { SettingsManager } from '../../util/SettingsManager';
+import { SnappingManager } from '../../util/SnappingManager';
 import { LightboxView } from '../LightboxView';
 import { ObservableReactComponent } from '../ObservableReactComponent';
 import { DocumentView } from './DocumentView';
@@ -28,22 +29,22 @@ export class DocumentIcon extends ObservableReactComponent<DocumentIconProps> {
         return LightboxView.LightboxDoc ? DocumentManager.Instance.DocumentViews.filter(v => LightboxView.Contains(v)) : DocumentManager.Instance.DocumentViews;
     }
     render() {
-        const view = this._props.view;
-        const { left, top, right, bottom } = view.getBounds || { left: 0, top: 0, right: 0, bottom: 0 };
+        const { view } = this._props;
+        const { left, top, right } = view.getBounds || { left: 0, top: 0, right: 0, bottom: 0 };
 
         return (
             <div
                 className="documentIcon-outerDiv"
-                onPointerEnter={action(e => (this._hovered = true))}
-                onPointerLeave={action(e => (this._hovered = false))}
+                onPointerEnter={action(() => { this._hovered = true; })} // prettier-ignore
+                onPointerLeave={action(() => { this._hovered = false; })} // prettier-ignore
                 style={{
                     pointerEvents: 'all',
                     opacity: this._hovered ? 0.3 : 1,
                     position: 'absolute',
-                    background: SettingsManager.userBackgroundColor,
+                    background: SnappingManager.userBackgroundColor,
                     transform: `translate(${(left + right) / 2}px, ${top}px)`,
                 }}>
-                <Tooltip title={<>{this._props.view.Document.title}</>}>
+                <Tooltip title={<div>{StrCast(this._props.view.Document?.title)}</div>}>
                     <p>d{this._props.index}</p>
                 </Tooltip>
             </div>
@@ -56,40 +57,40 @@ export class DocumentIconContainer extends React.Component {
     public static getTransformer(): Transformer {
         const usedDocuments = new Set<number>();
         return {
-            transformer: context => {
-                return root => {
-                    function visit(node: ts.Node) {
-                        node = ts.visitEachChild(node, visit, context);
+            transformer: context => root => {
+                function visit(nodeIn: ts.Node) {
+                    const node = ts.visitEachChild(nodeIn, visit, context);
 
-                        if (ts.isIdentifier(node)) {
-                            const isntPropAccess = !ts.isPropertyAccessExpression(node.parent) || node.parent.expression === node;
-                            const isntPropAssign = !ts.isPropertyAssignment(node.parent) || node.parent.name !== node;
-                            const isntParameter = !ts.isParameter(node.parent);
-                            if (isntPropAccess && isntPropAssign && isntParameter && !(node.text in globalThis)) {
-                                const match = node.text.match(/d([0-9]+)/);
-                                if (match) {
-                                    const m = parseInt(match[1]);
-                                    const doc = DocumentIcon.DocViews[m].Document;
-                                    usedDocuments.add(m);
-                                    return factory.createIdentifier(`idToDoc("${doc[Id]}")`);
-                                }
+                    if (ts.isIdentifier(node)) {
+                        const isntPropAccess = !ts.isPropertyAccessExpression(node.parent) || node.parent.expression === node;
+                        const isntPropAssign = !ts.isPropertyAssignment(node.parent) || node.parent.name !== node;
+                        const isntParameter = !ts.isParameter(node.parent);
+                        if (isntPropAccess && isntPropAssign && isntParameter && !(node.text in globalThis)) {
+                            const match = node.text.match(/d([0-9]+)/);
+                            if (match) {
+                                const m = parseInt(match[1]);
+                                const doc = DocumentIcon.DocViews[m].Document;
+                                usedDocuments.add(m);
+                                return factory.createIdentifier(`idToDoc("${doc[Id]}")`);
                             }
                         }
-
-                        return node;
                     }
-                    return ts.visitNode(root, visit);
-                };
+
+                    return node;
+                }
+                return ts.visitNode(root, visit);
             },
             getVars() {
                 const docs = DocumentIcon.DocViews;
-                const capturedVariables: { [name: string]: Field } = {};
-                usedDocuments.forEach(index => (capturedVariables[`d${index}`] = docs.length > index ? docs[index].Document : `d${index}`));
+                const capturedVariables: { [name: string]: FieldType } = {};
+                usedDocuments.forEach(index => {
+                    capturedVariables[`d${index}`] = docs.length > index ? docs[index].Document : `d${index}`;
+                });
                 return capturedVariables;
             },
         };
     }
     render() {
-        return DocumentIcon.DocViews.map((dv, i) => <DocumentIcon key={i} index={i} view={dv} />);
+        return DocumentIcon.DocViews.map((dv, i) => <DocumentIcon key={dv.DocUniqueId} index={i} view={dv} />);
     }
 }
diff --git a/src/client/views/nodes/DocumentLinksButton.tsx b/src/client/views/nodes/DocumentLinksButton.tsx
index 2a68d2bf6..d378082f8 100644
--- a/src/client/views/nodes/DocumentLinksButton.tsx
+++ b/src/client/views/nodes/DocumentLinksButton.tsx
@@ -3,21 +3,22 @@ import { Tooltip } from '@mui/material';
 import { action, computed, makeObservable, observable, runInAction } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
-import { StopEvent, emptyFunction, returnFalse, setupMoveUpEvents } from '../../../Utils';
+import { StopEvent, returnFalse, setupMoveUpEvents } from '../../../ClientUtils';
+import { emptyFunction } from '../../../Utils';
 import { Doc } from '../../../fields/Doc';
+import { DocData } from '../../../fields/DocSymbols';
 import { StrCast } from '../../../fields/Types';
 import { DocUtils } from '../../documents/Documents';
 import { DragManager } from '../../util/DragManager';
 import { Hypothesis } from '../../util/HypothesisUtils';
 import { LinkManager } from '../../util/LinkManager';
 import { UndoManager, undoBatch } from '../../util/UndoManager';
+import { PinProps } from '../DocComponent';
 import { ObservableReactComponent } from '../ObservableReactComponent';
 import './DocumentLinksButton.scss';
 import { DocumentView } from './DocumentView';
 import { LinkDescriptionPopup } from './LinkDescriptionPopup';
 import { TaskCompletionBox } from './TaskCompletedBox';
-import { PinProps } from './trails';
-import { DocData } from '../../../fields/DocSymbols';
 
 interface DocumentLinksButtonProps {
     View: DocumentView;
@@ -151,7 +152,7 @@ export class DocumentLinksButton extends ObservableReactComponent<DocumentLinksB
                 DocumentLinksButton.StartLinkView = undefined;
                 DocumentLinksButton.AnnotationId = undefined;
                 DocumentLinksButton.AnnotationUri = undefined;
-                //!this._props.StartLink
+                // !this._props.StartLink
             } else if (startLink !== endLink) {
                 endLink = endLinkView?.ComponentView?.getAnchor?.(true, pinProps) || endLink;
                 startLink = DocumentLinksButton.StartLinkView?.ComponentView?.getAnchor?.(true) || startLink;
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index ee7bbbdba..dbd2ebe0d 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -1,11 +1,15 @@
+/* eslint-disable no-use-before-define */
+/* eslint-disable react/jsx-props-no-spreading */
+/* eslint-disable jsx-a11y/no-static-element-interactions */
 import { IconProp } from '@fortawesome/fontawesome-svg-core';
 import { Howl } from 'howler';
 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';
-import { DivWidth, Utils, emptyFunction, isTargetChildOf as isParentOf, lightOrDark, returnEmptyString, returnFalse, returnTrue, returnVal, simulateMouseClick } from '../../../Utils';
-import { Doc, DocListCast, Field, Opt, StrListCast } from '../../../fields/Doc';
+import { ClientUtils, DivWidth, isTargetChildOf as isParentOf, lightOrDark, returnEmptyString, returnFalse, returnTrue, returnVal, simulateMouseClick } from '../../../ClientUtils';
+import { Utils, emptyFunction, emptyPath } from '../../../Utils';
+import { Doc, DocListCast, Field, FieldType, Opt, StrListCast } from '../../../fields/Doc';
 import { AclPrivate, Animation, AudioPlay, DocData, DocViews } from '../../../fields/DocSymbols';
 import { Id } from '../../../fields/FieldSymbols';
 import { InkTool } from '../../../fields/InkField';
@@ -23,14 +27,14 @@ import { CollectionViewType, DocumentType } from '../../documents/DocumentTypes'
 import { DocUtils, Docs } from '../../documents/Documents';
 import { DictationManager } from '../../util/DictationManager';
 import { DocumentManager } from '../../util/DocumentManager';
-import { DragManager, dropActionType } from '../../util/DragManager';
+import { DragManager } from '../../util/DragManager';
+import { dropActionType } from '../../util/DropActionTypes';
 import { MakeTemplate, makeUserTemplateButton } from '../../util/DropConverter';
 import { FollowLinkScript } from '../../util/LinkFollower';
-import { LinkManager } from '../../util/LinkManager';
+import { LinkManager, UPDATE_SERVER_CACHE } from '../../util/LinkManager';
 import { ScriptingGlobals } from '../../util/ScriptingGlobals';
 import { SearchUtil } from '../../util/SearchUtil';
 import { SelectionManager } from '../../util/SelectionManager';
-import { SettingsManager } from '../../util/SettingsManager';
 import { SharingManager } from '../../util/SharingManager';
 import { SnappingManager } from '../../util/SnappingManager';
 import { UndoManager, undoBatch, undoable } from '../../util/UndoManager';
@@ -42,6 +46,7 @@ import { FieldsDropdown } from '../FieldsDropdown';
 import { GestureOverlay } from '../GestureOverlay';
 import { LightboxView } from '../LightboxView';
 import { AudioAnnoState, StyleProp } from '../StyleProvider';
+import { CollectionFreeFormDocumentView } from './CollectionFreeFormDocumentView';
 import { DocumentContentsView, ObserverJsxParser } from './DocumentContentsView';
 import { DocumentLinksButton } from './DocumentLinksButton';
 import './DocumentView.scss';
@@ -50,12 +55,6 @@ import { KeyValueBox } from './KeyValueBox';
 import { LinkAnchorBox } from './LinkAnchorBox';
 import { FormattedTextBox } from './formattedText/FormattedTextBox';
 import { PresEffect, PresEffectDirection } from './trails';
-interface Window {
-    MediaRecorder: MediaRecorder;
-}
-declare class MediaRecorder {
-    constructor(e: any); // whatever MediaRecorder has
-}
 
 export enum OpenWhereMod {
     none = '',
@@ -83,9 +82,6 @@ export enum OpenWhere {
     addRightKeyvalue = 'add:right:keyValue',
 }
 
-export function returnEmptyDocViewList() {
-    return [] as DocumentView[];
-}
 export interface DocumentViewProps extends FieldViewSharedProps {
     hideDecorations?: boolean; // whether to suppress all DocumentDecorations when doc is selected
     hideResizeHandles?: boolean; // whether to suppress resized handles on doc decorations when this document is selected
@@ -183,12 +179,12 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
 
     @computed get disableClickScriptFunc() {
         const onScriptDisable = this._props.onClickScriptDisable ?? this._componentView?.onClickScriptDisable?.() ?? this.layoutDoc.onClickScriptDisable;
-        // prettier-ignore
         return (
+            // eslint-disable-next-line no-use-before-define
             DocumentView.LongPress ||
             onScriptDisable === 'always' ||
             (onScriptDisable !== 'never' && (this.rootSelected() || this._componentView?.isAnyChildContentActive?.()))
-        );
+        ); // prettier-ignore
     }
     @computed get _rootSelected() {
         return this._props.isSelected() || BoolCast(this._props.TemplateDataDocument && this._props.rootSelected?.());
@@ -237,7 +233,9 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
     }
 
     componentDidMount() {
-        runInAction(() => (this._mounted = true));
+        runInAction(() => {
+            this._mounted = true;
+        });
         this.setupHandlers();
         this._disposers.contentActive = reaction(
             () =>
@@ -249,19 +247,23 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
                     : Doc.ActiveTool !== InkTool.None || SnappingManager.CanEmbed || this.rootSelected() || this.Document.forceActive || this._componentView?.isAnyChildContentActive?.() || this._props.isContentActive()
                       ? true
                       : undefined,
-            active => (this._isContentActive = active),
+            active => {
+                this._isContentActive = active;
+            },
             { fireImmediately: true }
         );
         this._disposers.pointerevents = reaction(
             () => this.style(this.Document, StyleProp.PointerEvents),
-            pointerevents => (this._pointerEvents = pointerevents),
+            pointerevents => {
+                this._pointerEvents = pointerevents;
+            },
             { fireImmediately: true }
         );
     }
     preDrop = (e: Event, de: DragManager.DropEvent, dropAction: dropActionType) => {
         const dragData = de.complete.docDragData;
         if (dragData && this.isContentActive() && !this.props.dontRegisterView) {
-            dragData.dropAction = dropAction ? dropAction : dragData.dropAction;
+            dragData.dropAction = dropAction || dragData.dropAction;
             e.stopPropagation();
         }
     };
@@ -291,7 +293,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
             dragData.removeDocument = this._props.removeDocument;
             dragData.moveDocument = this._props.moveDocument;
             dragData.draggedViews = [docView];
-            dragData.canEmbed = this.Document.dragAction ?? this._props.dragAction ? true : false;
+            dragData.canEmbed = !!(this.Document.dragAction ?? this._props.dragAction);
             (this._props.dragConfig ?? this._componentView?.dragConfig)?.(dragData);
             DragManager.StartDocumentDrag(
                 selected.map(dv => dv.ContentDiv!),
@@ -311,7 +313,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
     onClick = action((e: React.MouseEvent | React.PointerEvent) => {
         if (this._props.isGroupActive?.() === 'child' && !this._props.isDocumentActive?.()) return;
         const documentView = this._docView;
-        if (documentView && !this.Document.ignoreClick && this._props.renderDepth >= 0 && Utils.isClick(e.clientX, e.clientY, this._downX, this._downY, this._downTime)) {
+        if (documentView && !this.Document.ignoreClick && this._props.renderDepth >= 0 && ClientUtils.isClick(e.clientX, e.clientY, this._downX, this._downY, this._downTime)) {
             let stopPropagate = true;
             let preventDefault = true;
             !this.layoutDoc._keepZWhenDragged && this._props.bringToFront?.(this.Document);
@@ -368,6 +370,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
                 if ((clickFunc && waitFordblclick !== 'never') || waitFordblclick === 'always') {
                     this._doubleClickTimeout && clearTimeout(this._doubleClickTimeout);
                     this._doubleClickTimeout = setTimeout(this._singleClickFunc, 300);
+                    // eslint-disable-next-line no-use-before-define
                 } else if (!DocumentView.LongPress) {
                     this._singleClickFunc();
                     this._singleClickFunc = undefined;
@@ -380,6 +383,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
 
     onPointerDown = (e: React.PointerEvent): void => {
         if (this._props.isGroupActive?.() === 'child' && !this._props.isDocumentActive?.()) return;
+        // eslint-disable-next-line no-use-before-define
         this._longPressSelector = setTimeout(() => DocumentView.LongPress && this._props.select(false), 1000);
         if (!GestureOverlay.DownDocView) GestureOverlay.DownDocView = this._docView;
 
@@ -412,7 +416,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
     onPointerMove = (e: PointerEvent): void => {
         if (e.buttons !== 1 || [InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool)) return;
 
-        if (!Utils.isClick(e.clientX, e.clientY, this._downX, this._downY, Date.now())) {
+        if (!ClientUtils.isClick(e.clientX, e.clientY, this._downX, this._downY, Date.now())) {
             this.cleanupPointerEvents();
             this._longPressSelector && clearTimeout(this._longPressSelector);
             this.startDragging(this._downX, this._downY, ((e.ctrlKey || e.altKey) && dropActionType.embed) || ((this.Document.dragAction || this._props.dragAction || undefined) as dropActionType));
@@ -430,14 +434,15 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
 
         if (this.onPointerUpHandler?.script) {
             this.onPointerUpHandler.script.run({ this: this.Document }, console.log);
-        } else if (e.button === 0 && Utils.isClick(e.clientX, e.clientY, this._downX, this._downY, this._downTime)) {
-            this._doubleTap = (this.onDoubleClickHandler?.script || this.Document.defaultDoubleClick !== 'ignore') && Date.now() - this._lastTap < Utils.CLICK_TIME;
+        } else if (e.button === 0 && ClientUtils.isClick(e.clientX, e.clientY, this._downX, this._downY, this._downTime)) {
+            this._doubleTap = (this.onDoubleClickHandler?.script || this.Document.defaultDoubleClick !== 'ignore') && Date.now() - this._lastTap < ClientUtils.CLICK_TIME;
             if (!this.isContentActive()) this._lastTap = Date.now(); // don't want to process the start of a double tap if the doucment is selected
         }
+        // eslint-disable-next-line no-use-before-define
         if (DocumentView.LongPress) e.preventDefault();
     };
 
-    toggleFollowLink = undoable((zoom?: boolean, setTargetToggle?: boolean): void => {
+    toggleFollowLink = undoable((): void => {
         const hadOnClick = this.Document.onClick;
         this.noOnClick();
         this.Document.onClick = hadOnClick ? undefined : FollowLinkScript();
@@ -458,16 +463,14 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
     }, 'default on click');
 
     deleteClicked = undoable(() => this._props.removeDocument?.(this.Document), 'delete doc');
-    setToggleDetail = undoable(
-        (scriptFieldKey: 'onClick') =>
-            (this.Document[scriptFieldKey] = ScriptField.MakeScript(
-                `toggleDetail(documentView, "${StrCast(this.Document.layout_fieldKey)
-                    .replace('layout_', '')
-                    .replace(/^layout$/, 'detail')}")`,
-                { documentView: 'any' }
-            )),
-        'set toggle detail'
-    );
+    setToggleDetail = undoable((scriptFieldKey: 'onClick') => {
+        this.Document[scriptFieldKey] = ScriptField.MakeScript(
+            `toggleDetail(documentView, "${StrCast(this.Document.layout_fieldKey)
+                .replace('layout_', '')
+                .replace(/^layout$/, 'detail')}")`,
+            { documentView: 'any' }
+        );
+    }, 'set toggle detail');
 
     drop = undoable((e: Event, de: DragManager.DropEvent) => {
         if (this._props.dontRegisterView || this._props.LayoutTemplateString?.includes(LinkAnchorBox.name)) return false;
@@ -505,7 +508,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
         const input = document.createElement('input');
         input.type = 'file';
         input.accept = '.zip';
-        input.onchange = _e => {
+        input.onchange = () => {
             if (input.files) {
                 const batch = UndoManager.StartBatch('importing');
                 Doc.importDocument(input.files[0]).then(doc => {
@@ -523,7 +526,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
         if (e && this.layoutDoc.layout_hideContextMenu && Doc.noviceMode) {
             e.preventDefault();
             e.stopPropagation();
-            //!this._props.isSelected(true) && SelectionManager.SelectView(this.DocumentView(), false);
+            // !this._props.isSelected(true) && SelectionManager.SelectView(this.DocumentView(), false);
         }
         // the touch onContextMenu is button 0, the pointer onContextMenu is button 2
         if (e) {
@@ -535,7 +538,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
             e.stopPropagation();
             e.persist();
 
-            if (!navigator.userAgent.includes('Mozilla') && (Math.abs(this._downX - e?.clientX) > 3 || Math.abs(this._downY - e?.clientY) > 3)) {
+            if (!navigator.userAgent.includes('Mozilla') && (Math.abs(this._downX - (e?.clientX ?? 0)) > 3 || Math.abs(this._downY - (e?.clientY ?? 0)) > 3)) {
                 return;
             }
         }
@@ -587,7 +590,11 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
                 zorderItems.push({ description: 'Send to Back', event: () => SelectionManager.Views.forEach(dv => dv._props.bringToFront?.(dv.Document, true)), icon: 'arrow-down' });
                 zorderItems.push({
                     description: !this.layoutDoc._keepZDragged ? 'Keep ZIndex when dragged' : 'Allow ZIndex to change when dragged',
-                    event: undoBatch(action(() => (this.layoutDoc._keepZWhenDragged = !this.layoutDoc._keepZWhenDragged))),
+                    event: undoBatch(
+                        action(() => {
+                            this.layoutDoc._keepZWhenDragged = !this.layoutDoc._keepZWhenDragged;
+                        })
+                    ),
                     icon: 'hand-point-up',
                 });
                 !zorders && cm.addItem({ description: 'Z Order...', addDivider: true, noexpand: true, subitems: zorderItems, icon: 'layer-group' });
@@ -597,7 +604,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
                 const existingOnClick = cm.findByDescription('OnClick...');
                 const onClicks: ContextMenuProps[] = existingOnClick && 'subitems' in existingOnClick ? existingOnClick.subitems : [];
 
-                onClicks.push({ description: 'Enter Portal', event: undoable(e => DocUtils.makeIntoPortal(this.Document, this.layoutDoc, this._allLinks), 'make into portal'), icon: 'window-restore' });
+                onClicks.push({ description: 'Enter Portal', event: undoable(() => DocUtils.makeIntoPortal(this.Document, this.layoutDoc, this._allLinks), 'make into portal'), icon: 'window-restore' });
                 !Doc.noviceMode && onClicks.push({ description: 'Toggle Detail', event: this.setToggleDetail, icon: 'concierge-bell' });
 
                 if (!this.Document.annotationOn) {
@@ -613,9 +620,9 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
 
             const funcs: ContextMenuProps[] = [];
             if (!Doc.noviceMode && this.layoutDoc.onDragStart) {
-                funcs.push({ description: 'Drag an Embedding', icon: 'edit', event: () => this.Document.dragFactory && (this.layoutDoc.onDragStart = ScriptField.MakeFunction('getEmbedding(this.dragFactory)')) });
-                funcs.push({ description: 'Drag a Copy', icon: 'edit', event: () => this.Document.dragFactory && (this.layoutDoc.onDragStart = ScriptField.MakeFunction('getCopy(this.dragFactory, true)')) });
-                funcs.push({ description: 'Drag Document', icon: 'edit', event: () => (this.layoutDoc.onDragStart = undefined) });
+                funcs.push({ description: 'Drag an Embedding', icon: 'edit', event: () => { this.Document.dragFactory && (this.layoutDoc.onDragStart = ScriptField.MakeFunction('getEmbedding(this.dragFactory)')); } }); // prettier-ignore
+                funcs.push({ description: 'Drag a Copy', icon: 'edit', event: () => { this.Document.dragFactory && (this.layoutDoc.onDragStart = ScriptField.MakeFunction('getCopy(this.dragFactory, true)')); } }); // prettier-ignore
+                funcs.push({ description: 'Drag Document', icon: 'edit', event: () => { this.layoutDoc.onDragStart = undefined; } }); // prettier-ignore
                 cm.addItem({ description: 'OnDrag...', noexpand: true, subitems: funcs, icon: 'asterisk' });
             }
 
@@ -624,14 +631,14 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
             if (!Doc.IsSystem(this.Document)) {
                 if (!Doc.noviceMode) {
                     moreItems.push({ description: 'Make View of Metadata Field', event: () => Doc.MakeMetadataFieldTemplate(this.Document, this._props.TemplateDataDocument), icon: 'concierge-bell' });
-                    moreItems.push({ description: `${this.Document._chromeHidden ? 'Show' : 'Hide'} Chrome`, event: () => (this.Document._chromeHidden = !this.Document._chromeHidden), icon: 'project-diagram' });
+                    moreItems.push({ description: `${this.Document._chromeHidden ? 'Show' : 'Hide'} Chrome`, event: () => { this.Document._chromeHidden = !this.Document._chromeHidden; }, icon: 'project-diagram' }); // prettier-ignore
 
                     if (Cast(Doc.GetProto(this.Document).data, listSpec(Doc))) {
                         moreItems.push({ description: 'Export to Google Photos Album', event: () => GooglePhotos.Export.CollectionToAlbum({ collection: this.Document }).then(console.log), icon: 'caret-square-right' });
                         moreItems.push({ description: 'Tag Child Images via Google Photos', event: () => GooglePhotos.Query.TagChildImages(this.Document), icon: 'caret-square-right' });
                         moreItems.push({ description: 'Write Back Link to Album', event: () => GooglePhotos.Transactions.AddTextEnrichment(this.Document), icon: 'caret-square-right' });
                     }
-                    moreItems.push({ description: 'Copy ID', event: () => Utils.CopyText(Doc.globalServerPath(this.Document)), icon: 'fingerprint' });
+                    moreItems.push({ description: 'Copy ID', event: () => ClientUtils.CopyText(Doc.globalServerPath(this.Document)), icon: 'fingerprint' });
                 }
             }
 
@@ -639,7 +646,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
         }
         const constantItems: ContextMenuProps[] = [];
         if (!Doc.IsSystem(this.Document) && this.Document._type_collection !== CollectionViewType.Docking) {
-            constantItems.push({ description: 'Zip Export', icon: 'download', event: async () => Doc.Zip(this.Document) });
+            constantItems.push({ description: 'Zip Export', icon: 'download', event: async () => DocUtils.Zip(this.Document) });
             (this.Document._type_collection !== CollectionViewType.Docking || !Doc.noviceMode) && constantItems.push({ description: 'Share', event: () => SharingManager.Instance.open(this._docView), icon: 'users' });
             if (this._props.removeDocument && Doc.ActiveDashboard !== this.Document) {
                 // need option to gray out menu items ... preferably with a '?' that explains why they're grayed out (eg., no permissions)
@@ -655,8 +662,8 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
         !Doc.noviceMode && helpItems.push({ description: 'Print Document in Console', event: () => console.log(this.Document), icon: 'hand-point-right' });
         !Doc.noviceMode && helpItems.push({ description: 'Print DataDoc in Console', event: () => console.log(this.dataDoc), icon: 'hand-point-right' });
 
-        let documentationDescription: string | undefined = undefined;
-        let documentationLink: string | undefined = undefined;
+        let documentationDescription: string | undefined;
+        let documentationLink: string | undefined;
         switch (this.Document.type) {
             case DocumentType.COL:
                 documentationDescription = 'See collection documentation';
@@ -690,6 +697,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
                 documentationDescription = 'See DataViz node documentation';
                 documentationLink = 'https://brown-dash.github.io/Dash-Documentation/documents/dataViz/';
                 break;
+            default:
         }
         // Add link to help documentation (unless the doc contents have been overriden in which case the documentation isn't relevant)
         if (!this.docContents && documentationDescription && documentationLink) {
@@ -710,8 +718,8 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
     panelHeight = () => this._props.PanelHeight() - this.headerMargin;
     screenToLocalContent = () => this._props.ScreenToLocalTransform().translate(0, -this.headerMargin);
     onClickFunc = this.disableClickScriptFunc ? undefined : () => this.onClickHandler;
-    setHeight = (height: number) => !this._props.suppressSetHeight && (this.layoutDoc._height = Math.min(NumCast(this.layoutDoc._maxHeight, Number.MAX_SAFE_INTEGER), height));
-    setContentView = action((view: ViewBoxInterface) => (this._componentView = view));
+    setHeight = (height: number) => { !this._props.suppressSetHeight && (this.layoutDoc._height = Math.min(NumCast(this.layoutDoc._maxHeight, Number.MAX_SAFE_INTEGER), height)); } // prettier-ignore
+    setContentView = action((view: ViewBoxInterface) => { this._componentView = view; }); // prettier-ignore
     isContentActive = (): boolean | undefined => this._isContentActive;
     childFilters = () => [...this._props.childFilters(), ...StrListCast(this.layoutDoc.childFilters)];
 
@@ -729,11 +737,13 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
                 const filtered = DocUtils.FilterDocs(this.directLinks, this._props.childFilters?.() ?? [], []).filter(d => d.link_displayLine || Doc.UserDoc().showLinkLines);
                 return filtered.some(link => link._link_displayArrow) ? 0 : undefined;
             }
+            default:
         }
         return this._props.styleProvider?.(doc, props, property);
     };
 
-    removeLinkByHiding = (link: Doc) => () => (link.link_displayLine = false);
+    // eslint-disable-next-line no-return-assign
+    removeLinkByHiding = (link: Doc) => () => link.link_displayLine = false; // prettier-ignore
     @computed get allLinkEndpoints() {
         // the small blue dots that mark the endpoints of links
         if (this._componentView instanceof KeyValueBox || this._props.hideLinkAnchors || this.layoutDoc.layout_hideLinkAnchors || this._props.dontRegisterView || this.layoutDoc.layout_unrendered) return null;
@@ -748,8 +758,8 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
                     PanelHeight={this.anchorPanelHeight}
                     dontRegisterView={false}
                     layout_showTitle={returnEmptyString}
-                    hideCaptions={true}
-                    hideLinkAnchors={true}
+                    hideCaptions
+                    hideLinkAnchors
                     layout_fitWidth={returnTrue}
                     removeDocument={this.removeLinkByHiding(link)}
                     styleProvider={this.anchorStyleProvider}
@@ -792,32 +802,30 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
     }
 
     captionStyleProvider = (doc: Opt<Doc>, props: Opt<FieldViewProps>, property: string) => this._props?.styleProvider?.(doc, props, property + ':caption');
-    fieldsDropdown = (placeholder: string) => {
-        return (
-            <div
-                ref={action((r: any) => r && (this._titleDropDownInnerWidth = DivWidth(r)))}
-                onPointerDown={action(e => (this._changingTitleField = true))}
-                style={{ width: 'max-content', background: SettingsManager.userBackgroundColor, color: SettingsManager.userColor, transformOrigin: 'left', transform: `scale(${this.titleHeight / 30 /* height of Dropdown */})` }}>
-                <FieldsDropdown
-                    Document={this.Document}
-                    placeholder={placeholder}
-                    selectFunc={action((field: string | number) => {
-                        if (this.layoutDoc.layout_showTitle) {
-                            this.layoutDoc._layout_showTitle = field;
-                        } else if (!this._props.layout_showTitle) {
-                            Doc.UserDoc().layout_showTitle = field;
-                        }
-                        this._changingTitleField = false;
-                    })}
-                    menuClose={action(() => (this._changingTitleField = false))}
-                />
-            </div>
-        );
-    };
+    fieldsDropdown = (placeholder: string) => (
+        <div
+            ref={action((r: any) => { r && (this._titleDropDownInnerWidth = DivWidth(r));} )} // prettier-ignore
+            onPointerDown={action(() => { this._changingTitleField = true; })} // prettier-ignore
+            style={{ width: 'max-content', background: SnappingManager.userBackgroundColor, color: SnappingManager.userColor, transformOrigin: 'left', transform: `scale(${this.titleHeight / 30 /* height of Dropdown */})` }}>
+            <FieldsDropdown
+                Document={this.Document}
+                placeholder={placeholder}
+                selectFunc={action((field: string | number) => {
+                    if (this.layoutDoc.layout_showTitle) {
+                        this.layoutDoc._layout_showTitle = field;
+                    } else if (!this._props.layout_showTitle) {
+                        Doc.UserDoc().layout_showTitle = field;
+                    }
+                    this._changingTitleField = false;
+                })}
+                menuClose={action(() => { this._changingTitleField = false; })} // prettier-ignore
+            />
+        </div>
+    );
     /**
      * 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[:hover]
-     **/
+     * */
     @computed get titleView() {
         const showTitle = this.layout_showTitle?.split(':')[0];
         const showTitleHover = this.layout_showTitle?.includes(':hover');
@@ -825,7 +833,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
         const targetDoc = showTitle?.startsWith('_') ? this.layoutDoc : this.Document;
         const background = StrCast(
             this.layoutDoc.layout_headingColor,
-            StrCast(SharingManager.Instance.users.find(u => u.user.email === this.dataDoc.author)?.sharingDoc.headingColor, StrCast(Doc.SharingDoc().headingColor, SettingsManager.userBackgroundColor))
+            StrCast(SharingManager.Instance.users.find(u => u.user.email === this.dataDoc.author)?.sharingDoc.headingColor, StrCast(Doc.SharingDoc().headingColor, SnappingManager.userBackgroundColor))
         );
         const dropdownWidth = this._titleRef.current?._editing || this._changingTitleField ? Math.max(10, (this._titleDropDownInnerWidth * this.titleHeight) / 30) : 0;
         const sidebarWidthPercent = +StrCast(this.layoutDoc.layout_sidebarWidthPercent).replace('%', '');
@@ -839,7 +847,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
                     position: this.headerMargin ? 'relative' : 'absolute',
                     height: this.titleHeight,
                     width: 100 - sidebarWidthPercent + '%',
-                    color: background === 'transparent' ? SettingsManager.userColor : lightOrDark(background),
+                    color: background === 'transparent' ? SnappingManager.userColor : lightOrDark(background),
                     background,
                     pointerEvents: (!this.disableClickScriptFunc && this.onClickHandler) || this.Document.ignoreClick ? 'none' : this.isContentActive() || this._props.isDocumentActive?.() ? 'all' : undefined,
                 }}>
@@ -860,11 +868,11 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
                         contents={
                             showTitle
                                 .split(';')
-                                .map(field => Field.toJavascriptString(this.Document[field] as Field))
+                                .map(field => Field.toJavascriptString(this.Document[field] as FieldType))
                                 .join(' \\ ') || '-unset-'
                         }
                         display="block"
-                        oneLine={true}
+                        oneLine
                         fontSize={(this.titleHeight / 15) * 10}
                         GetValue={() =>
                             showTitle
@@ -905,10 +913,10 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
                     xPadding={10}
                     fieldKey={this.layout_showCaption}
                     styleProvider={this.captionStyleProvider}
-                    dontRegisterView={true}
+                    dontRegisterView
                     rootSelected={this.rootSelected}
-                    noSidebar={true}
-                    dontScale={true}
+                    noSidebar
+                    dontScale
                     renderDepth={this._props.renderDepth}
                     isContentActive={this.isContentActive}
                 />
@@ -952,8 +960,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
 
     render() {
         TraceMobx();
-        const highlighting = this.highlighting;
-        const borderPath = this.borderPath;
+        const { highlighting, borderPath } = this;
         const boxShadow = !highlighting
             ? this.boxShadow
             : highlighting && this.borderRounding && highlighting.highlightStyle !== 'dashed'
@@ -968,23 +975,22 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
         });
 
         return (
+            // eslint-disable-next-line jsx-a11y/click-events-have-key-events
             <div
                 className={`${DocumentView.ROOT_DIV} docView-hack`}
                 ref={this._mainCont}
                 onContextMenu={this.onContextMenu}
                 onPointerDown={this.onPointerDown}
                 onClick={this.onClick}
-                onPointerEnter={e => (!SnappingManager.IsDragging || SnappingManager.CanEmbed) && Doc.BrushDoc(this.Document)}
-                onPointerOver={e => (!SnappingManager.IsDragging || SnappingManager.CanEmbed) && Doc.BrushDoc(this.Document)}
+                onPointerEnter={() => (!SnappingManager.IsDragging || SnappingManager.CanEmbed) && Doc.BrushDoc(this.Document)}
+                onPointerOver={() => (!SnappingManager.IsDragging || SnappingManager.CanEmbed) && Doc.BrushDoc(this.Document)}
                 onPointerLeave={e => !isParentOf(this._contentDiv, document.elementFromPoint(e.nativeEvent.x, e.nativeEvent.y)) && Doc.UnBrushDoc(this.Document)}
                 style={{
                     borderRadius: this.borderRounding,
                     pointerEvents: this._pointerEvents === 'visiblePainted' ? 'none' : this._pointerEvents, // visible painted means that the underlying doc contents are irregular and will process their own pointer events (otherwise, the contents are expected to fill the entire doc view box so we can handle pointer events here)
                 }}>
-                <>
-                    {this._componentView instanceof KeyValueBox ? renderDoc : DocumentViewInternal.AnimationEffect(renderDoc, this.Document[Animation], this.Document)}
-                    {borderPath?.jsx}
-                </>
+                {this._componentView instanceof KeyValueBox ? renderDoc : DocumentViewInternal.AnimationEffect(renderDoc, this.Document[Animation])}
+                {borderPath?.jsx}
             </div>
         );
     }
@@ -994,7 +1000,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
      * @param presEffectDoc presentation effects document that specifies the animation effect parameters
      * @returns a function that will wrap a JSX animation element wrapping any JSX element
      */
-    public static AnimationEffect(renderDoc: JSX.Element, presEffectDoc: Opt<Doc>, root: Doc) {
+    public static AnimationEffect(renderDoc: JSX.Element, presEffectDoc: Opt<Doc> /* , root: Doc */) {
         const dir = presEffectDoc?.presentation_effectDirection ?? presEffectDoc?.followLinkAnimDirection;
         const effectProps = {
             left: dir === PresEffectDirection.Left,
@@ -1005,10 +1011,8 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
             delay: 0,
             duration: Cast(presEffectDoc?.presentation_transition, 'number', Cast(presEffectDoc?.followLinkTransitionTime, 'number', null)),
         };
-        //prettier-ignore
+        // prettier-ignore
         switch (StrCast(presEffectDoc?.presentation_effect, StrCast(presEffectDoc?.followLinkAnimEffect))) {
-            default: 
-            case PresEffect.None:       return renderDoc;
             case PresEffect.Zoom:       return <Zoom {...effectProps}>{renderDoc}</Zoom>;
             case PresEffect.Fade:       return <Fade {...effectProps}>{renderDoc}</Fade>;
             case PresEffect.Flip:       return <Flip {...effectProps}>{renderDoc}</Flip>;
@@ -1016,17 +1020,19 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
             case PresEffect.Bounce:     return <Bounce {...effectProps}>{renderDoc}</Bounce>;
             case PresEffect.Roll:       return <Roll {...effectProps}>{renderDoc}</Roll>;
             case PresEffect.Lightspeed: return <JackInTheBox {...effectProps}>{renderDoc}</JackInTheBox>;
+            case PresEffect.None:       
+            default:                    return renderDoc;
         }
     }
     public static recordAudioAnnotation(dataDoc: Doc, field: string, onRecording?: (stop: () => void) => void, onEnd?: () => void) {
         let gumStream: any;
         let recorder: any;
-        navigator.mediaDevices.getUserMedia({ audio: true }).then(function (stream) {
+        navigator.mediaDevices.getUserMedia({ audio: true }).then(stream => {
             let audioTextAnnos = Cast(dataDoc[field + '_audioAnnotations_text'], listSpec('string'), null);
             if (audioTextAnnos) audioTextAnnos.push('');
             else audioTextAnnos = dataDoc[field + '_audioAnnotations_text'] = new List<string>(['']);
             DictationManager.Controls.listen({
-                interimHandler: value => (audioTextAnnos[audioTextAnnos.length - 1] = value),
+                interimHandler: value => { audioTextAnnos[audioTextAnnos.length - 1] = value; }, // prettier-ignore
                 continuous: { indefinite: false },
             }).then(results => {
                 if (results && [DictationManager.Controls.Infringed].includes(results)) {
@@ -1060,9 +1066,9 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
 }
 
 @observer
-export class DocumentView extends DocComponent<DocumentViewProps>() {
+export class DocumentView extends DocComponent<DocumentViewProps & { CollectionFreeFormDocumentView?: () => CollectionFreeFormDocumentView }>() {
     public static ROOT_DIV = 'documentView-effectsWrapper';
-    public get displayName() {  return 'DocumentView(' + this.Document?.title + ')';  } // prettier-ignore
+    public get displayName() {  return 'DocumentView(' + (this.Document?.title??"") + ')';  } // prettier-ignore
     public ContentRef = React.createRef<HTMLDivElement>();
     private _htmlOverlayEffect: Opt<Doc>;
     private _disposers: { [name: string]: IReactionDisposer } = {};
@@ -1084,7 +1090,7 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
         return () => (SnappingManager.ExploreMode ? ScriptField.MakeScript('CollectionBrowseClick(documentView, clientX, clientY)', { documentView: 'any', clientX: 'number', clientY: 'number' })! : undefined);
     }
 
-    constructor(props: DocumentViewProps) {
+    constructor(props: DocumentViewProps & { CollectionFreeFormDocumentView?: () => CollectionFreeFormDocumentView }) {
         super(props);
         makeObservable(this);
     }
@@ -1161,7 +1167,7 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
         !BoolCast(this.Document.dontRegisterView, this._props.dontRegisterView) && DocumentManager.Instance.RemoveView(this);
     }
 
-    public set IsSelected(val)  { runInAction(() => (this._selected = val)); } // prettier-ignore
+    public set IsSelected(val)  { runInAction(() => { this._selected = val; }); } // prettier-ignore
     public get IsSelected()     { return this._selected; } // prettier-ignore
     public get topMost()        { return this._props.renderDepth === 0; } // prettier-ignore
     public get ContentDiv()     { return this._docViewInternal?._contentDiv; } // prettier-ignore
@@ -1176,7 +1182,7 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
         return this._props.layout_fitWidth?.(this.layoutDoc) ?? this.layoutDoc?.layout_fitWidth;
     }
     @computed get anchorViewDoc() {
-        return this._props.LayoutTemplateString?.includes('link_anchor_2') ? DocCast(this.Document['link_anchor_2']) : this._props.LayoutTemplateString?.includes('link_anchor_1') ? DocCast(this.Document['link_anchor_1']) : undefined;
+        return this._props.LayoutTemplateString?.includes('link_anchor_2') ? DocCast(this.Document.link_anchor_2) : this._props.LayoutTemplateString?.includes('link_anchor_1') ? DocCast(this.Document.link_anchor_1) : undefined;
     }
 
     @computed get getBounds(): Opt<{ left: number; top: number; right: number; bottom: number; transition?: string }> {
@@ -1213,6 +1219,7 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
     public get containerViewPath() { return this._props.containerViewPath; } // prettier-ignore
     public get CollectionFreeFormView() { return this.CollectionFreeFormDocumentView?.CollectionFreeFormView; } // prettier-ignore
     public get CollectionFreeFormDocumentView() { return this._props.CollectionFreeFormDocumentView?.(); } // prettier-ignore
+    public get LocalRotation() { return this._props.LocalRotation?.(); } // prettier-ignore
 
     public clearViewTransition = () => {
         this._viewTimer && clearTimeout(this._viewTimer);
@@ -1231,18 +1238,18 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
     public iconify(finished?: () => void, animateTime?: number) {
         this.ComponentView?.updateIcon?.();
         const animTime = this._docViewInternal?.animateScaleTime();
-        runInAction(() => this._docViewInternal && animateTime !== undefined && (this._docViewInternal._animateScaleTime = animateTime));
+        runInAction(() => { this._docViewInternal && animateTime !== undefined && (this._docViewInternal._animateScaleTime = animateTime); }); // prettier-ignore
         const finalFinished = action(() => {
             finished?.();
             this._docViewInternal && (this._docViewInternal._animateScaleTime = animTime);
         });
-        const layout_fieldKey = Cast(this.Document.layout_fieldKey, 'string', null);
-        if (layout_fieldKey !== 'layout_icon') {
+        const layoutFieldKey = Cast(this.Document.layout_fieldKey, 'string', null);
+        if (layoutFieldKey !== 'layout_icon') {
             this.switchViews(true, 'icon', finalFinished);
-            if (layout_fieldKey && layout_fieldKey !== 'layout' && layout_fieldKey !== 'layout_icon') this.Document.deiconifyLayout = layout_fieldKey.replace('layout_', '');
+            if (layoutFieldKey && layoutFieldKey !== 'layout' && layoutFieldKey !== 'layout_icon') this.Document.deiconifyLayout = layoutFieldKey.replace('layout_', '');
         } else {
             const deiconifyLayout = Cast(this.Document.deiconifyLayout, 'string', null);
-            this.switchViews(deiconifyLayout ? true : false, deiconifyLayout, finalFinished, true);
+            this.switchViews(!!deiconifyLayout, deiconifyLayout, finalFinished, true);
             this.Document.deiconifyLayout = undefined;
             this._props.bringToFront?.(this.Document);
         }
@@ -1262,7 +1269,7 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
                         autoplay: true,
                         loop: false,
                         volume: 0.5,
-                        onend: action(() => (self.dataDoc.audioAnnoState = AudioAnnoState.stopped)),
+                        onend: action(() => { self.dataDoc.audioAnnoState = AudioAnnoState.stopped; }), // prettier-ignore
                     });
                     this.dataDoc.audioAnnoState = AudioAnnoState.playing;
                     break;
@@ -1270,6 +1277,7 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
                     this.dataDoc[AudioPlay]?.stop();
                     this.dataDoc.audioAnnoState = AudioAnnoState.stopped;
                     break;
+                default:
             }
         }
     };
@@ -1284,10 +1292,10 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
             this._docViewInternal._animateScaleTime = time;
         }
     });
-    public setAnimEffect = (presEffect: Doc, timeInMs: number, afterTrans?: () => void) => {
+    public setAnimEffect = (presEffect: Doc, timeInMs: number /* , afterTrans?: () => void */) => {
         this._animEffectTimer && clearTimeout(this._animEffectTimer);
         this.Document[Animation] = presEffect;
-        this._animEffectTimer = setTimeout(() => (this.Document[Animation] = undefined), timeInMs);
+        this._animEffectTimer = setTimeout(() => { this.Document[Animation] = undefined; }, timeInMs); // prettier-ignore
     };
     public setViewTransition = (transProp: string, timeInMs: number, afterTrans?: () => void, dataTrans = false) => {
         this._viewTimer = DocumentView.SetViewTransition([this.layoutDoc], transProp, timeInMs, this._viewTimer, afterTrans, dataTrans);
@@ -1304,7 +1312,7 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
         }
         const view = SelectionManager.Views[0]?._props.renderDepth > 0 ? SelectionManager.Views[0] : undefined;
         undoable(() => {
-            var tempDoc: Opt<Doc>;
+            let tempDoc: Opt<Doc>;
             if (view) {
                 if (!view.layoutDoc.isTemplateDoc) {
                     tempDoc = view.Document;
@@ -1322,6 +1330,7 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
             }
             Doc.UserDoc().defaultTextLayout = tempDoc ? new PrefetchProxy(tempDoc) : undefined;
         }, 'set default template')();
+        return undefined;
     }
 
     /**
@@ -1335,12 +1344,13 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
         const curLayout = StrCast(this.Document.layout_fieldKey).replace('layout_', '').replace('layout', '');
         if (!this.Document.layout_default && curLayout !== detailLayoutKeySuffix) this.Document.layout_default = curLayout;
         const defaultLayout = StrCast(this.Document.layout_default);
-        if (this.Document.layout_fieldKey === 'layout_' + detailLayoutKeySuffix) this.switchViews(defaultLayout ? true : false, defaultLayout, undefined, true);
+        if (this.Document.layout_fieldKey === 'layout_' + detailLayoutKeySuffix) this.switchViews(!!defaultLayout, defaultLayout, undefined, true);
         else this.switchViews(true, detailLayoutKeySuffix, undefined, true);
     };
     public switchViews = (custom: boolean, view: string, finished?: () => void, useExistingLayout = false) => {
         const batch = UndoManager.StartBatch('switchView:' + view);
-        runInAction(() => this._docViewInternal && (this._docViewInternal._animateScalingTo = 0.1)); // shrink doc
+        // shrink doc first..
+        runInAction(() => { this._docViewInternal && (this._docViewInternal._animateScalingTo = 0.1); }); // prettier-ignore
         setTimeout(
             action(() => {
                 if (useExistingLayout && custom && this.Document['layout_' + view]) {
@@ -1348,7 +1358,7 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
                 } else {
                     this.setCustomView(custom, view);
                 }
-                this._docViewInternal && (this._docViewInternal._animateScalingTo = 1); // expand it
+                this._docViewInternal && (this._docViewInternal._animateScalingTo = 1); // now expand it
                 setTimeout(
                     action(() => {
                         this._docViewInternal && (this._docViewInternal._animateScalingTo = 0);
@@ -1366,7 +1376,7 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
      */
     public docViewPath = () => (this.containerViewPath ? [...this.containerViewPath(), this] : [this]);
 
-    layout_fitWidthFunc = (doc: Doc) => BoolCast(this.layout_fitWidth);
+    layout_fitWidthFunc = (/* doc: Doc */) => BoolCast(this.layout_fitWidth);
     screenToLocalScale = () => this._props.ScreenToLocalTransform().Scale;
     isSelected = () => this.IsSelected;
     select = (extendSelection: boolean, focusSelection?: boolean) => {
@@ -1390,7 +1400,7 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
     PanelWidth = () => this.panelWidth;
     PanelHeight = () => this.panelHeight;
     NativeDimScaling = () => this.nativeScaling;
-    hideLinkCount = () => (this.hideLinkButton ? true : false);
+    hideLinkCount = () => !!this.hideLinkButton;
     selfView = () => this;
     /**
      * @returns Transform to the document view (in the coordinate system of whatever contains the DocumentView)
@@ -1413,17 +1423,16 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
                 ref={r => {
                     const val = r?.style.display !== 'none'; // if the outer overlay has been displayed, trigger the innner div to start it's opacity fade in transition
                     if (r && val !== this._enableHtmlOverlayTransitions) {
-                        setTimeout(action(() => (this._enableHtmlOverlayTransitions = val)));
+                        setTimeout(action(() => { this._enableHtmlOverlayTransitions = val; })); // prettier-ignore
                     }
                 }}
                 style={{ display: !this._htmlOverlayText ? 'none' : undefined }}>
                 <div className="documentView-htmlOverlayInner" style={{ transition: `all 500ms`, opacity: this._enableHtmlOverlayTransitions ? 0.9 : 0 }}>
                     {DocumentViewInternal.AnimationEffect(
                         <div className="webBox-textHighlight">
-                            <ObserverJsxParser autoCloseVoidElements={true} key={42} onError={(e: any) => console.log('PARSE error', e)} renderInWrapper={false} jsx={StrCast(this._htmlOverlayText)} />
+                            <ObserverJsxParser autoCloseVoidElements key={42} onError={(e: any) => console.log('PARSE error', e)} renderInWrapper={false} jsx={StrCast(this._htmlOverlayText)} />
                         </div>,
-                        { ...(this._htmlOverlayEffect ?? {}), presentation_effect: effect ?? PresEffect.Zoom } as any as Doc,
-                        this.Document
+                        { ...(this._htmlOverlayEffect ?? {}), presentation_effect: effect ?? PresEffect.Zoom } as any as Doc
                     )}
                 </div>
             </div>
@@ -1436,7 +1445,15 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
         const yshift = Math.abs(this.Yshift) <= 0.001 ? this._props.PanelHeight() : undefined;
 
         return (
-            <div id={this.ViewGuid} 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"
@@ -1462,14 +1479,16 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
                             layout_fitWidth={this.layout_fitWidthFunc}
                             ScreenToLocalTransform={this.screenToContentsTransform}
                             focus={this._props.focus || emptyFunction}
-                            ref={action((r: DocumentViewInternal | null) => r && (this._docViewInternal = r))}
+                            ref={action((r: DocumentViewInternal | null) => {
+                                r && (this._docViewInternal = r);
+                            })}
                         />
                         {this.htmlOverlay()}
                         {this.ComponentView?.infoUI?.()}
                     </div>
                 )}
                 {/* display link count button */}
-                <DocumentLinksButton hideCount={this.hideLinkCount} View={this} scaling={this.screenToLocalScale} OnHover={true} Bottom={this.topMost} ShowCount={true} />
+                <DocumentLinksButton hideCount={this.hideLinkCount} View={this} scaling={this.screenToLocalScale} OnHover Bottom={this.topMost} ShowCount />
             </div>
         );
     }
@@ -1493,7 +1512,7 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
 
     // shows a stacking view collection (by default, but the user can change) of all documents linked to the source
     public static showBackLinks(linkAnchor: Doc) {
-        const docId = Doc.CurrentUserEmail + Doc.GetProto(linkAnchor)[Id] + '-pivotish';
+        const docId = ClientUtils.CurrentUserEmail + Doc.GetProto(linkAnchor)[Id] + '-pivotish';
         // prettier-ignore
         DocServer.GetRefField(docId).then(docx =>
             LightboxView.Instance.SetLightboxDoc(
@@ -1504,19 +1523,27 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
     }
 }
 
+export function returnEmptyDocViewList() {
+    return emptyPath;
+}
+
+// eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function deiconifyView(documentView: DocumentView) {
     documentView.iconify();
     documentView.select(false);
 });
 
+// eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function deiconifyViewToLightbox(documentView: DocumentView) {
-    LightboxView.Instance.AddDocTab(documentView.Document, OpenWhere.lightbox, 'layout'); //, 0);
+    LightboxView.Instance.AddDocTab(documentView.Document, OpenWhere.lightbox, 'layout'); // , 0);
 });
 
+// eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function toggleDetail(dv: DocumentView, detailLayoutKeySuffix: string) {
     dv.toggleDetail(detailLayoutKeySuffix);
 });
 
+// eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function updateLinkCollection(linkCollection: Doc, linkSource: Doc) {
     const collectedLinks = DocListCast(linkCollection[DocData].data);
     let wid = NumCast(linkSource._width);
@@ -1534,9 +1561,10 @@ ScriptingGlobals.add(function updateLinkCollection(linkCollection: Doc, linkSour
             Doc.AddDocToList(Doc.GetProto(linkCollection), 'data', embedding);
         }
     });
-    embedding && DocServer.UPDATE_SERVER_CACHE(); // if a new embedding was made, update the client's server cache so that it will not come back as a promise
+    embedding && UPDATE_SERVER_CACHE(); // if a new embedding was made, update the client's server cache so that it will not come back as a promise
     return links;
 });
+// eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function updateTagsCollection(collection: Doc) {
     const tag = StrCast(collection.title).split('-->')[1];
     const matchedTags = Array.from(SearchUtil.SearchCollection(Doc.MyFilesystem, tag, false, ['tags']).keys());
diff --git a/src/client/views/nodes/EquationBox.tsx b/src/client/views/nodes/EquationBox.tsx
index a557cff4f..9be66ba4a 100644
--- a/src/client/views/nodes/EquationBox.tsx
+++ b/src/client/views/nodes/EquationBox.tsx
@@ -1,7 +1,7 @@
 import { action, makeObservable, reaction } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
-import { DivHeight, DivWidth } from '../../../Utils';
+import { DivHeight, DivWidth } from '../../../ClientUtils';
 import { Id } from '../../../fields/FieldSymbols';
 import { NumCast, StrCast } from '../../../fields/Types';
 import { TraceMobx } from '../../../fields/util';
diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx
index 771856788..14454ff61 100644
--- a/src/client/views/nodes/FieldView.tsx
+++ b/src/client/views/nodes/FieldView.tsx
@@ -1,3 +1,4 @@
+import { computed } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
 import { DateField } from '../../../fields/DateField';
@@ -5,13 +6,10 @@ import { Doc, Field, Opt } from '../../../fields/Doc';
 import { List } from '../../../fields/List';
 import { ScriptField } from '../../../fields/ScriptField';
 import { WebField } from '../../../fields/URLField';
-import { dropActionType } from '../../util/DragManager';
+import { dropActionType } from '../../util/DropActionTypes';
 import { Transform } from '../../util/Transform';
-import { ViewBoxInterface } from '../DocComponent';
-import { CollectionFreeFormDocumentView } from './CollectionFreeFormDocumentView';
+import { PinProps, ViewBoxInterface } from '../DocComponent';
 import { DocumentView, OpenWhere } from './DocumentView';
-import { PinProps } from './trails';
-import { computed } from 'mobx';
 
 export interface FocusViewOptions {
     willPan?: boolean; // determines whether to pan to target document
@@ -56,7 +54,7 @@ export interface FieldViewSharedProps {
     disableBrushing?: boolean; // should highlighting for this view be disabled when same document in another view is hovered over.
     hideClickBehaviors?: boolean; // whether to suppress menu item options for changing click behaviors
     ignoreUsePath?: boolean; // ignore the usePath field for selecting the fieldKey (eg., on text docs)
-    CollectionFreeFormDocumentView?: () => CollectionFreeFormDocumentView;
+    LocalRotation?: () => number | undefined; // amount of rotation applied to freeformdocumentview containing document view
     containerViewPath?: () => DocumentView[];
     fitContentsToBox?: () => boolean; // used by freeformview to fit its contents to its panel. corresponds to _freeform_fitContentsToBox property on a Document
     isGroupActive?: () => string | undefined; // is this document part of a group that is active
diff --git a/src/client/views/nodes/FontIconBox/FontIconBox.tsx b/src/client/views/nodes/FontIconBox/FontIconBox.tsx
index 57ae92359..70fc63115 100644
--- a/src/client/views/nodes/FontIconBox/FontIconBox.tsx
+++ b/src/client/views/nodes/FontIconBox/FontIconBox.tsx
@@ -6,7 +6,8 @@ import { observer } from 'mobx-react';
 import * as React from 'react';
 import { Doc, DocListCast, StrListCast } from '../../../../fields/Doc';
 import { BoolCast, DocCast, NumCast, ScriptCast, StrCast } from '../../../../fields/Types';
-import { emptyFunction, returnTrue, setupMoveUpEvents, Utils } from '../../../../Utils';
+import { emptyFunction, Utils } from '../../../../Utils';
+import { ClientUtils, returnTrue, setupMoveUpEvents } from '../../../../ClientUtils';
 import { CollectionViewType, DocumentType } from '../../../documents/DocumentTypes';
 import { SelectionManager } from '../../../util/SelectionManager';
 import { SettingsManager } from '../../../util/SettingsManager';
@@ -183,7 +184,7 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() {
                     if (selected.length > 1) {
                         text = selected.length + ' selected';
                     } else {
-                        text = Utils.cleanDocumentType(StrCast(selected.lastElement().type) as DocumentType);
+                        text = ClientUtils.cleanDocumentType(StrCast(selected.lastElement().type) as DocumentType);
                         icon = Doc.toIcon(selected.lastElement());
                     }
                     return (
diff --git a/src/client/views/nodes/FunctionPlotBox.tsx b/src/client/views/nodes/FunctionPlotBox.tsx
index a86bdbd79..180c651fb 100644
--- a/src/client/views/nodes/FunctionPlotBox.tsx
+++ b/src/client/views/nodes/FunctionPlotBox.tsx
@@ -2,18 +2,18 @@ import functionPlot from 'function-plot';
 import { computed, makeObservable, reaction } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
-import { Doc, DocListCast } from '../../../fields/Doc';
+import { DocListCast } from '../../../fields/Doc';
 import { List } from '../../../fields/List';
 import { listSpec } from '../../../fields/Schema';
 import { Cast, StrCast } from '../../../fields/Types';
 import { TraceMobx } from '../../../fields/util';
 import { DocUtils, Docs } from '../../documents/Documents';
 import { DragManager } from '../../util/DragManager';
+import { LinkManager } from '../../util/LinkManager';
 import { undoBatch } from '../../util/UndoManager';
-import { ViewBoxAnnotatableComponent } from '../DocComponent';
+import { PinProps, ViewBoxAnnotatableComponent } from '../DocComponent';
 import { FieldView, FieldViewProps } from './FieldView';
-import { PinProps, PresBox } from './trails';
-import { LinkManager } from '../../util/LinkManager';
+import { PresBox } from './trails';
 
 @observer
 export class FunctionPlotBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
@@ -89,7 +89,7 @@ export class FunctionPlotBox extends ViewBoxAnnotatableComponent<FieldViewProps>
     drop = (e: Event, de: DragManager.DropEvent) => {
         if (de.complete.docDragData?.droppedDocuments.length) {
             const added = de.complete.docDragData.droppedDocuments.reduce((res, doc) => {
-                ///const ret = res && Doc.AddDocToList(this.dataDoc, this._props.fieldKey, 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);
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index bb1f70f97..231300a65 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -1,10 +1,11 @@
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { Tooltip } from '@mui/material';
 import { Colors } from 'browndash-components';
-import { action, computed, IReactionDisposer, makeObservable, observable, ObservableMap, reaction, runInAction } from 'mobx';
+import { action, computed, IReactionDisposer, makeObservable, observable, ObservableMap, reaction } from 'mobx';
 import { observer } from 'mobx-react';
 import { extname } from 'path';
 import * as React from 'react';
+import { ClientUtils, DashColor, returnEmptyString, returnFalse, returnOne, returnZero, setupMoveUpEvents } from '../../../ClientUtils';
 import { Doc, Opt } from '../../../fields/Doc';
 import { DocData } from '../../../fields/DocSymbols';
 import { Id } from '../../../fields/FieldSymbols';
@@ -14,7 +15,7 @@ import { ObjectField } from '../../../fields/ObjectField';
 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';
+import { emptyFunction } from '../../../Utils';
 import { Docs, DocUtils } from '../../documents/Documents';
 import { DocumentType } from '../../documents/DocumentTypes';
 import { Networking } from '../../Network';
@@ -24,7 +25,7 @@ import { undoBatch } from '../../util/UndoManager';
 import { ContextMenu } from '../../views/ContextMenu';
 import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView';
 import { ContextMenuProps } from '../ContextMenuItem';
-import { ViewBoxAnnotatableComponent, ViewBoxInterface } from '../DocComponent';
+import { PinProps, ViewBoxAnnotatableComponent, ViewBoxInterface } from '../DocComponent';
 import { MarqueeAnnotator } from '../MarqueeAnnotator';
 import { OverlayView } from '../OverlayView';
 import { AnchorMenu } from '../pdf/AnchorMenu';
@@ -32,26 +33,29 @@ import { StyleProp } from '../StyleProvider';
 import { OpenWhere } from './DocumentView';
 import { FieldView, FieldViewProps, FocusViewOptions } from './FieldView';
 import './ImageBox.scss';
-import { PinProps, PresBox } from './trails';
+import { PresBox } from './trails';
 
 export class ImageEditorData {
+    // eslint-disable-next-line no-use-before-define
     private static _instance: ImageEditorData;
     private static get imageData() { return (ImageEditorData._instance ?? new ImageEditorData()).imageData; } // prettier-ignore
     @observable imageData: { rootDoc: Doc | undefined; open: boolean; source: string; addDoc: Opt<(doc: Doc | Doc[], annotationKey?: string) => boolean> } = observable({ rootDoc: undefined, open: false, source: '', addDoc: undefined });
-    @action private static set = (open: boolean, rootDoc: Doc | undefined, source: string, addDoc: Opt<(doc: Doc | Doc[], annotationKey?: string) => boolean>) => (this._instance.imageData = { open, rootDoc, source, addDoc });
+    @action private static set = (open: boolean, rootDoc: Doc | undefined, source: string, addDoc: Opt<(doc: Doc | Doc[], annotationKey?: string) => boolean>) => {
+        this._instance.imageData = { open, rootDoc, source, addDoc };
+    };
 
     constructor() {
         makeObservable(this);
         ImageEditorData._instance = this;
     }
 
-    public static get Open()    { return ImageEditorData.imageData.open;  } // prettier-ignore
-    public static get Source()  { return ImageEditorData.imageData.source; } // prettier-ignore
-    public static get RootDoc() { return ImageEditorData.imageData.rootDoc; } // prettier-ignore
-    public static get AddDoc()  { return ImageEditorData.imageData.addDoc; } // prettier-ignore
+    public static get Open()                     { return ImageEditorData.imageData.open;  } // prettier-ignore
     public static set Open(open: boolean)        { ImageEditorData.set(open, this.imageData.rootDoc, this.imageData.source, this.imageData.addDoc); } // prettier-ignore
+    public static get Source()                   { return ImageEditorData.imageData.source; } // prettier-ignore
     public static set Source(source: string)     { ImageEditorData.set(this.imageData.open, this.imageData.rootDoc, source, this.imageData.addDoc); } // prettier-ignore
+    public static get RootDoc()                  { return ImageEditorData.imageData.rootDoc; } // prettier-ignore
     public static set RootDoc(rootDoc: Opt<Doc>) { ImageEditorData.set(this.imageData.open, rootDoc, this.imageData.source, this.imageData.addDoc); } // prettier-ignore
+    public static get AddDoc()                   { return ImageEditorData.imageData.addDoc; } // prettier-ignore
     public static set AddDoc(addDoc: Opt<(doc: Doc | Doc[], annotationKey?: string) => boolean>) { ImageEditorData.set(this.imageData.open, this.imageData.rootDoc, this.imageData.source, addDoc); } // prettier-ignore
 }
 @observer
@@ -93,7 +97,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl
         if (anchor) {
             if (!addAsAnnotation) anchor.backgroundColor = 'transparent';
             addAsAnnotation && this.addDocument(anchor);
-            PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), pannable: visibleAnchor ? false : true } }, this.Document);
+            PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), pannable: !visibleAnchor } }, this.Document);
             return anchor;
         }
         return this.Document;
@@ -106,10 +110,12 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl
                 scrSize: (this.ScreenToLocalBoxXf().inverse().transformDirection(this.nativeSize.nativeWidth, this.nativeSize.nativeHeight)[0] / this.nativeSize.nativeWidth) * NumCast(this.layoutDoc._freeform_scale, 1),
                 selected: this._props.isSelected(),
             }),
-            ({ forceFull, scrSize, selected }) => (this._curSuffix = selected ? '_o' : this.fieldKey === 'icon' ? '_m' : forceFull ? '_o' : scrSize < 0.25 ? '_s' : scrSize < 0.5 ? '_m' : scrSize < 0.8 ? '_l' : '_o'),
+            ({ forceFull, scrSize, selected }) => {
+                this._curSuffix = selected ? '_o' : this.fieldKey === 'icon' ? '_m' : forceFull ? '_o' : scrSize < 0.25 ? '_s' : scrSize < 0.5 ? '_m' : scrSize < 0.8 ? '_l' : '_o';
+            },
             { fireImmediately: true, delay: 1000 }
         );
-        const layoutDoc = this.layoutDoc;
+        const { layoutDoc } = this;
         this._disposers.path = reaction(
             () => ({ nativeSize: this.nativeSize, width: NumCast(this.layoutDoc._width) }),
             ({ nativeSize, width }) => {
@@ -121,10 +127,10 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl
         );
         this._disposers.scroll = reaction(
             () => this.layoutDoc.layout_scrollTop,
-            s_top => {
+            sTop => {
                 this._forcedScroll = true;
-                !this._ignoreScroll && this._mainCont.current && (this._mainCont.current.scrollTop = NumCast(s_top));
-                this._mainCont.current?.scrollTo({ top: NumCast(s_top) });
+                !this._ignoreScroll && this._mainCont.current && (this._mainCont.current.scrollTop = NumCast(sTop));
+                this._mainCont.current?.scrollTo({ top: NumCast(sTop) });
                 this._forcedScroll = false;
             },
             { fireImmediately: true }
@@ -138,7 +144,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl
     @undoBatch
     drop = (e: Event, de: DragManager.DropEvent) => {
         if (de.complete.docDragData) {
-            let added: boolean | undefined = undefined;
+            let added: boolean | undefined;
             const targetIsBullseye = (ele: HTMLElement): boolean => {
                 if (!ele) return false;
                 if (ele === this._overlayIconRef.current) return true;
@@ -168,7 +174,9 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl
     };
 
     @undoBatch
-    resolution = () => (this.layoutDoc._showFullRes = !this.layoutDoc._showFullRes);
+    resolution = () => {
+        this.layoutDoc._showFullRes = !this.layoutDoc._showFullRes;
+    };
 
     @undoBatch
     setNativeSize = action(() => {
@@ -189,7 +197,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl
         const nh = NumCast(this.dataDoc[this.fieldKey + '_nativeHeight']);
         const w = this.layoutDoc._width;
         const h = this.layoutDoc._height;
-        this.dataDoc[this.fieldKey + '-rotation'] = (NumCast(this.dataDoc[this.fieldKey + '-rotation']) + 90) % 360;
+        this.dataDoc[this.fieldKey + '_rotation'] = (NumCast(this.dataDoc[this.fieldKey + '_rotation']) + 90) % 360;
         this.dataDoc[this.fieldKey + '_nativeWidth'] = nh;
         this.dataDoc[this.fieldKey + '_nativeHeight'] = nw;
         this.layoutDoc._width = h;
@@ -197,7 +205,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl
     });
 
     crop = (region: Doc | undefined, addCrop?: boolean) => {
-        if (!region) return;
+        if (!region) return undefined;
         const cropping = Doc.MakeCopy(region, true);
         const regionData = region[DocData];
         regionData.lockedPosition = true;
@@ -223,8 +231,8 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl
         croppingProto.type = DocumentType.IMG;
         croppingProto.layout = ImageBox.LayoutString('data');
         croppingProto.data = ObjectField.MakeCopy(this.dataDoc[this.fieldKey] as ObjectField);
-        croppingProto['data_nativeWidth'] = anchw;
-        croppingProto['data_nativeHeight'] = anchh;
+        croppingProto.data_nativeWidth = anchw;
+        croppingProto.data_nativeHeight = anchh;
         croppingProto.freeform_scale = viewScale;
         croppingProto.freeform_scale_min = viewScale;
         croppingProto.freeform_panX = anchx / viewScale;
@@ -244,14 +252,14 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl
         return cropping;
     };
 
-    specificContextMenu = (e: React.MouseEvent): void => {
+    specificContextMenu = (): void => {
         const field = Cast(this.dataDoc[this.fieldKey], ImageField);
         if (field) {
             const funcs: ContextMenuProps[] = [];
             funcs.push({ description: 'Rotate Clockwise 90', event: this.rotate, icon: 'redo-alt' });
             funcs.push({ description: `Show ${this.layoutDoc._showFullRes ? 'Dynamic Res' : 'Full Res'}`, event: this.resolution, icon: 'expand' });
             funcs.push({ description: 'Set Native Pixel Size', event: this.setNativeSize, icon: 'expand-arrows-alt' });
-            funcs.push({ description: 'Copy path', event: () => Utils.CopyText(this.choosePath(field.url)), icon: 'copy' });
+            funcs.push({ description: 'Copy path', event: () => ClientUtils.CopyText(this.choosePath(field.url)), icon: 'copy' });
             funcs.push({
                 description: 'Open Image Editor',
                 event: action(() => {
@@ -270,7 +278,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl
         if (!url?.href) return '';
         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 (url.href.indexOf(window.location.origin) === -1 && url.href.indexOf('dashblobstore') === -1) return ClientUtils.CorsProxy(url.href);
         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);
@@ -282,7 +290,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl
         TraceMobx();
         const nativeWidth = NumCast(this.dataDoc[this.fieldKey + '_nativeWidth'], NumCast(this.layoutDoc[this.fieldKey + '_nativeWidth'], 500));
         const nativeHeight = NumCast(this.dataDoc[this.fieldKey + '_nativeHeight'], NumCast(this.layoutDoc[this.fieldKey + '_nativeHeight'], 500));
-        const nativeOrientation = NumCast(this.dataDoc[this.fieldKey + '-nativeOrientation'], 1);
+        const nativeOrientation = NumCast(this.dataDoc[this.fieldKey + '_nativeOrientation'], 1);
         return { nativeWidth, nativeHeight, nativeOrientation };
     }
     @computed get overlayImageIcon() {
@@ -307,7 +315,11 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl
                 <div
                     className="imageBox-alternateDropTarget"
                     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))}
+                    onPointerDown={e =>
+                        setupMoveUpEvents(e.target, e, returnFalse, emptyFunction, () => {
+                            this.layoutDoc[`_${this.fieldKey}_usePath`] = usePath === undefined ? 'alternate' : usePath === 'alternate' ? 'alternate:hover' : undefined;
+                        })
+                    }
                     style={{
                         display: (this._props.isContentActive() !== false && DragManager.DocDragData?.canEmbed) || this.dataDoc[this.fieldKey + '_alternates'] ? 'block' : 'none',
                         width: 'min(10%, 25px)',
@@ -324,7 +336,7 @@ 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 = 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 defaultUrl = new URL(ClientUtils.prepend('/assets/unknown-file-icon-hi.png'));
         const altpaths =
             alts
                 ?.map(doc => (doc instanceof Doc ? ImageCast(doc[Doc.LayoutFieldKey(doc)])?.url ?? defaultUrl : defaultUrl))
@@ -344,8 +356,8 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl
         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();
-        const { nativeWidth, nativeHeight, nativeOrientation } = this.nativeSize;
-        const rotation = NumCast(this.dataDoc[this.fieldKey + '-rotation']);
+        const { nativeWidth, nativeHeight /* , nativeOrientation */ } = this.nativeSize;
+        const rotation = NumCast(this.dataDoc[this.fieldKey + '_rotation']);
         const aspect = rotation % 180 ? nativeHeight / nativeWidth : 1;
         let transformOrigin = 'center center';
         let transform = `translate(0%, 0%) rotate(${rotation}deg) scale(${aspect})`;
@@ -361,12 +373,32 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl
         const usePath = this.layoutDoc[`_${this.fieldKey}_usePath`];
 
         return (
-            <div className="imageBox-cont" onPointerEnter={action(() => (this._isHovering = true))} onPointerLeave={action(() => (this._isHovering = false))} key={this.layoutDoc[Id]} ref={this.createDropTarget} onPointerDown={this.marqueeDown}>
+            <div
+                className="imageBox-cont"
+                onPointerEnter={action(() => {
+                    this._isHovering = true;
+                })}
+                onPointerLeave={action(() => {
+                    this._isHovering = false;
+                })}
+                key={this.layoutDoc[Id]}
+                ref={this.createDropTarget}
+                onPointerDown={this.marqueeDown}>
                 <div className="imageBox-fader" style={{ opacity: backAlpha }}>
-                    <img key="paths" src={srcpath} style={{ transform, transformOrigin }} onError={action(e => (this._error = e.toString()))} draggable={false} width={nativeWidth} />
+                    <img
+                        alt=""
+                        key="paths"
+                        src={srcpath}
+                        style={{ transform, transformOrigin }}
+                        onError={action(e => {
+                            this._error = e.toString();
+                        })}
+                        draggable={false}
+                        width={nativeWidth}
+                    />
                     {fadepath === srcpath ? null : (
                         <div className={`imageBox-fadeBlocker${(this._isHovering && usePath === 'alternate:hover') || usePath === 'alternate' ? '-hover' : ''}`} style={{ transition: StrCast(this.layoutDoc.viewTransition, 'opacity 1000ms') }}>
-                            <img className="imageBox-fadeaway" key="fadeaway" src={fadepath} style={{ transform, transformOrigin }} draggable={false} width={nativeWidth} />
+                            <img alt="" className="imageBox-fadeaway" key="fadeaway" src={fadepath} style={{ transform, transformOrigin }} draggable={false} width={nativeWidth} />
                         </div>
                     )}
                 </div>
@@ -384,8 +416,15 @@ 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)) {
+        if (!this.dataDoc[this.fieldKey]) {
+            this.chooseImage();
+        } else 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,
                 e,
@@ -419,7 +458,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl
                 className="imageBox"
                 onContextMenu={this.specificContextMenu}
                 ref={this._mainCont}
-                onScroll={action(e => {
+                onScroll={action(() => {
                     if (!this._forcedScroll) {
                         if (this.layoutDoc._layout_scrollTop || this._mainCont.current?.scrollTop) {
                             this._ignoreScroll = true;
@@ -444,8 +483,8 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl
                     renderDepth={this._props.renderDepth + 1}
                     fieldKey={this.annotationKey}
                     styleProvider={this._props.styleProvider}
-                    isAnnotationOverlay={true}
-                    annotationLayerHostsContent={true}
+                    isAnnotationOverlay
+                    annotationLayerHostsContent
                     PanelWidth={this._props.PanelWidth}
                     PanelHeight={this._props.PanelHeight}
                     ScreenToLocalTransform={this.screenToLocalTransform}
@@ -476,7 +515,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl
                         selectionText={returnEmptyString}
                         annotationLayer={this._annotationLayer.current}
                         marqueeContainer={this._mainCont.current}
-                        highlightDragSrcColor={''}
+                        highlightDragSrcColor=""
                         anchorMenuCrop={this.crop}
                     />
                 )}
@@ -489,7 +528,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl
         input.type = 'file';
         input.multiple = true;
         input.accept = 'image/*';
-        input.onchange = async _e => {
+        input.onchange = async () => {
             const file = input.files?.[0];
             if (file) {
                 const disposer = OverlayView.ShowSpinner();
diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx
index 31a2367fc..74773b244 100644
--- a/src/client/views/nodes/KeyValueBox.tsx
+++ b/src/client/views/nodes/KeyValueBox.tsx
@@ -1,8 +1,8 @@
 import { action, computed, makeObservable, observable } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
-import { returnAlways, returnTrue } from '../../../Utils';
-import { Doc, Field, FieldResult } from '../../../fields/Doc';
+import { returnAlways, returnTrue } from '../../../ClientUtils';
+import { Doc, Field, FieldType, FieldResult } from '../../../fields/Doc';
 import { List } from '../../../fields/List';
 import { RichTextField } from '../../../fields/RichTextField';
 import { ComputedField, ScriptField } from '../../../fields/ScriptField';
@@ -96,15 +96,15 @@ export class KeyValueBox extends ObservableReactComponent<FieldViewProps> {
 
     public static ApplyKVPScript(doc: Doc, key: string, kvpScript: KVPScript, forceOnDelegate?: boolean, setResult?: (value: FieldResult) => void) {
         const { script, type, onDelegate } = kvpScript;
-        //const target = onDelegate ? Doc.Layout(doc.layout) : Doc.GetProto(doc); // bcz: TODO need to be able to set fields on layout templates
+        // const target = onDelegate ? Doc.Layout(doc.layout) : Doc.GetProto(doc); // bcz: TODO need to be able to set fields on layout templates
         const target = forceOnDelegate || onDelegate || key.startsWith('_') ? doc : DocCast(doc.proto, doc);
-        let field: Field | undefined;
+        let field: FieldType | undefined;
         switch (type) {
             case 'computed': field = new ComputedField(script); break; // prettier-ignore
             case 'script': field = new ScriptField(script); break; // prettier-ignore
             default: {
                 const _setCacheResult_ = (value: FieldResult) => {
-                    field = value as Field;
+                    field = value as FieldType;
                     if (setResult) setResult?.(value);
                     else target[key] = field;
                 };
diff --git a/src/client/views/nodes/KeyValuePair.tsx b/src/client/views/nodes/KeyValuePair.tsx
index f9e8ce4f3..c3afc198d 100644
--- a/src/client/views/nodes/KeyValuePair.tsx
+++ b/src/client/views/nodes/KeyValuePair.tsx
@@ -2,7 +2,8 @@ import { Tooltip } from '@mui/material';
 import { action, makeObservable, observable } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
-import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnZero } from '../../../Utils';
+import { returnEmptyDoclist, returnEmptyFilter, returnFalse, returnZero } from '../../../ClientUtils';
+import { emptyFunction } from '../../../Utils';
 import { Doc, Field } from '../../../fields/Doc';
 import { DocCast } from '../../../fields/Types';
 import { DocumentOptions, FInfo } from '../../documents/Documents';
diff --git a/src/client/views/nodes/LabelBox.tsx b/src/client/views/nodes/LabelBox.tsx
index 74e78c671..d1c8c62ed 100644
--- a/src/client/views/nodes/LabelBox.tsx
+++ b/src/client/views/nodes/LabelBox.tsx
@@ -1,21 +1,21 @@
 import { action, computed, makeObservable, observable } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
-import { Doc, DocListCast, Field } from '../../../fields/Doc';
+import { Doc, DocListCast, Field, FieldType } from '../../../fields/Doc';
 import { List } from '../../../fields/List';
 import { listSpec } from '../../../fields/Schema';
 import { BoolCast, Cast, NumCast, StrCast } from '../../../fields/Types';
+import { Docs } from '../../documents/Documents';
 import { DragManager } from '../../util/DragManager';
 import { undoBatch } from '../../util/UndoManager';
 import { ContextMenu } from '../ContextMenu';
 import { ContextMenuProps } from '../ContextMenuItem';
-import { ViewBoxBaseComponent } from '../DocComponent';
+import { PinProps, ViewBoxBaseComponent } from '../DocComponent';
 import { StyleProp } from '../StyleProvider';
 import { FieldView, FieldViewProps } from './FieldView';
 import BigText from './LabelBigText';
 import './LabelBox.scss';
-import { PinProps, PresBox } from './trails';
-import { Docs } from '../../documents/Documents';
+import { PresBox } from './trails';
 
 @observer
 export class LabelBox extends ViewBoxBaseComponent<FieldViewProps>() {
@@ -41,7 +41,7 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps>() {
     }
 
     @computed get Title() {
-        return Field.toString(this.dataDoc[this.fieldKey] as Field) || StrCast(this.Document.title);
+        return Field.toString(this.dataDoc[this.fieldKey] as FieldType) || StrCast(this.Document.title);
     }
 
     protected createDropTarget = (ele: HTMLDivElement) => {
diff --git a/src/client/views/nodes/LinkAnchorBox.tsx b/src/client/views/nodes/LinkAnchorBox.tsx
index ff1e62885..0155defb7 100644
--- a/src/client/views/nodes/LinkAnchorBox.tsx
+++ b/src/client/views/nodes/LinkAnchorBox.tsx
@@ -1,11 +1,13 @@
 import { action, computed, makeObservable } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
-import { Utils, emptyFunction, setupMoveUpEvents } from '../../../Utils';
+import { setupMoveUpEvents } from '../../../ClientUtils';
+import { Utils, emptyFunction } from '../../../Utils';
 import { Doc } from '../../../fields/Doc';
 import { NumCast, StrCast } from '../../../fields/Types';
 import { TraceMobx } from '../../../fields/util';
-import { DragManager, dropActionType } from '../../util/DragManager';
+import { DragManager } from '../../util/DragManager';
+import { dropActionType } from '../../util/DropActionTypes';
 import { LinkFollower } from '../../util/LinkFollower';
 import { SelectionManager } from '../../util/SelectionManager';
 import { ViewBoxBaseComponent } from '../DocComponent';
@@ -39,7 +41,7 @@ export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps>() {
     }
 
     onPointerDown = (e: React.PointerEvent) => {
-        const linkSource = this.linkSource;
+        const { linkSource } = this;
         linkSource &&
             setupMoveUpEvents(this, e, this.onPointerMove, emptyFunction, (e, doubleTap) => {
                 if (doubleTap) LinkFollower.FollowLink(this.Document, linkSource, false);
@@ -58,11 +60,10 @@ export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps>() {
                 dragData.dropPropertiesToRemove = ['link_anchor_1_x', 'link_anchor_1_y', 'link_anchor_2_x', 'link_anchor_2_y', 'onClick'];
                 DragManager.StartDocumentDrag([this._ref.current!], dragData, pt[0], pt[1]);
                 return true;
-            } else {
-                this.layoutDoc[this.fieldKey + '_x'] = ((pt[0] - bounds.left) / bounds.width) * 100;
-                this.layoutDoc[this.fieldKey + '_y'] = ((pt[1] - bounds.top) / bounds.height) * 100;
-                this.layoutDoc.link_autoMoveAnchors = false;
             }
+            this.layoutDoc[this.fieldKey + '_x'] = ((pt[0] - bounds.left) / bounds.width) * 100;
+            this.layoutDoc[this.fieldKey + '_y'] = ((pt[1] - bounds.top) / bounds.height) * 100;
+            this.layoutDoc.link_autoMoveAnchors = false;
         }
         return false;
     });
diff --git a/src/client/views/nodes/LinkBox.tsx b/src/client/views/nodes/LinkBox.tsx
index 3a2509c3d..2593491cc 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 { DashColor, lightOrDark, returnFalse } from '../../../ClientUtils';
 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 { emptyFunction } from '../../../Utils';
 import { DocumentManager } from '../../util/DocumentManager';
 import { SnappingManager } from '../../util/SnappingManager';
 import { ViewBoxBaseComponent } from '../DocComponent';
diff --git a/src/client/views/nodes/LinkDocPreview.tsx b/src/client/views/nodes/LinkDocPreview.tsx
index c9c8f9260..539daf0bd 100644
--- a/src/client/views/nodes/LinkDocPreview.tsx
+++ b/src/client/views/nodes/LinkDocPreview.tsx
@@ -4,7 +4,8 @@ import { action, computed, makeObservable, observable, runInAction } from 'mobx'
 import { observer } from 'mobx-react';
 import * as React from 'react';
 import wiki from 'wikijs';
-import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnEmptyString, returnFalse, returnNone, setupMoveUpEvents } from '../../../Utils';
+import { returnEmptyDoclist, returnEmptyFilter, returnEmptyString, returnFalse, returnNone, setupMoveUpEvents } from '../../../ClientUtils';
+import { emptyFunction } from '../../../Utils';
 import { Doc, Opt } from '../../../fields/Doc';
 import { Cast, DocCast, NumCast, PromiseValue, StrCast } from '../../../fields/Types';
 import { DocServer } from '../../DocServer';
diff --git a/src/client/views/nodes/MapBox/DirectionsAnchorMenu.tsx b/src/client/views/nodes/MapBox/DirectionsAnchorMenu.tsx
index 7e99795b5..fe7f8d8b3 100644
--- a/src/client/views/nodes/MapBox/DirectionsAnchorMenu.tsx
+++ b/src/client/views/nodes/MapBox/DirectionsAnchorMenu.tsx
@@ -4,7 +4,8 @@ import { IconButton } from 'browndash-components';
 import { IReactionDisposer, ObservableMap, reaction } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
-import { returnFalse, unimplementedFunction } from '../../../../Utils';
+import { returnFalse } from '../../../../ClientUtils';
+import { unimplementedFunction } from '../../../../Utils';
 import { Doc, Opt } from '../../../../fields/Doc';
 import { NumCast, StrCast } from '../../../../fields/Types';
 import { SelectionManager } from '../../../util/SelectionManager';
diff --git a/src/client/views/nodes/MapBox/MapAnchorMenu.tsx b/src/client/views/nodes/MapBox/MapAnchorMenu.tsx
index 08bea5d9d..d17c4298c 100644
--- a/src/client/views/nodes/MapBox/MapAnchorMenu.tsx
+++ b/src/client/views/nodes/MapBox/MapAnchorMenu.tsx
@@ -7,7 +7,8 @@ import { IReactionDisposer, ObservableMap, action, makeObservable, observable, r
 import { observer } from 'mobx-react';
 import * as React from 'react';
 import { CirclePicker, ColorResult } from 'react-color';
-import { returnFalse, setupMoveUpEvents, unimplementedFunction } from '../../../../Utils';
+import { returnFalse, setupMoveUpEvents } from '../../../../ClientUtils';
+import { unimplementedFunction } from '../../../../Utils';
 import { Doc, Opt } from '../../../../fields/Doc';
 import { NumCast, StrCast } from '../../../../fields/Types';
 import { CalendarManager } from '../../../util/CalendarManager';
diff --git a/src/client/views/nodes/MapBox/MapBox.tsx b/src/client/views/nodes/MapBox/MapBox.tsx
index b73898f59..7855f8fe8 100644
--- a/src/client/views/nodes/MapBox/MapBox.tsx
+++ b/src/client/views/nodes/MapBox/MapBox.tsx
@@ -12,7 +12,8 @@ import * as React from 'react';
 import { CirclePicker, ColorResult } from 'react-color';
 import { Layer, MapProvider, MapRef, Map as MapboxMap, Marker, Source, ViewState, ViewStateChangeEvent } from 'react-map-gl';
 import { MarkerEvent } from 'react-map-gl/dist/esm/types';
-import { Utils, emptyFunction, setupMoveUpEvents } from '../../../../Utils';
+import { ClientUtils, setupMoveUpEvents } from '../../../../ClientUtils';
+import { emptyFunction } from '../../../../Utils';
 import { Doc, DocListCast, Field, LinkedTo, Opt } from '../../../../fields/Doc';
 import { DocCast, NumCast, StrCast } from '../../../../fields/Types';
 import { DocumentType } from '../../../documents/DocumentTypes';
@@ -22,14 +23,14 @@ import { DragManager } from '../../../util/DragManager';
 import { LinkManager } from '../../../util/LinkManager';
 import { SnappingManager } from '../../../util/SnappingManager';
 import { UndoManager, undoable } from '../../../util/UndoManager';
-import { ViewBoxAnnotatableComponent, ViewBoxInterface } from '../../DocComponent';
+import { PinProps, ViewBoxAnnotatableComponent, ViewBoxInterface } from '../../DocComponent';
 import { SidebarAnnos } from '../../SidebarAnnos';
 import { MarqueeOptionsMenu } from '../../collections/collectionFreeForm';
 import { Colors } from '../../global/globalEnums';
 import { DocumentView } from '../DocumentView';
 import { FieldView, FieldViewProps, FocusViewOptions } from '../FieldView';
 import { FormattedTextBox } from '../formattedText/FormattedTextBox';
-import { PinProps, PresBox } from '../trails';
+import { PresBox } from '../trails';
 import { fastSpeedIcon, mediumSpeedIcon, slowSpeedIcon } from './AnimationSpeedIcons';
 import { AnimationSpeed, AnimationStatus, AnimationUtility } from './AnimationUtility';
 import { MapAnchorMenu } from './MapAnchorMenu';
@@ -401,8 +402,8 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
     panelWidth = () => this._props.PanelWidth() / (this._props.NativeDimScaling?.() || 1) - this.sidebarWidth();
     panelHeight = () => this._props.PanelHeight() / (this._props.NativeDimScaling?.() || 1);
     scrollXf = () => this.ScreenToLocalBoxXf().translate(0, NumCast(this.layoutDoc._layout_scrollTop));
-    transparentFilter = () => [...this._props.childFilters(), Utils.TransparentBackgroundFilter];
-    opaqueFilter = () => [...this._props.childFilters(), Utils.OpaqueBackgroundFilter];
+    transparentFilter = () => [...this._props.childFilters(), ClientUtils.TransparentBackgroundFilter];
+    opaqueFilter = () => [...this._props.childFilters(), ClientUtils.OpaqueBackgroundFilter];
     infoWidth = () => this._props.PanelWidth() / 5;
     infoHeight = () => this._props.PanelHeight() / 5;
     anchorMenuClick = () => this._sidebarRef.current?.anchorMenuClick;
diff --git a/src/client/views/nodes/MapboxMapBox/MapboxContainer.tsx b/src/client/views/nodes/MapboxMapBox/MapboxContainer.tsx
index 3eb051dbf..e857ef722 100644
--- a/src/client/views/nodes/MapboxMapBox/MapboxContainer.tsx
+++ b/src/client/views/nodes/MapboxMapBox/MapboxContainer.tsx
@@ -4,9 +4,11 @@ import { IReactionDisposer, ObservableMap, action, computed, makeObservable, obs
 import { observer } from 'mobx-react';
 import * as React from 'react';
 import { MapProvider, Map as MapboxMap } from 'react-map-gl';
-import { Utils, emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnOne, setupMoveUpEvents } from '../../../../Utils';
-import { Doc, DocListCast, Field, LinkedTo, Opt } from '../../../../fields/Doc';
+import { ClientUtils, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnOne, setupMoveUpEvents } from '../../../../ClientUtils';
+import { emptyFunction } from '../../../../Utils';
+import { Doc, DocListCast, LinkedTo, Opt } from '../../../../fields/Doc';
 import { DocCss, Highlight } from '../../../../fields/DocSymbols';
+import { Id } from '../../../../fields/FieldSymbols';
 import { DocCast, NumCast, StrCast } from '../../../../fields/Types';
 import { DocumentType } from '../../../documents/DocumentTypes';
 import { DocUtils, Docs } from '../../../documents/Documents';
@@ -16,15 +18,15 @@ import { LinkManager } from '../../../util/LinkManager';
 import { SnappingManager } from '../../../util/SnappingManager';
 import { Transform } from '../../../util/Transform';
 import { UndoManager, undoable } from '../../../util/UndoManager';
-import { ViewBoxAnnotatableComponent } from '../../DocComponent';
+import { PinProps, ViewBoxAnnotatableComponent } from '../../DocComponent';
 import { SidebarAnnos } from '../../SidebarAnnos';
 import { MarqueeOptionsMenu } from '../../collections/collectionFreeForm';
 import { Colors } from '../../global/globalEnums';
 import { DocumentView } from '../DocumentView';
-import { FocusViewOptions, FieldView, FieldViewProps } from '../FieldView';
+import { FieldView, FieldViewProps, FocusViewOptions } from '../FieldView';
 import { MapAnchorMenu } from '../MapBox/MapAnchorMenu';
 import { FormattedTextBox } from '../formattedText/FormattedTextBox';
-import { PinProps, PresBox } from '../trails';
+import { PresBox } from '../trails';
 import './MapBox.scss';
 
 /**
@@ -268,7 +270,7 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent<FieldViewProps>
     sidebarDown = (e: React.PointerEvent) => {
         setupMoveUpEvents(this, e, this.sidebarMove, emptyFunction, () => setTimeout(this.toggleSidebar), true);
     };
-    sidebarMove = (e: PointerEvent, down: number[], delta: number[]) => {
+    sidebarMove = (e: PointerEvent) => {
         const bounds = this._ref.current!.getBoundingClientRect();
         this.layoutDoc._layout_sidebarWidthPercent = '' + 100 * Math.max(0, 1 - (e.clientX - bounds.left) / bounds.width) + '%';
         this.layoutDoc._layout_showSidebar = this.layoutDoc._layout_sidebarWidthPercent !== '0%';
@@ -276,7 +278,9 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent<FieldViewProps>
         return false;
     };
 
-    setPreviewCursor = (func?: (x: number, y: number, drag: boolean, hide: boolean, doc: Opt<Doc>) => void) => (this._setPreviewCursor = func);
+    setPreviewCursor = (func?: (x: number, y: number, drag: boolean, hide: boolean, doc: Opt<Doc>) => void) => {
+        this._setPreviewCursor = func;
+    };
 
     addDocumentWrapper = (doc: Doc | Doc[], annotationKey?: string) => this.addDocument(doc, annotationKey);
 
@@ -285,8 +289,8 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent<FieldViewProps>
     panelWidth = () => this._props.PanelWidth() / (this._props.NativeDimScaling?.() || 1) - this.sidebarWidth();
     panelHeight = () => this._props.PanelHeight() / (this._props.NativeDimScaling?.() || 1);
     scrollXf = () => this.ScreenToLocalBoxXf().translate(0, NumCast(this.layoutDoc._layout_scrollTop));
-    transparentFilter = () => [...this._props.childFilters(), Utils.TransparentBackgroundFilter];
-    opaqueFilter = () => [...this._props.childFilters(), Utils.OpaqueBackgroundFilter];
+    transparentFilter = () => [...this._props.childFilters(), ClientUtils.TransparentBackgroundFilter];
+    opaqueFilter = () => [...this._props.childFilters(), ClientUtils.OpaqueBackgroundFilter];
     infoWidth = () => this._props.PanelWidth() / 5;
     infoHeight = () => this._props.PanelHeight() / 5;
     anchorMenuClick = () => this._sidebarRef.current?.anchorMenuClick;
@@ -306,11 +310,11 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent<FieldViewProps>
     //        center: new this.MicrosoftMaps.Location(loc.latitude, loc.longitude),
     //     });
     //
-    bingGeocode = (map: any, query: string) => {
-        return new Promise<{ latitude: number; longitude: number }>((res, reject) => {
-            //If search manager is not defined, load the search module.
+    bingGeocode = (map: any, query: string) =>
+        new Promise<{ latitude: number; longitude: number }>((res, reject) => {
+            // If search manager is not defined, load the search module.
             if (!this._bingSearchManager) {
-                //Create an instance of the search manager and call the geocodeQuery function again.
+                // Create an instance of the search manager and call the geocodeQuery function again.
                 this.MicrosoftMaps.loadModule('Microsoft.Maps.Search', () => {
                     this._bingSearchManager = new this.MicrosoftMaps.Search.SearchManager(map.current);
                     res(this.bingGeocode(map, query));
@@ -319,11 +323,10 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent<FieldViewProps>
                 this._bingSearchManager.geocode({
                     where: query,
                     callback: action((r: any) => res(r.results[0].location)),
-                    errorCallback: (e: any) => reject(),
+                    errorCallback: () => reject(),
                 });
             }
         });
-    };
 
     @observable
     bingSearchBarContents: any = this.Document.map; // For Bing Maps: The contents of the Bing search bar (string)
@@ -368,7 +371,7 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent<FieldViewProps>
                 this._bingMap.current.entities.remove(this.map_docToPinMap.get(temp));
             }
             const newpin = new this.MicrosoftMaps.Pushpin(new this.MicrosoftMaps.Location(temp.latitude, temp.longitude));
-            this.MicrosoftMaps.Events.addHandler(newpin, 'click', (e: any) => this.pushpinClicked(temp as Doc));
+            this.MicrosoftMaps.Events.addHandler(newpin, 'click', () => this.pushpinClicked(temp as Doc));
             if (!this._unmounting) {
                 this._bingMap.current.entities.push(newpin);
             }
@@ -383,7 +386,9 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent<FieldViewProps>
             this.toggleSidebar();
             options.didMove = true;
         }
-        return new Promise<Opt<DocumentView>>(res => DocumentManager.Instance.AddViewRenderedCb(doc, dv => res(dv)));
+        return new Promise<Opt<DocumentView>>(res => {
+            DocumentManager.Instance.AddViewRenderedCb(doc, dv => res(dv));
+        });
     };
     /*
      * Pushpin onclick
@@ -418,7 +423,7 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent<FieldViewProps>
      * Map OnClick
      */
     @action
-    mapOnClick = (e: { location: { latitude: any; longitude: any } }) => {
+    mapOnClick = (/* e: { location: { latitude: any; longitude: any } } */) => {
         this._props.select(false);
         this.deselectPin();
     };
@@ -442,22 +447,23 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent<FieldViewProps>
      * Updates maptype
      */
     @action
-    updateMapType = () => (this.dataDoc.map_type = this._bingMap.current.getMapTypeId());
+    updateMapType = () => {
+        this.dataDoc.map_type = this._bingMap.current.getMapTypeId();
+    };
 
     /*
      * For Bing Maps
      * Called by search button's onClick
      * Finds the geocode of the searched contents and sets location to that location
-     **/
+     * */
     @action
-    bingSearch = () => {
-        return this.bingGeocode(this._bingMap, this.bingSearchBarContents).then(location => {
+    bingSearch = () =>
+        this.bingGeocode(this._bingMap, this.bingSearchBarContents).then(location => {
             this.dataDoc.latitude = location.latitude;
             this.dataDoc.longitude = location.longitude;
             this.dataDoc.map_zoom = this._bingMap.current.getZoom();
             this.dataDoc.map = this.bingSearchBarContents;
         });
-    };
 
     /*
      * Returns doc w/ relevant info
@@ -502,7 +508,7 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent<FieldViewProps>
 
         this._bingMap.current.entities.push(pushPin);
 
-        this.MicrosoftMaps.Events.addHandler(pushPin, 'click', (e: any) => this.pushpinClicked(pin));
+        this.MicrosoftMaps.Events.addHandler(pushPin, 'click', () => this.pushpinClicked(pin));
         // this.MicrosoftMaps.Events.addHandler(pushPin, 'dblclick', (e: any) => this.pushpinDblClicked(pushPin, pin));
         this.map_docToPinMap.set(pin, pushPin);
     };
@@ -591,7 +597,9 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent<FieldViewProps>
     };
 
     @action
-    searchbarOnEdit = (newText: string) => (this.bingSearchBarContents = newText);
+    searchbarOnEdit = (newText: string) => {
+        this.bingSearchBarContents = newText;
+    };
 
     recolorPin = (pin: Doc, color?: string) => {
         this._bingMap.current.entities.remove(this.map_docToPinMap.get(pin));
@@ -620,7 +628,9 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent<FieldViewProps>
 
         this._disposers.mapLocation = reaction(
             () => this.Document.map,
-            mapLoc => (this.bingSearchBarContents = mapLoc),
+            mapLoc => {
+                this.bingSearchBarContents = mapLoc;
+            },
             { fireImmediately: true }
         );
         this._disposers.highlight = reaction(
@@ -720,6 +730,7 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent<FieldViewProps>
                         MapBoxContainer._rerenderDelay = 0;
                     }
                     this._rerenderTimeout = undefined;
+                    // eslint-disable-next-line operator-assignment
                     this.Document[DocCss] = this.Document[DocCss] + 1;
                 }), MapBoxContainer._rerenderDelay);
             return null;
@@ -752,18 +763,19 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent<FieldViewProps>
                                 <svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="magnifying-glass" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" color="#DFDFDF">
                                     <path
                                         fill="currentColor"
-                                        d="M416 208c0 45.9-14.9 88.3-40 122.7L502.6 457.4c12.5 12.5 12.5 32.8 0 45.3s-32.8 12.5-45.3 0L330.7 376c-34.4 25.2-76.8 40-122.7 40C93.1 416 0 322.9 0 208S93.1 0 208 0S416 93.1 416 208zM208 352a144 144 0 1 0 0-288 144 144 0 1 0 0 288z"></path>
+                                        d="M416 208c0 45.9-14.9 88.3-40 122.7L502.6 457.4c12.5 12.5 12.5 32.8 0 45.3s-32.8 12.5-45.3 0L330.7 376c-34.4 25.2-76.8 40-122.7 40C93.1 416 0 322.9 0 208S93.1 0 208 0S416 93.1 416 208zM208 352a144 144 0 1 0 0-288 144 144 0 1 0 0 288z"
+                                    />
                                 </svg>
                             }
                             onClick={this.bingSearch}
                             type={Type.TERT}
                         />
                         <div style={{ width: 30, height: 30 }} ref={this._dragRef} onPointerDown={this.dragToggle}>
-                            <Button tooltip="drag to place a pushpin" icon={<FontAwesomeIcon size={'lg'} icon={'bullseye'} />} />
+                            <Button tooltip="drag to place a pushpin" icon={<FontAwesomeIcon size="lg" icon="bullseye" />} />
                         </div>
                     </div>
                     <MapProvider>
-                        <MapboxMap id="mabox-map" mapStyle={`mapbox://styles/mapbox/streets-v9`} mapboxAccessToken={mapboxApiKey} />
+                        <MapboxMap id="mabox-map" mapStyle="mapbox://styles/mapbox/streets-v9" mapboxAccessToken={mapboxApiKey} />
                     </MapProvider>
 
                     {/* 
@@ -782,7 +794,8 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent<FieldViewProps>
                                   .filter(anno => !anno.layout_unrendered)
                                   .map((pushpin, i) => (
                                       <DocumentView
-                                          key={i}
+                                          key={pushpin[Id]}
+                                          // eslint-disable-next-line react/jsx-props-no-spreading
                                           {...this._props}
                                           renderDepth={this._props.renderDepth + 1}
                                           Document={pushpin}
@@ -821,12 +834,13 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent<FieldViewProps>
                 <div className="mapBox-sidebar" style={{ width: `${this.sidebarWidthPercent}`, backgroundColor: `${this.sidebarColor}` }}>
                     <SidebarAnnos
                         ref={this._sidebarRef}
+                        // eslint-disable-next-line react/jsx-props-no-spreading
                         {...this._props}
                         fieldKey={this.fieldKey}
                         Document={this.Document}
                         layoutDoc={this.layoutDoc}
                         dataDoc={this.dataDoc}
-                        usePanelWidth={true}
+                        usePanelWidth
                         showSidebar={this.SidebarShown}
                         nativeWidth={NumCast(this.layoutDoc._nativeWidth)}
                         whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx
index 1274220b6..8140c0ca7 100644
--- a/src/client/views/nodes/PDFBox.tsx
+++ b/src/client/views/nodes/PDFBox.tsx
@@ -4,6 +4,7 @@ import { observer } from 'mobx-react';
 import * as Pdfjs from 'pdfjs-dist';
 import 'pdfjs-dist/web/pdf_viewer.css';
 import * as React from 'react';
+import { ClientUtils, returnFalse, setupMoveUpEvents } from '../../../ClientUtils';
 import { Doc, DocListCast, Opt } from '../../../fields/Doc';
 import { DocData } from '../../../fields/DocSymbols';
 import { Id } from '../../../fields/FieldSymbols';
@@ -12,7 +13,7 @@ import { ComputedField } from '../../../fields/ScriptField';
 import { Cast, FieldValue, ImageCast, NumCast, StrCast } from '../../../fields/Types';
 import { ImageField, PdfField } from '../../../fields/URLField';
 import { TraceMobx } from '../../../fields/util';
-import { emptyFunction, returnFalse, setupMoveUpEvents, Utils } from '../../../Utils';
+import { emptyFunction } from '../../../Utils';
 import { Docs, DocUtils } from '../../documents/Documents';
 import { CollectionViewType, DocumentType } from '../../documents/DocumentTypes';
 import { DocumentManager } from '../../util/DocumentManager';
@@ -23,16 +24,16 @@ import { CollectionFreeFormView } from '../collections/collectionFreeForm';
 import { CollectionStackingView } from '../collections/CollectionStackingView';
 import { ContextMenu } from '../ContextMenu';
 import { ContextMenuProps } from '../ContextMenuItem';
-import { ViewBoxAnnotatableComponent, ViewBoxInterface } from '../DocComponent';
+import { PinProps, ViewBoxAnnotatableComponent, ViewBoxInterface } from '../DocComponent';
 import { Colors } from '../global/globalEnums';
 import { CreateImage } from '../nodes/WebBoxRenderer';
 import { PDFViewer } from '../pdf/PDFViewer';
 import { SidebarAnnos } from '../SidebarAnnos';
 import { DocumentView, OpenWhere } from './DocumentView';
-import { FocusViewOptions, FieldView, FieldViewProps } from './FieldView';
+import { FieldView, FieldViewProps, FocusViewOptions } from './FieldView';
 import { ImageBox } from './ImageBox';
 import './PDFBox.scss';
-import { PinProps, PresBox } from './trails';
+import { PresBox } from './trails';
 
 @observer
 export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implements ViewBoxInterface {
@@ -128,9 +129,9 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
         croppingProto.proto = Cast(this.Document.proto, Doc, null)?.proto; // set proto of cropping's data doc to be IMAGE_PROTO
         croppingProto.type = DocumentType.IMG;
         croppingProto.layout = ImageBox.LayoutString('data');
-        croppingProto.data = new ImageField(Utils.CorsProxy('http://www.cs.brown.edu/~bcz/noImage.png'));
-        croppingProto['data_nativeWidth'] = anchw;
-        croppingProto['data_nativeHeight'] = anchh;
+        croppingProto.data = new ImageField(ClientUtils.CorsProxy('http://www.cs.brown.edu/~bcz/noImage.png'));
+        croppingProto.data_nativeWidth = anchw;
+        croppingProto.data_nativeHeight = anchh;
         if (addCrop) {
             DocUtils.MakeLink(region, cropping, { link_relationship: 'cropped image' });
         }
@@ -146,8 +147,8 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
             (NumCast(region.x) * this._props.PanelWidth()) / NumCast(this.dataDoc[this.fieldKey + '_nativeWidth']),
             4
         )
-            .then((data_url: any) => {
-                Utils.convertDataUri(data_url, region[Id]).then(returnedfilename =>
+            .then((dataUrl: any) => {
+                ClientUtils.convertDataUri(dataUrl, region[Id]).then(returnedfilename =>
                     setTimeout(
                         action(() => {
                             croppingProto.data = new ImageField(returnedfilename);
@@ -182,8 +183,8 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
                 (iconFile: string, nativeWidth: number, nativeHeight: number) => {
                     setTimeout(() => {
                         this.dataDoc.icon = new ImageField(iconFile);
-                        this.dataDoc['icon_nativeWidth'] = nativeWidth;
-                        this.dataDoc['icon_nativeHeight'] = nativeHeight;
+                        this.dataDoc.icon_nativeWidth = nativeWidth;
+                        this.dataDoc.icon_nativeHeight = nativeHeight;
                     }, 500);
                 }
             );
@@ -231,11 +232,13 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
             options.didMove = true;
             this.toggleSidebar(false);
         }
-        return new Promise<Opt<DocumentView>>(res => DocumentManager.Instance.AddViewRenderedCb(doc, dv => res(dv)));
+        return new Promise<Opt<DocumentView>>(res => {
+            DocumentManager.Instance.AddViewRenderedCb(doc, dv => res(dv));
+        });
     };
 
     getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => {
-        let ele: Opt<HTMLDivElement> = undefined;
+        let ele: Opt<HTMLDivElement>;
         if (this._pdfViewer?.selectionContent()) {
             ele = document.createElement('div');
             ele.append(this._pdfViewer.selectionContent()!);
@@ -451,7 +454,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
         !options && ContextMenu.Instance.addItem({ description: 'Options...', subitems: optionItems, icon: 'asterisk' });
         const help = cm.findByDescription('Help...');
         const helpItems: ContextMenuProps[] = help && 'subitems' in help ? help.subitems : [];
-        helpItems.push({ description: 'Copy path', event: () => this.pdfUrl && Utils.CopyText(Utils.prepend('') + this.pdfUrl.url.pathname), icon: 'expand-arrows-alt' });
+        helpItems.push({ description: 'Copy path', event: () => this.pdfUrl && ClientUtils.CopyText(ClientUtils.prepend('') + this.pdfUrl.url.pathname), icon: 'expand-arrows-alt' });
         !help && ContextMenu.Instance.addItem({ description: 'Help...', noexpand: true, subitems: helpItems, icon: 'asterisk' });
     };
 
diff --git a/src/client/views/nodes/RecordingBox/RecordingBox.tsx b/src/client/views/nodes/RecordingBox/RecordingBox.tsx
index 1f976f926..40199cce1 100644
--- a/src/client/views/nodes/RecordingBox/RecordingBox.tsx
+++ b/src/client/views/nodes/RecordingBox/RecordingBox.tsx
@@ -11,17 +11,18 @@ import { Upload } from '../../../../server/SharedMediaTypes';
 import { DocumentType } from '../../../documents/DocumentTypes';
 import { Docs } from '../../../documents/Documents';
 import { DocumentManager } from '../../../util/DocumentManager';
-import { DragManager, dropActionType } from '../../../util/DragManager';
+import { DragManager } from '../../../util/DragManager';
 import { ScriptingGlobals } from '../../../util/ScriptingGlobals';
 import { Presentation } from '../../../util/TrackMovements';
 import { undoBatch } from '../../../util/UndoManager';
 import { ViewBoxBaseComponent } from '../../DocComponent';
 import { CollectionFreeFormView } from '../../collections/collectionFreeForm/CollectionFreeFormView';
-import { media_state } from '../AudioBox';
+import { mediaState } from '../AudioBox';
 import { FieldView, FieldViewProps } from '../FieldView';
 import { VideoBox } from '../VideoBox';
 import { RecordingView } from './RecordingView';
 import { DocData } from '../../../../fields/DocSymbols';
+import { dropActionType } from '../../../util/DropActionTypes';
 
 @observer
 export class RecordingBox extends ViewBoxBaseComponent<FieldViewProps>() {
@@ -78,7 +79,7 @@ export class RecordingBox extends ViewBoxBaseComponent<FieldViewProps>() {
             }, 100);
             //could break if recording takes too long to turn into videobox. If so, either increase time on setTimeout below or find diff place to do this
             setTimeout(() => Doc.RemFromMyOverlay(remDoc), 1000);
-            Doc.UserDoc().workspaceRecordingState = media_state.Paused;
+            Doc.UserDoc().workspaceRecordingState = mediaState.Paused;
             Doc.AddDocToList(Doc.UserDoc(), 'workspaceRecordings', remDoc);
         }
     }
@@ -110,7 +111,7 @@ export class RecordingBox extends ViewBoxBaseComponent<FieldViewProps>() {
             RecordingBox.screengrabber = docView.ComponentView as RecordingBox;
             RecordingBox.screengrabber.Record?.();
         });
-        Doc.UserDoc().workspaceRecordingState = media_state.Recording;
+        Doc.UserDoc().workspaceRecordingState = mediaState.Recording;
     }
 
     /**
@@ -150,7 +151,7 @@ export class RecordingBox extends ViewBoxBaseComponent<FieldViewProps>() {
         if (docView?.ComponentView instanceof VideoBox) {
             docView.ComponentView.Play();
         }
-        Doc.UserDoc().workspaceReplayingState = media_state.Playing;
+        Doc.UserDoc().workspaceReplayingState = mediaState.Playing;
     }
 
     public static pauseWorkspaceReplaying(doc: Doc) {
@@ -159,7 +160,7 @@ export class RecordingBox extends ViewBoxBaseComponent<FieldViewProps>() {
         if (videoBox) {
             videoBox.Pause();
         }
-        Doc.UserDoc().workspaceReplayingState = media_state.Paused;
+        Doc.UserDoc().workspaceReplayingState = mediaState.Paused;
     }
 
     public static stopWorkspaceReplaying(value: Doc) {
@@ -224,7 +225,7 @@ ScriptingGlobals.add(function getWorkspaceRecordings() {
     return new List<any>(['Record Workspace', `Record Webcam`, ...DocListCast(Doc.UserDoc().workspaceRecordings)]);
 });
 ScriptingGlobals.add(function isWorkspaceRecording() {
-    return Doc.UserDoc().workspaceRecordingState === media_state.Recording;
+    return Doc.UserDoc().workspaceRecordingState === mediaState.Recording;
 });
 ScriptingGlobals.add(function isWorkspaceReplaying() {
     return Doc.UserDoc().workspaceReplayingState;
diff --git a/src/client/views/nodes/RecordingBox/RecordingView.tsx b/src/client/views/nodes/RecordingBox/RecordingView.tsx
index f7ed82643..9c05a3e94 100644
--- a/src/client/views/nodes/RecordingBox/RecordingView.tsx
+++ b/src/client/views/nodes/RecordingBox/RecordingView.tsx
@@ -4,7 +4,7 @@ import { IconContext } from 'react-icons';
 import { FaCheckCircle } from 'react-icons/fa';
 import { MdBackspace } from 'react-icons/md';
 import { Upload } from '../../../../server/SharedMediaTypes';
-import { returnFalse, returnTrue, setupMoveUpEvents } from '../../../../Utils';
+import { returnFalse, returnTrue, setupMoveUpEvents } from '../../../../ClientUtils';
 import { Networking } from '../../../Network';
 import { Presentation, TrackMovements } from '../../../util/TrackMovements';
 import { ProgressBar } from './ProgressBar';
diff --git a/src/client/views/nodes/ScreenshotBox.tsx b/src/client/views/nodes/ScreenshotBox.tsx
index 1e3933ac3..e29e47514 100644
--- a/src/client/views/nodes/ScreenshotBox.tsx
+++ b/src/client/views/nodes/ScreenshotBox.tsx
@@ -4,30 +4,31 @@ import * as React from 'react';
 import { computed, makeObservable, observable, runInAction } from 'mobx';
 import { observer } from 'mobx-react';
 // import { BufferAttribute, Camera, Vector2, Vector3 } from 'three';
+import { returnFalse, returnOne, returnZero } from '../../../ClientUtils';
+import { emptyFunction } from '../../../Utils';
 import { DateField } from '../../../fields/DateField';
 import { Doc } from '../../../fields/Doc';
+import { DocData } from '../../../fields/DocSymbols';
 import { Id } from '../../../fields/FieldSymbols';
 import { ComputedField } from '../../../fields/ScriptField';
 import { Cast, DocCast, NumCast } from '../../../fields/Types';
 import { AudioField, VideoField } from '../../../fields/URLField';
 import { TraceMobx } from '../../../fields/util';
-import { emptyFunction, returnFalse, returnOne, returnZero } from '../../../Utils';
-import { DocUtils } from '../../documents/Documents';
-import { DocumentType } from '../../documents/DocumentTypes';
 import { Networking } from '../../Network';
+import { DocumentType } from '../../documents/DocumentTypes';
+import { DocUtils } from '../../documents/Documents';
 import { CaptureManager } from '../../util/CaptureManager';
 import { SettingsManager } from '../../util/SettingsManager';
 import { TrackMovements } from '../../util/TrackMovements';
-import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView';
-import { CollectionStackedTimeline } from '../collections/CollectionStackedTimeline';
 import { ContextMenu } from '../ContextMenu';
 import { ViewBoxAnnotatableComponent } from '../DocComponent';
-import { media_state } from './AudioBox';
+import { CollectionStackedTimeline } from '../collections/CollectionStackedTimeline';
+import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView';
+import { mediaState } from './AudioBox';
 import { FieldView, FieldViewProps } from './FieldView';
 import { FormattedTextBox } from './formattedText/FormattedTextBox';
 import './ScreenshotBox.scss';
 import { VideoBox } from './VideoBox';
-import { DocData } from '../../../fields/DocSymbols';
 
 declare class MediaRecorder {
     constructor(e: any, options?: any); // whatever MediaRecorder has
@@ -175,7 +176,7 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
                 ref={r => {
                     this._videoRef = r;
                     setTimeout(() => {
-                        if (this.layoutDoc.mediaState === media_state.PendingRecording && this._videoRef) {
+                        if (this.layoutDoc.mediaState === mediaState.PendingRecording && this._videoRef) {
                             this.toggleRecording();
                         }
                     }, 100);
diff --git a/src/client/views/nodes/ScriptingBox.tsx b/src/client/views/nodes/ScriptingBox.tsx
index 8c65fd34e..60d7e4b00 100644
--- a/src/client/views/nodes/ScriptingBox.tsx
+++ b/src/client/views/nodes/ScriptingBox.tsx
@@ -2,7 +2,7 @@ let ReactTextareaAutocomplete = require('@webscopeio/react-textarea-autocomplete
 import { action, computed, makeObservable, observable } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
-import { returnAlways, returnEmptyString } from '../../../Utils';
+import { returnAlways, returnEmptyString } from '../../../ClientUtils';
 import { Doc } from '../../../fields/Doc';
 import { List } from '../../../fields/List';
 import { listSpec } from '../../../fields/Schema';
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index 4773a21c9..60141b2a6 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -3,6 +3,7 @@ import { action, computed, IReactionDisposer, makeObservable, observable, Observ
 import { observer } from 'mobx-react';
 import { basename } from 'path';
 import * as React from 'react';
+import { ClientUtils, returnEmptyString, returnFalse, returnOne, returnZero, setupMoveUpEvents } from '../../../ClientUtils';
 import { Doc, Opt, StrListCast } from '../../../fields/Doc';
 import { DocData } from '../../../fields/DocSymbols';
 import { InkTool } from '../../../fields/InkField';
@@ -10,11 +11,11 @@ import { List } from '../../../fields/List';
 import { ObjectField } from '../../../fields/ObjectField';
 import { Cast, NumCast, StrCast } from '../../../fields/Types';
 import { AudioField, ImageField, VideoField } from '../../../fields/URLField';
-import { emptyFunction, formatTime, returnEmptyString, returnFalse, returnOne, returnZero, setupMoveUpEvents, Utils } from '../../../Utils';
+import { emptyFunction, formatTime } from '../../../Utils';
 import { Docs, DocUtils } from '../../documents/Documents';
 import { DocumentType } from '../../documents/DocumentTypes';
 import { DocumentManager } from '../../util/DocumentManager';
-import { dropActionType } from '../../util/DragManager';
+import { dropActionType } from '../../util/DropActionTypes';
 import { FollowLinkScript } from '../../util/LinkFollower';
 import { LinkManager } from '../../util/LinkManager';
 import { ReplayMovements } from '../../util/ReplayMovements';
@@ -23,14 +24,14 @@ import { CollectionFreeFormView } from '../collections/collectionFreeForm/Collec
 import { CollectionStackedTimeline, TrimScope } from '../collections/CollectionStackedTimeline';
 import { ContextMenu } from '../ContextMenu';
 import { ContextMenuProps } from '../ContextMenuItem';
-import { ViewBoxAnnotatableComponent, ViewBoxInterface } from '../DocComponent';
+import { PinProps, ViewBoxAnnotatableComponent, ViewBoxInterface } from '../DocComponent';
 import { MarqueeAnnotator } from '../MarqueeAnnotator';
 import { AnchorMenu } from '../pdf/AnchorMenu';
 import { StyleProp } from '../StyleProvider';
 import { DocumentView } from './DocumentView';
 import { FieldView, FieldViewProps, FocusViewOptions } from './FieldView';
 import { RecordingBox } from './RecordingBox';
-import { PinProps, PresBox } from './trails';
+import { PresBox } from './trails';
 import './VideoBox.scss';
 
 /**
@@ -303,7 +304,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl
             const retitled = StrCast(this.Document.title).replace(/[ -\.:]/g, '');
             const encodedFilename = encodeURIComponent(('snapshot' + retitled + '_' + (this.layoutDoc._layout_currentTimecode || 0).toString()).replace(/[\.\/\?\=]/g, '_'));
             const filename = basename(encodedFilename);
-            Utils.convertDataUri(dataUrl, filename).then((returnedFilename: string) => returnedFilename && (cb ?? this.createSnapshotLink)(returnedFilename, downX, downY));
+            ClientUtils.convertDataUri(dataUrl, filename).then((returnedFilename: string) => returnedFilename && (cb ?? this.createSnapshotLink)(returnedFilename, downX, downY));
         }
     };
 
@@ -318,7 +319,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl
 
     // creates link for snapshot
     createSnapshotLink = (imagePath: string, downX?: number, downY?: number) => {
-        const url = !imagePath.startsWith('/') ? Utils.CorsProxy(imagePath) : imagePath;
+        const url = !imagePath.startsWith('/') ? ClientUtils.CorsProxy(imagePath) : imagePath;
         const width = NumCast(this.layoutDoc._width) || 1;
         const height = NumCast(this.layoutDoc._height);
         const imageSnapshot = Docs.Create.ImageDocument(url, {
@@ -399,7 +400,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl
             canvas.getContext('2d')?.drawImage(video, 0, 0, video.videoWidth, video.videoHeight, 0, 0, 100, 100);
             const retitled = StrCast(this.Document.title).replace(/[ -\.:]/g, '');
             const encodedFilename = encodeURIComponent('thumbnail' + retitled + '_' + video.currentTime.toString().replace(/\./, '_'));
-            thumbnailPromises?.push(Utils.convertDataUri(canvas.toDataURL(), basename(encodedFilename), true));
+            thumbnailPromises?.push(ClientUtils.convertDataUri(canvas.toDataURL(), basename(encodedFilename), true));
             const newTime = video.currentTime + video.duration / (VideoBox.numThumbnails - 1);
             if (newTime < video.duration) {
                 video.currentTime = newTime;
@@ -480,7 +481,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl
             subitems.push({
                 description: 'Copy path',
                 event: () => {
-                    Utils.CopyText(url);
+                    ClientUtils.CopyText(url);
                 },
                 icon: 'expand-arrows-alt',
             });
@@ -644,7 +645,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl
     playFrom = (seekTimeInSeconds: number, endTime?: number, fullPlay: boolean = false) => {
         clearTimeout(this._playRegionTimer);
         this._playRegionTimer = undefined;
-        if (Number.isNaN(this.player?.duration)) {
+        if (isNaN(this.player?.duration)) {
             setTimeout(() => this.playFrom(seekTimeInSeconds, endTime), 500);
         } else if (this.player) {
             // trimBounds override requested playback bounds
@@ -806,7 +807,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl
 
     marqueeOffset = () => [((this.panelWidth() / 2) * (1 - this.heightPercent / 100)) / (this.heightPercent / 100), 0];
 
-    timelineDocFilter = () => [`_isTimelineLabel:true,${Utils.noRecursionHack}:x`];
+    timelineDocFilter = () => [`_isTimelineLabel:true,${ClientUtils.noRecursionHack}:x`];
 
     // renders video controls
     componentUI = (boundsLeft: number, boundsTop: number) => {
diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx
index 033b01d24..446e83dd3 100644
--- a/src/client/views/nodes/WebBox.tsx
+++ b/src/client/views/nodes/WebBox.tsx
@@ -4,7 +4,8 @@ import { action, computed, IReactionDisposer, makeObservable, observable, Observ
 import { observer } from 'mobx-react';
 import * as React from 'react';
 import * as WebRequest from 'web-request';
-import { Doc, DocListCast, Field, Opt } from '../../../fields/Doc';
+import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, ClientUtils, DivHeight, getWordAtPoint, lightOrDark, returnFalse, returnOne, returnZero, setupMoveUpEvents, smoothScroll } from '../../../ClientUtils';
+import { Doc, DocListCast, Field, FieldType, Opt } from '../../../fields/Doc';
 import { Id } from '../../../fields/FieldSymbols';
 import { HtmlField } from '../../../fields/HtmlField';
 import { InkTool } from '../../../fields/InkField';
@@ -14,7 +15,7 @@ import { listSpec } from '../../../fields/Schema';
 import { Cast, NumCast, StrCast, WebCast } from '../../../fields/Types';
 import { ImageField, WebField } from '../../../fields/URLField';
 import { TraceMobx } from '../../../fields/util';
-import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, DivHeight, emptyFunction, getWordAtPoint, lightOrDark, returnFalse, returnOne, returnZero, setupMoveUpEvents, smoothScroll, stringHash, Utils } from '../../../Utils';
+import { emptyFunction, stringHash, Utils } from '../../../Utils';
 import { Docs, DocUtils } from '../../documents/Documents';
 import { DocumentManager } from '../../util/DocumentManager';
 import { ScriptingGlobals } from '../../util/ScriptingGlobals';
@@ -24,7 +25,7 @@ import { MarqueeOptionsMenu } from '../collections/collectionFreeForm';
 import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView';
 import { ContextMenu } from '../ContextMenu';
 import { ContextMenuProps } from '../ContextMenuItem';
-import { ViewBoxAnnotatableComponent, ViewBoxInterface } from '../DocComponent';
+import { PinProps, ViewBoxAnnotatableComponent, ViewBoxInterface } from '../DocComponent';
 import { Colors } from '../global/globalEnums';
 import { LightboxView } from '../LightboxView';
 import { MarqueeAnnotator } from '../MarqueeAnnotator';
@@ -36,7 +37,7 @@ import { StyleProp } from '../StyleProvider';
 import { DocumentView, OpenWhere } from './DocumentView';
 import { FieldView, FieldViewProps, FocusViewOptions } from './FieldView';
 import { LinkInfo } from './LinkDocPreview';
-import { PinProps, PresBox } from './trails';
+import { PresBox } from './trails';
 import './WebBox.scss';
 const { CreateImage } = require('./WebBoxRenderer');
 const _global = (window /* browser */ || global) /* node */ as any;
@@ -143,7 +144,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
         const nativeHeight = (nativeWidth * this._props.PanelHeight()) / this._props.PanelWidth();
         var htmlString = this._iframe.contentDocument && new XMLSerializer().serializeToString(this._iframe.contentDocument);
         if (!htmlString) {
-            htmlString = await (await fetch(Utils.CorsProxy(this.webField!.href))).text();
+            htmlString = await (await fetch(ClientUtils.CorsProxy(this.webField!.href))).text();
         }
         this.layoutDoc.thumb = undefined;
         this.Document.thumbLockout = true; // lock to prevent multiple thumb updates.
@@ -219,7 +220,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
                 }
             } // else it's an HTMLfield
         } else if (this.webField && !this.dataDoc.text) {
-            WebRequest.get(Utils.CorsProxy(this.webField.href)) //
+            WebRequest.get(ClientUtils.CorsProxy(this.webField.href)) //
                 .then(result => result && (this.dataDoc.text = htmlToText(result.content)));
         }
 
@@ -254,7 +255,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
             const clientRects = selRange.getClientRects();
             for (let i = 0; i < clientRects.length; i++) {
                 const rect = clientRects.item(i);
-                const mainrect = this._url ? { translateX: 0, translateY: 0, scale: 1 } : Utils.GetScreenTransform(this._mainCont.current);
+                const mainrect = this._url ? { translateX: 0, translateY: 0, scale: 1 } : ClientUtils.GetScreenTransform(this._mainCont.current);
                 if (rect && rect.width !== this._mainCont.current.clientWidth) {
                     const annoBox = document.createElement('div');
                     annoBox.className = 'marqueeAnnotator-annotationBox';
@@ -283,7 +284,14 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
     focus = (anchor: Doc, options: FocusViewOptions) => {
         if (anchor !== this.Document && this._outerRef.current) {
             const windowHeight = this._props.PanelHeight() / (this._props.NativeDimScaling?.() || 1);
-            const scrollTo = Utils.scrollIntoView(NumCast(anchor.y), NumCast(anchor._height), NumCast(this.layoutDoc._layout_scrollTop), windowHeight, windowHeight * 0.1, Math.max(NumCast(anchor.y) + NumCast(anchor._height), this._scrollHeight));
+            const scrollTo = ClientUtils.scrollIntoView(
+                NumCast(anchor.y),
+                NumCast(anchor._height),
+                NumCast(this.layoutDoc._layout_scrollTop),
+                windowHeight,
+                windowHeight * 0.1,
+                Math.max(NumCast(anchor.y) + NumCast(anchor._height), this._scrollHeight)
+            );
             if (scrollTo !== undefined) {
                 if (this._initialScroll === undefined) {
                     const focusTime = options.zoomTime ?? 500;
@@ -356,7 +364,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
         this._textAnnotationCreator = undefined;
         this.DocumentView?.()?.cleanupPointerEvents(); // pointerup events aren't generated on containing document view, so we have to invoke it here.
         if (this._iframe?.contentWindow && this._iframe.contentDocument && !this._iframe.contentWindow.getSelection()?.isCollapsed) {
-            const mainContBounds = Utils.GetScreenTransform(this._mainCont.current!);
+            const mainContBounds = ClientUtils.GetScreenTransform(this._mainCont.current!);
             const scale = (this._props.NativeDimScaling?.() || 1) * mainContBounds.scale;
             const sel = this._iframe.contentWindow.getSelection();
             if (sel) {
@@ -491,7 +499,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
             runInAction(() => this._warning++);
             href = undefined;
         }
-        let requrlraw = decodeURIComponent(href?.replace(Utils.prepend('') + '/corsProxy/', '') ?? this._url.toString());
+        let requrlraw = decodeURIComponent(href?.replace(ClientUtils.prepend('') + '/corsProxy/', '') ?? this._url.toString());
         if (requrlraw !== this._url.toString()) {
             if (requrlraw.match(/q=.*&/)?.length && this._url.toString().match(/q=.*&/)?.length) {
                 const matches = requrlraw.match(/[^a-zA-z]q=[^&]*/g);
@@ -553,7 +561,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
                             const batch = UndoManager.StartBatch('webclick');
                             e.stopPropagation();
                             setTimeout(() => {
-                                this.setData(href.replace(Utils.prepend(''), origin));
+                                this.setData(href.replace(ClientUtils.prepend(''), origin));
                                 batch.end();
                             });
                             if (this._outerRef.current) {
@@ -698,7 +706,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
     };
 
     @action
-    setData = (data: Field | Promise<RefField | undefined>) => {
+    setData = (data: FieldType | Promise<RefField | undefined>) => {
         if (!(typeof data === 'string') && !(data instanceof WebField)) return false;
         if (Field.toString(data) === this._url) return false;
         this._scrollHeight = 0;
@@ -816,7 +824,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
             );
         }
         if (field instanceof WebField) {
-            const url = this.layoutDoc[this.fieldKey + '_useCors'] ? Utils.CorsProxy(this._webUrl) : this._webUrl;
+            const url = this.layoutDoc[this.fieldKey + '_useCors'] ? ClientUtils.CorsProxy(this._webUrl) : this._webUrl;
             const scripts = this.dataDoc[this.fieldKey + '_allowScripts'] || this._webUrl.includes('wikipedia.org') || this._webUrl.includes('google.com') || this._webUrl.startsWith('https://bing');
             //if (!scripts) console.log('No scripts for: ' + url);
             return (
@@ -1081,8 +1089,8 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
     panelHeight = () => this._props.PanelHeight() / (this._props.NativeDimScaling?.() || 1);
     scrollXf = () => this.ScreenToLocalBoxXf().translate(0, NumCast(this.layoutDoc._layout_scrollTop));
     anchorMenuClick = () => this._sidebarRef.current?.anchorMenuClick;
-    transparentFilter = () => [...this._props.childFilters(), Utils.TransparentBackgroundFilter];
-    opaqueFilter = () => [...this._props.childFilters(), Utils.noDragDocsFilter, ...(SnappingManager.CanEmbed ? [] : [Utils.OpaqueBackgroundFilter])];
+    transparentFilter = () => [...this._props.childFilters(), ClientUtils.TransparentBackgroundFilter];
+    opaqueFilter = () => [...this._props.childFilters(), ClientUtils.noDragDocsFilter, ...(SnappingManager.CanEmbed ? [] : [ClientUtils.OpaqueBackgroundFilter])];
     childStyleProvider = (doc: Doc | undefined, props: Opt<FieldViewProps>, property: string): any => {
         if (doc instanceof Doc && property === StyleProp.PointerEvents) {
             if (this.inlineTextAnnotations.includes(doc)) return 'none';
diff --git a/src/client/views/nodes/calendarBox/CalendarBox.tsx b/src/client/views/nodes/calendarBox/CalendarBox.tsx
index 748c3322e..8577510e3 100644
--- a/src/client/views/nodes/calendarBox/CalendarBox.tsx
+++ b/src/client/views/nodes/calendarBox/CalendarBox.tsx
@@ -4,7 +4,7 @@ import multiMonthPlugin from '@fullcalendar/multimonth';
 import { makeObservable } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
-import { dateRangeStrToDates } from '../../../../Utils';
+import { dateRangeStrToDates } from '../../../../ClientUtils';
 import { Doc } from '../../../../fields/Doc';
 import { StrCast } from '../../../../fields/Types';
 import { ViewBoxBaseComponent } from '../../DocComponent';
diff --git a/src/client/views/nodes/formattedText/DashDocView.tsx b/src/client/views/nodes/formattedText/DashDocView.tsx
index 7335c9286..dee7d70bb 100644
--- a/src/client/views/nodes/formattedText/DashDocView.tsx
+++ b/src/client/views/nodes/formattedText/DashDocView.tsx
@@ -3,10 +3,11 @@ import { observer } from 'mobx-react';
 import { NodeSelection } from 'prosemirror-state';
 import * as React from 'react';
 import * as ReactDOM from 'react-dom/client';
+import { Utils } from '../../../../Utils';
+import { returnFalse } from '../../../../ClientUtils';
 import { Doc } from '../../../../fields/Doc';
 import { Height, Width } from '../../../../fields/DocSymbols';
 import { NumCast } from '../../../../fields/Types';
-import { emptyFunction, returnFalse, Utils } from '../../../../Utils';
 import { DocServer } from '../../../DocServer';
 import { Docs, DocUtils } from '../../../documents/Documents';
 import { Transform } from '../../../util/Transform';
diff --git a/src/client/views/nodes/formattedText/DashFieldView.tsx b/src/client/views/nodes/formattedText/DashFieldView.tsx
index 439d4785e..eaa8fffaa 100644
--- a/src/client/views/nodes/formattedText/DashFieldView.tsx
+++ b/src/client/views/nodes/formattedText/DashFieldView.tsx
@@ -1,15 +1,17 @@
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { Tooltip } from '@mui/material';
-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 { NodeSelection } from 'prosemirror-state';
 import * as React from 'react';
 import * as ReactDOM from 'react-dom/client';
+import { returnFalse, returnZero, setupMoveUpEvents } from '../../../../ClientUtils';
 import { Doc, DocListCast, Field } from '../../../../fields/Doc';
 import { List } from '../../../../fields/List';
 import { listSpec } from '../../../../fields/Schema';
 import { SchemaHeaderField } from '../../../../fields/SchemaHeaderField';
 import { Cast, DocCast } from '../../../../fields/Types';
-import { emptyFunction, returnFalse, returnZero, setupMoveUpEvents } from '../../../../Utils';
+import { emptyFunction } from '../../../../Utils';
 import { DocServer } from '../../../DocServer';
 import { CollectionViewType } from '../../../documents/DocumentTypes';
 import { Transform } from '../../../util/Transform';
@@ -21,8 +23,6 @@ import { ObservableReactComponent } from '../../ObservableReactComponent';
 import { OpenWhere } from '../DocumentView';
 import './DashFieldView.scss';
 import { FormattedTextBox } from './FormattedTextBox';
-import { DocData } from '../../../../fields/DocSymbols';
-import { NodeSelection } from 'prosemirror-state';
 
 export class DashFieldView {
     dom: HTMLDivElement; // container for label and value
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index a2db2a1cc..31252e0ab 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -1,3 +1,4 @@
+/* eslint-disable jsx-a11y/no-static-element-interactions */
 import { IconProp } from '@fortawesome/fontawesome-svg-core';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { Tooltip } from '@mui/material';
@@ -12,8 +13,9 @@ import { EditorState, NodeSelection, Plugin, TextSelection, Transaction } from '
 import { EditorView } from 'prosemirror-view';
 import * as React from 'react';
 import { BsMarkdownFill } from 'react-icons/bs';
+import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, ClientUtils, DivWidth, returnFalse, returnZero, setupMoveUpEvents, smoothScroll } from '../../../../ClientUtils';
 import { DateField } from '../../../../fields/DateField';
-import { Doc, DocListCast, Field, Opt, StrListCast } from '../../../../fields/Doc';
+import { Doc, DocListCast, Field, FieldType, Opt, StrListCast } from '../../../../fields/Doc';
 import { AclAdmin, AclAugment, AclEdit, AclSelfEdit, DocCss, DocData, ForceServerWrite, UpdatingFromServer } from '../../../../fields/DocSymbols';
 import { Id } from '../../../../fields/FieldSymbols';
 import { InkTool } from '../../../../fields/InkField';
@@ -23,14 +25,15 @@ import { RichTextField } from '../../../../fields/RichTextField';
 import { ComputedField } from '../../../../fields/ScriptField';
 import { BoolCast, Cast, DateCast, DocCast, FieldValue, NumCast, RTFCast, ScriptCast, StrCast } from '../../../../fields/Types';
 import { GetEffectiveAcl, TraceMobx } from '../../../../fields/util';
-import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, DivWidth, emptyFunction, numberRange, returnFalse, returnZero, setupMoveUpEvents, smoothScroll, unimplementedFunction, Utils } from '../../../../Utils';
+import { emptyFunction, numberRange, unimplementedFunction, Utils } from '../../../../Utils';
 import { gptAPICall, GPTCallType } from '../../../apis/gpt/GPT';
 import { DocServer } from '../../../DocServer';
 import { Docs, DocUtils } from '../../../documents/Documents';
 import { CollectionViewType } from '../../../documents/DocumentTypes';
 import { DictationManager } from '../../../util/DictationManager';
 import { DocumentManager } from '../../../util/DocumentManager';
-import { DragManager, dropActionType } from '../../../util/DragManager';
+import { DragManager } from '../../../util/DragManager';
+import { dropActionType } from '../../../util/DropActionTypes';
 import { MakeTemplate } from '../../../util/DropConverter';
 import { LinkManager } from '../../../util/LinkManager';
 import { RTFMarkup } from '../../../util/RTFMarkup';
@@ -42,18 +45,18 @@ import { CollectionStackingView } from '../../collections/CollectionStackingView
 import { CollectionTreeView } from '../../collections/CollectionTreeView';
 import { ContextMenu } from '../../ContextMenu';
 import { ContextMenuProps } from '../../ContextMenuItem';
-import { ViewBoxAnnotatableComponent, ViewBoxInterface } from '../../DocComponent';
+import { PinProps, ViewBoxAnnotatableComponent, ViewBoxInterface } from '../../DocComponent';
 import { Colors } from '../../global/globalEnums';
 import { LightboxView } from '../../LightboxView';
 import { AnchorMenu } from '../../pdf/AnchorMenu';
 import { GPTPopup } from '../../pdf/GPTPopup/GPTPopup';
 import { SidebarAnnos } from '../../SidebarAnnos';
-import { StyleProp } from '../../StyleProvider';
-import { media_state } from '../AudioBox';
+import { styleFromLayoutString, StyleProp } from '../../StyleProvider';
+import { mediaState } from '../AudioBox';
 import { DocumentView, DocumentViewInternal, OpenWhere } from '../DocumentView';
 import { FieldView, FieldViewProps, FocusViewOptions } from '../FieldView';
 import { LinkInfo } from '../LinkDocPreview';
-import { PinProps, PresBox } from '../trails';
+import { PresBox } from '../trails';
 import { DashDocCommentView } from './DashDocCommentView';
 import { DashDocView } from './DashDocView';
 import { DashFieldView } from './DashFieldView';
@@ -152,10 +155,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
         return this.titleHeight + NumCast(this.layoutDoc._layout_autoHeightMargins);
     }
     @computed get _recordingDictation() {
-        return this.dataDoc?.mediaState === media_state.Recording;
+        return this.dataDoc?.mediaState === mediaState.Recording;
     }
     set _recordingDictation(value) {
-        !this.dataDoc[`${this.fieldKey}_recordingSource`] && (this.dataDoc.mediaState = value ? media_state.Recording : undefined);
+        !this.dataDoc[`${this.fieldKey}_recordingSource`] && (this.dataDoc.mediaState = value ? mediaState.Recording : undefined);
     }
     @computed get config() {
         this._keymap = buildKeymap(schema, this._props);
@@ -275,7 +278,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
                     anchor.followLinkAudio = true;
                     let stopFunc: any;
                     const targetData = target[DocData];
-                    targetData.mediaState = media_state.Recording;
+                    targetData.mediaState = mediaState.Recording;
                     DocumentViewInternal.recordAudioAnnotation(targetData, Doc.LayoutFieldKey(target), stop => (stopFunc = stop));
                     const reactionDisposer = reaction(
                         () => target.mediaState,
@@ -313,7 +316,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
         });
         const coordsB = this._editorView!.coordsAtPos(this._editorView!.state.selection.to);
         this._props.rootSelected?.() && AnchorMenu.Instance.jumpTo(coordsB.left, coordsB.bottom);
-        let ele: Opt<HTMLDivElement> = undefined;
+        let ele: Opt<HTMLDivElement>;
         try {
             const contents = window.getSelection()?.getRangeAt(0).cloneContents();
             if (contents) {
@@ -330,7 +333,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
             const fieldKey = StrCast(node.attrs.fieldKey);
             return (
                 (node.attrs.hideKey ? '' : fieldKey + ':') + //
-                (node.attrs.hideValue ? '' : Field.toJavascriptString(refDoc[fieldKey] as Field))
+                (node.attrs.hideValue ? '' : Field.toJavascriptString(refDoc[fieldKey] as FieldType))
             );
         }
         return '';
@@ -345,8 +348,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
 
     tryUpdateDoc = (force: boolean) => {
         if (this._editorView && (this._editorView as any).docView) {
-            const state = this._editorView.state;
-            const dataDoc = this.dataDoc;
+            const { state } = this._editorView;
+            const { dataDoc } = this;
             const newText = state.doc.textBetween(0, state.doc.content.size, ' \n', this.leafText);
             const newJson = JSON.stringify(state.toJSON());
             const prevData = Cast(this.layoutDoc[this.fieldKey], RichTextField, null); // the actual text in the text box
@@ -386,7 +389,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
                     } else {
                         // if we've deleted all the text in a note driven by a template, then restore the template data
                         dataDoc[this.fieldKey] = undefined;
-                        this._editorView.updateState(EditorState.fromJSON(this.config, JSON.parse(((layoutData !== prevData ? layoutData : undefined) ?? protoData).Data)));
+                        this._editorView.updateState(EditorState.fromJSON(this.config, JSON.parse(((layoutData !== prevData ? layoutData : undefined) ?? protoData)?.Data)));
                         ScriptCast(this.layoutDoc.onTextChanged, null)?.script.run({ this: this.layoutDoc, text: newText });
                         unchanged = false;
                     }
@@ -417,7 +420,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
         let link;
         LinkManager.Links(this.dataDoc).forEach((l, i) => {
             const anchor = (l.link_anchor_1 as Doc).annotationOn ? (l.link_anchor_1 as Doc) : (l.link_anchor_2 as Doc).annotationOn ? (l.link_anchor_2 as Doc) : undefined;
-            if (anchor && (anchor.annotationOn as Doc).mediaState === media_state.Recording) {
+            if (anchor && (anchor.annotationOn as Doc).mediaState === mediaState.Recording) {
                 linkTime = NumCast(anchor._timecodeToShow /* audioStart */);
                 linkAnchor = anchor;
                 link = l;
@@ -426,7 +429,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
         if (this._editorView && linkTime) {
             const state = this._editorView.state;
             const now = Date.now();
-            let mark = schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.floor(now / 1000) });
+            let mark = schema.marks.user_mark.create({ userid: ClientUtils.CurrentUserEmail, modified: Math.floor(now / 1000) });
             if (!this._break && state.selection.to !== state.selection.from) {
                 for (let i = state.selection.from; i <= state.selection.to; i++) {
                     const pos = state.doc.resolve(i);
@@ -507,7 +510,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
     hyperlinkTerm = (tr: any, target: Doc, newAutoLinks: Set<Doc>) => {
         const editorView = this._editorView;
         if (editorView && (editorView as any).docView && !Doc.AreProtosEqual(target, this.Document)) {
-            const autoLinkTerm = Field.toString(target.title as Field).replace(/^@/, '');
+            const autoLinkTerm = Field.toString(target.title as FieldType).replace(/^@/, '');
             var alink: Doc | undefined;
             this.findInNode(editorView, editorView.state.doc, autoLinkTerm).forEach(sel => {
                 if (
@@ -587,7 +590,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
     };
     adoptAnnotation = (start: number, end: number, mark: Mark) => {
         const view = this._editorView!;
-        const nmark = view.state.schema.marks.user_mark.create({ ...mark.attrs, userid: Doc.CurrentUserEmail });
+        const nmark = view.state.schema.marks.user_mark.create({ ...mark.attrs, userid: ClientUtils.CurrentUserEmail });
         view.dispatch(view.state.tr.removeMark(start, end, nmark).addMark(start, end, nmark));
     };
     protected createDropTarget = (ele: HTMLDivElement) => {
@@ -726,7 +729,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
             addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UM-remote', { background: 'yellow' });
         }
         if (highlights.includes('My Text')) {
-            addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UM-' + Doc.CurrentUserEmail.replace(/\./g, '').replace(/@/g, ''), { background: 'moccasin' });
+            addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UM-' + ClientUtils.CurrentUserEmail.replace(/\./g, '').replace(/@/g, ''), { background: 'moccasin' });
         }
         if (highlights.includes('Todo Items')) {
             addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UT-todo', { outline: 'black solid 1px' });
@@ -745,12 +748,12 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
             addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UT-ignore', { 'font-size': '1' });
         }
         if (highlights.includes('By Recent Minute')) {
-            addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UM-' + Doc.CurrentUserEmail.replace('.', '').replace('@', ''), { opacity: '0.1' });
+            addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UM-' + ClientUtils.CurrentUserEmail.replace('.', '').replace('@', ''), { opacity: '0.1' });
             const min = Math.round(Date.now() / 1000 / 60);
             numberRange(10).map(i => addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UM-min-' + (min - i), { opacity: ((10 - i - 1) / 10).toString() }));
         }
         if (highlights.includes('By Recent Hour')) {
-            addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UM-' + Doc.CurrentUserEmail.replace('.', '').replace('@', ''), { opacity: '0.1' });
+            addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UM-' + ClientUtils.CurrentUserEmail.replace('.', '').replace('@', ''), { opacity: '0.1' });
             const hr = Math.round(Date.now() / 1000 / 60 / 60);
             numberRange(10).map(i => addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UM-hr-' + (hr - i), { opacity: ((10 - i - 1) / 10).toString() }));
         }
@@ -1214,18 +1217,18 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
             width => this.tryUpdateScrollHeight()
         );
         this._disposers.scrollHeight = reaction(
-            () => ({ scrollHeight: this.scrollHeight, layout_autoHeight: this.layout_autoHeight, width: NumCast(this.layoutDoc._width) }),
-            ({ width, scrollHeight, layout_autoHeight }) => width && layout_autoHeight && this.resetNativeHeight(scrollHeight),
+            () => ({ scrollHeight: this.scrollHeight, layoutAutoHeight: this.layout_autoHeight, width: NumCast(this.layoutDoc._width) }),
+            ({ width, scrollHeight, layoutAutoHeight }) => width && layoutAutoHeight && this.resetNativeHeight(scrollHeight),
             { fireImmediately: true }
         );
         this._disposers.componentHeights = reaction(
             // set the document height when one of the component heights changes and layout_autoHeight is on
-            () => ({ sidebarHeight: this.sidebarHeight, textHeight: this.textHeight, layout_autoHeight: this.layout_autoHeight, marginsHeight: this.layout_autoHeightMargins }),
-            ({ sidebarHeight, textHeight, layout_autoHeight, marginsHeight }) => {
+            () => ({ sidebarHeight: this.sidebarHeight, textHeight: this.textHeight, layoutAutoHeight: this.layout_autoHeight, marginsHeight: this.layout_autoHeightMargins }),
+            ({ sidebarHeight, textHeight, layoutAutoHeight, marginsHeight }) => {
                 const newHeight = this.contentScaling * (marginsHeight + Math.max(sidebarHeight, textHeight));
                 if (
                     (!Array.from(FormattedTextBox._globalHighlights).includes('Bold Text') || this._props.isSelected()) && //
-                    layout_autoHeight &&
+                    layoutAutoHeight &&
                     newHeight &&
                     newHeight !== this.layoutDoc.height &&
                     !this._props.dontRegisterView
@@ -1273,14 +1276,15 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
         this._disposers.search = reaction(
             () => Doc.IsSearchMatch(this.Document),
             search => (search ? this.highlightSearchTerms([Doc.SearchQuery()], search.searchMatch < 0) : this.unhighlightSearchTerms()),
-            { fireImmediately: Doc.IsSearchMatchUnmemoized(this.Document) ? true : false }
+            { fireImmediately: !!Doc.IsSearchMatchUnmemoized(this.Document) }
         );
 
         this._disposers.selected = reaction(
             () => this._props.rootSelected?.(),
             action(selected => {
-                //selected && setTimeout(() => this.prepareForTyping());
+                // selected && setTimeout(() => this.prepareForTyping());
                 if (FormattedTextBox._globalHighlights.has('Bold Text')) {
+                    // eslint-disable-next-line operator-assignment
                     this.layoutDoc[DocCss] = this.layoutDoc[DocCss] + 1; // css change happens outside of mobx/react, so this will notify anyone interested in the layout that it has changed
                 }
                 if (RichTextMenu.Instance?.view === this._editorView && !selected) {
@@ -1321,10 +1325,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
     }
 
     clipboardTextSerializer = (slice: Slice): string => {
-        let text = '',
-            separated = true;
-        const from = 0,
-            to = slice.content.size;
+        let text = '';
+        let separated = true;
+        const from = 0;
+        const to = slice.content.size;
         slice.content.nodesBetween(
             from,
             to,
@@ -1346,7 +1350,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
 
     handlePaste = (view: EditorView, event: Event, slice: Slice): boolean => {
         const pdfAnchorId = (event as ClipboardEvent).clipboardData?.getData('dash/pdfAnchor');
-        return pdfAnchorId && this.addPdfReference(pdfAnchorId) ? true : false;
+        return !!(pdfAnchorId && this.addPdfReference(pdfAnchorId));
     };
 
     addPdfReference = (pdfAnchorId: string) => {
@@ -1389,7 +1393,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
         const self = this;
         return new Plugin({
             view(newView) {
-                runInAction(() => self._props.rootSelected?.() && RichTextMenu.Instance && (RichTextMenu.Instance.view = newView));
+                runInAction(() => {
+                    self._props.rootSelected?.() && RichTextMenu.Instance && (RichTextMenu.Instance.view = newView);
+                });
                 return new RichTextMenuPlugin({ editorProps: this._props });
             },
         });
@@ -1414,7 +1420,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
                         const shift = Math.min(topOff ?? Number.MAX_VALUE, botOff ?? Number.MAX_VALUE);
                         const scrollPos = scrollRef.scrollTop + shift * self.ScreenToLocalBoxXf().Scale;
                         if (this._focusSpeed !== undefined) {
-                            setTimeout(() => scrollPos && (this._scrollStopper = smoothScroll(this._focusSpeed || 0, scrollRef, scrollPos, 'ease', this._scrollStopper)));
+                            setTimeout(() => {
+                                scrollPos && (this._scrollStopper = smoothScroll(this._focusSpeed || 0, scrollRef, scrollPos, 'ease', this._scrollStopper));
+                            });
                         } else {
                             scrollRef.scrollTo({ top: scrollPos });
                         }
@@ -1424,25 +1432,12 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
                 },
                 dispatchTransaction: this.dispatchTransaction,
                 nodeViews: {
-                    dashComment(node: any, view: any, getPos: any) {
-                        return new DashDocCommentView(node, view, getPos);
-                    },
-                    dashDoc(node: any, view: any, getPos: any) {
-                        return new DashDocView(node, view, getPos, self);
-                    },
-                    dashField(node: any, view: any, getPos: any) {
-                        return new DashFieldView(node, view, getPos, self);
-                    },
-                    equation(node: any, view: any, getPos: any) {
-                        return new EquationView(node, view, getPos, self);
-                    },
-                    summary(node: any, view: any, getPos: any) {
-                        return new SummaryView(node, view, getPos);
-                    },
-                    //ordered_list(node: any, view: any, getPos: any) { return new OrderedListView(); },
-                    footnote(node: any, view: any, getPos: any) {
-                        return new FootnoteView(node, view, getPos);
-                    },
+                    dashComment(node: any, view: any, getPos: any) { return new DashDocCommentView(node, view, getPos); }, // prettier-ignore
+                    dashDoc(node: any, view: any, getPos: any)     { return new DashDocView(node, view, getPos, self); }, // prettier-ignore
+                    dashField(node: any, view: any, getPos: any)   { return new DashFieldView(node, view, getPos, self); }, // prettier-ignore
+                    equation(node: any, view: any, getPos: any)    { return new EquationView(node, view, getPos, self); }, // prettier-ignore
+                    summary(node: any, view: any, getPos: any)     { return new SummaryView(node, view, getPos); }, // prettier-ignore
+                    footnote(node: any, view: any, getPos: any)    { return new FootnoteView(node, view, getPos); }, // prettier-ignore
                 },
                 clipboardTextSerializer: this.clipboardTextSerializer,
                 handlePaste: this.handlePaste,
@@ -1450,7 +1445,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
             const { state, dispatch } = this._editorView;
             if (!rtfField) {
                 const dataDoc = Doc.IsDelegateField(DocCast(this.layoutDoc.proto), this.fieldKey) ? DocCast(this.layoutDoc.proto) : this.dataDoc;
-                const startupText = Field.toString(dataDoc[fieldKey] as Field);
+                const startupText = Field.toString(dataDoc[fieldKey] as FieldType);
                 if (startupText) {
                     dispatch(state.tr.insertText(startupText));
                 }
@@ -1474,7 +1469,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
             this._props.select(false);
             if (selLoadChar) {
                 const $from = this._editorView.state.selection.anchor ? this._editorView.state.doc.resolve(this._editorView.state.selection.anchor - 1) : undefined;
-                const mark = schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.floor(Date.now() / 1000) });
+                const mark = schema.marks.user_mark.create({ userid: ClientUtils.CurrentUserEmail, modified: Math.floor(Date.now() / 1000) });
                 const curMarks = this._editorView.state.storedMarks ?? $from?.marksAcross(this._editorView.state.selection.$head) ?? [];
                 const storedMarks = [...curMarks.filter(m => m.type !== mark.type), mark];
                 const tr1 = this._editorView.state.tr.setStoredMarks(storedMarks);
@@ -1510,7 +1505,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
             ...(Doc.UserDoc().fontFamily ? [schema.mark(schema.marks.pFontFamily, { family: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.FontFamily) })] : []),
             ...(Doc.UserDoc().fontSize ? [schema.mark(schema.marks.pFontSize, { fontSize: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.FontSize) })] : []),
             ...(Doc.UserDoc().fontWeight === 'bold' ? [schema.mark(schema.marks.strong)] : []),
-            ...[schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.floor(Date.now() / 1000) })],
+            ...[schema.marks.user_mark.create({ userid: ClientUtils.CurrentUserEmail, modified: Math.floor(Date.now() / 1000) })],
         ];
         this._editorView?.dispatch(this._editorView?.state.tr.setStoredMarks(docDefaultMarks));
     };
@@ -1761,7 +1756,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
         let stopPropagation = true;
         for (var i = state.selection.from; i <= state.selection.to; i++) {
             const node = state.doc.resolve(i);
-            if (state.doc.content.size - 1 > i && node?.marks?.().some(mark => mark.type === schema.marks.user_mark && mark.attrs.userid !== Doc.CurrentUserEmail) && [AclAugment, AclSelfEdit].includes(GetEffectiveAcl(this.Document))) {
+            if (state.doc.content.size - 1 > i && node?.marks?.().some(mark => mark.type === schema.marks.user_mark && mark.attrs.userid !== ClientUtils.CurrentUserEmail) && [AclAugment, AclSelfEdit].includes(GetEffectiveAcl(this.Document))) {
                 e.preventDefault();
             }
         }
@@ -1781,11 +1776,11 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
                 this._editorView?.state.selection.empty && (stopPropagation = false);
                 break;
             default:
-                if (this._lastTimedMark?.attrs.userid === Doc.CurrentUserEmail) break;
+                if (this._lastTimedMark?.attrs.userid === ClientUtils.CurrentUserEmail) break;
             case ' ':
                 if (e.code !== 'Space' && e.code !== 'Backspace') {
                     [AclEdit, AclAugment, AclAdmin].includes(GetEffectiveAcl(this.Document)) &&
-                        this._editorView!.dispatch(this._editorView!.state.tr.removeStoredMark(schema.marks.user_mark).addStoredMark(schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.floor(Date.now() / 1000) })));
+                        this._editorView!.dispatch(this._editorView!.state.tr.removeStoredMark(schema.marks.user_mark).addStoredMark(schema.marks.user_mark.create({ userid: ClientUtils.CurrentUserEmail, modified: Math.floor(Date.now() / 1000) })));
                 }
                 break;
         }
@@ -1892,7 +1887,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
                     Document={this.Document}
                     layoutDoc={this.layoutDoc}
                     dataDoc={this.dataDoc}
-                    usePanelWidth={true}
+                    usePanelWidth
                     nativeWidth={NumCast(this.layoutDoc._nativeWidth)}
                     showSidebar={this.SidebarShown}
                     whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
@@ -2000,11 +1995,11 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
         // if scrollTop is 0, then don't let wheel trigger scroll on any container (which it would since onScroll won't be triggered on this)
         if (this._props.isContentActive()) {
             const scale = this._props.NativeDimScaling?.() || 1;
-            const styleFromLayoutString = Doc.styleFromLayoutString(this.Document, this._props, scale); // this converts any expressions in the format string to style props.  e.g., <FormattedTextBox height='{this._header_height}px' >
-            const height = Number(styleFromLayoutString.height?.replace('px', ''));
+            const styleFromLayout = styleFromLayoutString(this.Document, this._props, scale); // this converts any expressions in the format string to style props.  e.g., <FormattedTextBox height='{this._header_height}px' >
+            const height = Number(styleFromLayout.height?.replace('px', ''));
             // prevent default if selected || child is active  but this doc isn't scrollable
             if (
-                !Number.isNaN(height) &&
+                !isNaN(height) &&
                 (this._scrollRef?.scrollHeight ?? 0) <= Math.ceil((height ? height : this._props.PanelHeight()) / scale) && //
                 (this._props.rootSelected?.() || this.isAnyChildContentActive())
             ) {
@@ -2033,15 +2028,15 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
         setTimeout(() => !this._props.isContentActive() && FormattedTextBoxComment.textBox === this && FormattedTextBoxComment.Hide);
         const paddingX = NumCast(this.layoutDoc._xMargin, this._props.xPadding || 0);
         const paddingY = NumCast(this.layoutDoc._yMargin, this._props.yPadding || 0);
-        const styleFromLayoutString = Doc.styleFromLayoutString(this.Document, this._props, scale); // this converts any expressions in the format string to style props.  e.g., <FormattedTextBox height='{this._header_height}px' >
-        return styleFromLayoutString?.height === '0px' ? null : (
+        const styleFromLayout = styleFromLayoutString(this.Document, this._props, scale); // this converts any expressions in the format string to style props.  e.g., <FormattedTextBox height='{this._header_height}px' >
+        return styleFromLayout?.height === '0px' ? null : (
             <div
                 className="formattedTextBox"
                 onPointerEnter={action(() => {
                     this._isHovering = true;
                     this.layoutDoc[`_${this._props.fieldKey}_usePath`] && (this.Document.isHovering = true);
                 })}
-                onPointerLeave={action(() => (this.Document.isHovering = this._isHovering = false))}
+                onPointerLeave={action(() => { this.Document.isHovering = this._isHovering = false; })} // prettier-ignore
                 ref={r => {
                     this._oldWheel?.removeEventListener('wheel', this.onPassiveWheel);
                     this._oldWheel = r;
@@ -2061,7 +2056,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
                     fontSize: this.fontSize,
                     fontFamily: this.fontFamily,
                     fontWeight: this.fontWeight,
-                    ...styleFromLayoutString,
+                    ...styleFromLayout,
                 }}>
                 <div
                     className="formattedTextBox-cont"
@@ -2083,7 +2078,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
                     onDoubleClick={this.onDoubleClick}>
                     <div
                         className="formattedTextBox-outer"
-                        ref={r => (this._scrollRef = r)}
+                        ref={r => {
+                            this._scrollRef = r;
+                        }}
                         style={{
                             width: this.noSidebar ? '100%' : `calc(100% - ${this.layout_sidebarWidthPercent})`,
                             overflow: this.layoutDoc._createDocOnCR ? 'hidden' : this.layoutDoc._layout_autoHeight ? 'visible' : undefined,
diff --git a/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx b/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx
index ce17af6ca..c0cb60c6d 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx
@@ -1,15 +1,16 @@
 import { Mark, ResolvedPos } from 'prosemirror-model';
-import { EditorState, NodeSelection } from 'prosemirror-state';
+import { EditorState } from 'prosemirror-state';
 import { EditorView } from 'prosemirror-view';
+import { ClientUtils } from '../../../../ClientUtils';
 import { Doc } from '../../../../fields/Doc';
 import { DocServer } from '../../../DocServer';
-import { LinkDocPreview, LinkInfo } from '../LinkDocPreview';
+import { LinkInfo } from '../LinkDocPreview';
 import { FormattedTextBox } from './FormattedTextBox';
 import './FormattedTextBoxComment.scss';
 import { schema } from './schema_rts';
 
 export function findOtherUserMark(marks: readonly Mark[]): Mark | undefined {
-    return marks.find(m => m.attrs.userid && m.attrs.userid !== Doc.CurrentUserEmail);
+    return marks.find(m => m.attrs.userid && m.attrs.userid !== ClientUtils.CurrentUserEmail);
 }
 export function findUserMark(marks: readonly Mark[]): Mark | undefined {
     return marks.find(m => m.attrs.userid);
diff --git a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts
index 03c902580..e9ed2549e 100644
--- a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts
+++ b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts
@@ -1,28 +1,28 @@
 import { chainCommands, deleteSelection, exitCode, joinBackward, joinDown, joinUp, lift, newlineInCode, selectNodeBackward, setBlockType, splitBlockKeepMarks, toggleMark, wrapIn } from 'prosemirror-commands';
 import { redo, undo } from 'prosemirror-history';
 import { Schema } from 'prosemirror-model';
-import { splitListItem, wrapInList, sinkListItem, liftListItem } from 'prosemirror-schema-list';
+import { liftListItem, sinkListItem, splitListItem, wrapInList } from 'prosemirror-schema-list';
 import { EditorState, NodeSelection, TextSelection, Transaction } from 'prosemirror-state';
 import { liftTarget } from 'prosemirror-transform';
+import { EditorView } from 'prosemirror-view';
+import { ClientUtils } from '../../../../ClientUtils';
+import { Utils } from '../../../../Utils';
 import { AclAdmin, AclAugment, AclEdit } from '../../../../fields/DocSymbols';
 import { GetEffectiveAcl } from '../../../../fields/util';
-import { Utils } from '../../../../Utils';
 import { Docs } from '../../../documents/Documents';
 import { RTFMarkup } from '../../../util/RTFMarkup';
 import { SelectionManager } from '../../../util/SelectionManager';
 import { OpenWhere } from '../DocumentView';
-import { Doc } from '../../../../fields/Doc';
-import { EditorView } from 'prosemirror-view';
 
 const mac = typeof navigator !== 'undefined' ? /Mac/.test(navigator.platform) : false;
 
 export type KeyMap = { [key: string]: any };
 
-export let updateBullets = (tx2: Transaction, schema: Schema, assignedMapStyle?: string, from?: number, to?: number) => {
+export const updateBullets = (tx2: Transaction, schema: Schema, assignedMapStyle?: string, from?: number, to?: number) => {
     let mapStyle = assignedMapStyle;
-    tx2.doc.descendants((node: any, offset: any, index: any) => {
+    tx2.doc.descendants((node: any, offset: any /* , index: any */) => {
         if ((from === undefined || to === undefined || (from <= offset + node.nodeSize && to >= offset)) && (node.type === schema.nodes.ordered_list || node.type === schema.nodes.list_item)) {
-            const path = (tx2.doc.resolve(offset) as any).path;
+            const { path } = tx2.doc.resolve(offset) as any;
             let depth = Array.from(path).reduce((p: number, c: any) => p + (c.hasOwnProperty('type') && c.type === schema.nodes.ordered_list ? 1 : 0), 0);
             if (node.type === schema.nodes.ordered_list) {
                 if (depth === 0 && !assignedMapStyle) mapStyle = node.attrs.mapStyle;
@@ -49,23 +49,27 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey
     const canEdit = (state: any) => {
         switch (GetEffectiveAcl(props.TemplateDataDocument)) {
             case AclAugment:
-                const prevNode = state.selection.$cursor.nodeBefore;
-                const prevUser = !prevNode ? Doc.CurrentUserEmail : prevNode.marks[prevNode.marks.length - 1].attrs.userid;
-                if (prevUser != Doc.CurrentUserEmail) {
-                    return false;
+                {
+                    const prevNode = state.selection.$cursor.nodeBefore;
+                    const prevUser = !prevNode ? ClientUtils.CurrentUserEmail : prevNode.marks[prevNode.marks.length - 1].attrs.userid;
+                    if (prevUser !== ClientUtils.CurrentUserEmail) {
+                        return false;
+                    }
                 }
+                break;
+            default:
         }
         return true;
     };
 
     const toggleEditableMark = (mark: any) => (state: EditorState, dispatch: (tx: Transaction) => void) => canEdit(state) && toggleMark(mark)(state, dispatch);
 
-    //History commands
+    // History commands
     bind('Mod-z', undo);
     bind('Shift-Mod-z', redo);
     !mac && bind('Mod-y', redo);
 
-    //Commands to modify Mark
+    // Commands to modify Mark
     bind('Mod-b', toggleEditableMark(schema.marks.strong));
     bind('Mod-B', toggleEditableMark(schema.marks.strong));
 
@@ -77,7 +81,7 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey
     bind('Mod-u', toggleEditableMark(schema.marks.underline));
     bind('Mod-U', toggleEditableMark(schema.marks.underline));
 
-    //Commands for lists
+    // Commands for lists
     bind('Ctrl-i', (state: EditorState, dispatch: (tx: Transaction) => void) => canEdit(state) && wrapInList(schema.nodes.ordered_list)(state as any, dispatch as any));
 
     bind('Ctrl-Tab', () => (props.onKey?.(event, props) ? true : true));
@@ -103,8 +107,8 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey
             if (
                 !wrapInList(schema.nodes.ordered_list)(newstate.state as any, (tx2: Transaction) => {
                     const tx25 = updateBullets(tx2, schema);
-                    const ol_node = tx25.doc.nodeAt(range!.start)!;
-                    const tx3 = tx25.setNodeMarkup(range!.start, ol_node.type, ol_node.attrs, marks);
+                    const olNode = tx25.doc.nodeAt(range!.start)!;
+                    const tx3 = tx25.setNodeMarkup(range!.start, olNode.type, olNode.attrs, marks);
                     // when promoting to a list, assume list will format things so don't copy the stored marks.
                     marks && tx3.ensureMarks([...marks]);
                     marks && tx3.setStoredMarks([...marks]);
@@ -134,13 +138,13 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey
         }
     });
 
-    //Command to create a new Tab with a PDF of all the command shortcuts
+    // Command to create a new Tab with a PDF of all the command shortcuts
     bind('Mod-/', (state: EditorState, dispatch: (tx: Transaction) => void) => {
-        const newDoc = Docs.Create.PdfDocument(Utils.prepend('/assets/cheat-sheet.pdf'), { _width: 300, _height: 300 });
+        const newDoc = Docs.Create.PdfDocument(ClientUtils.prepend('/assets/cheat-sheet.pdf'), { _width: 300, _height: 300 });
         props.addDocTab(newDoc, OpenWhere.addRight);
     });
 
-    //Commands to modify BlockType
+    // Commands to modify BlockType
     bind('Ctrl->', (state: EditorState, dispatch: (tx: Transaction) => void) => canEdit(state && wrapIn(schema.nodes.blockquote)(state as any, dispatch as any)));
     bind('Alt-\\', (state: EditorState, dispatch: (tx: Transaction) => void) => canEdit(state) && setBlockType(schema.nodes.paragraph)(state as any, dispatch as any));
     bind('Shift-Ctrl-\\', (state: EditorState, dispatch: (tx: Transaction) => void) => canEdit(state) && setBlockType(schema.nodes.code_block)(state as any, dispatch as any));
@@ -156,11 +160,11 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey
         bind('Shift-Ctrl-' + i, (state: EditorState, dispatch: (tx: Transaction) => void) => canEdit(state) && setBlockType(schema.nodes.heading, { level: i })(state as any, dispatch as any));
     }
 
-    //Command to create a horizontal break line
+    // Command to create a horizontal break line
     const hr = schema.nodes.horizontal_rule;
     bind('Mod-_', (state: EditorState, dispatch: (tx: Transaction) => void) => canEdit(state) && dispatch(state.tr.replaceSelectionWith(hr.create()).scrollIntoView()));
 
-    //Command to unselect all
+    // Command to unselect all
     bind('Escape', (state: EditorState, dispatch: (tx: Transaction) => void) => {
         dispatch(state.tr.setSelection(TextSelection.create(state.doc, state.selection.from, state.selection.from)));
         (document.activeElement as any).blur?.();
@@ -189,7 +193,7 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey
     });
     bind('Cmd-]', (state: EditorState, dispatch: (tx: Transaction) => void) => {
         const resolved = state.doc.resolve(state.selection.from) as any;
-        const tr = state.tr;
+        const { tr } = state;
         if (resolved?.parent.type.name === 'paragraph') {
             tr.setNodeMarkup(resolved.path[resolved.path.length - 4], schema.nodes.paragraph, { ...resolved.parent.attrs, align: 'right' }, resolved.parent.marks);
         } else {
@@ -204,7 +208,7 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey
     });
     bind('Cmd-\\', (state: EditorState, dispatch: (tx: Transaction) => void) => {
         const resolved = state.doc.resolve(state.selection.from) as any;
-        const tr = state.tr;
+        const { tr } = state;
         if (resolved?.parent.type.name === 'paragraph') {
             tr.setNodeMarkup(resolved.path[resolved.path.length - 4], schema.nodes.paragraph, { ...resolved.parent.attrs, align: 'center' }, resolved.parent.marks);
         } else {
@@ -219,7 +223,7 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey
     });
     bind('Cmd-[', (state: EditorState, dispatch: (tx: Transaction) => void) => {
         const resolved = state.doc.resolve(state.selection.from) as any;
-        const tr = state.tr;
+        const { tr } = state;
         if (resolved?.parent.type.name === 'paragraph') {
             tr.setNodeMarkup(resolved.path[resolved.path.length - 4], schema.nodes.paragraph, { ...resolved.parent.attrs, align: 'left' }, resolved.parent.marks);
         } else {
@@ -236,7 +240,7 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey
     bind('Cmd-f', (state: EditorState, dispatch: (tx: Transaction) => void) => {
         const content = state.tr.selection.empty ? undefined : state.tr.selection.content().content.textBetween(0, state.tr.selection.content().size + 1);
         const newNode = schema.nodes.footnote.create({}, content ? state.schema.text(content) : undefined);
-        const tr = state.tr;
+        const { tr } = state;
         tr.replaceSelectionWith(newNode); // replace insertion with a footnote.
         dispatch(
             tr.setSelection(
@@ -288,8 +292,8 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey
     };
     bind('Backspace', backspace);
 
-    //newlineInCode, createParagraphNear, liftEmptyBlock, splitBlock
-    //command to break line
+    // newlineInCode, createParagraphNear, liftEmptyBlock, splitBlock
+    // command to break line
 
     const enter = (state: EditorState, dispatch: (tx: Transaction) => void, view: EditorView, once = true) => {
         if (props.onKey?.(event, props)) return true;
@@ -356,7 +360,7 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey
     };
     bind('Enter', enter);
 
-    //Command to create a blank space
+    // Command to create a blank space
     bind('Space', (state: EditorState, dispatch: (tx: Transaction) => void) => {
         if (props.TemplateDataDocument && GetEffectiveAcl(props.TemplateDataDocument) != AclEdit && GetEffectiveAcl(props.TemplateDataDocument) != AclAugment && GetEffectiveAcl(props.TemplateDataDocument) != AclAdmin) return true;
         return false;
diff --git a/src/client/views/nodes/formattedText/RichTextMenu.tsx b/src/client/views/nodes/formattedText/RichTextMenu.tsx
index cecf106a3..ec9c1a15d 100644
--- a/src/client/views/nodes/formattedText/RichTextMenu.tsx
+++ b/src/client/views/nodes/formattedText/RichTextMenu.tsx
@@ -3,7 +3,7 @@ import { Tooltip } from '@mui/material';
 import { action, computed, IReactionDisposer, makeObservable, observable, reaction } from 'mobx';
 import { observer } from 'mobx-react';
 import { lift, wrapIn } from 'prosemirror-commands';
-import { Mark, MarkType, Node as ProsNode, ResolvedPos } from 'prosemirror-model';
+import { Mark, MarkType } from 'prosemirror-model';
 import { wrapInList } from 'prosemirror-schema-list';
 import { EditorState, NodeSelection, TextSelection } from 'prosemirror-state';
 import { EditorView } from 'prosemirror-view';
diff --git a/src/client/views/nodes/formattedText/RichTextRules.ts b/src/client/views/nodes/formattedText/RichTextRules.ts
index 42665830f..78ea99592 100644
--- a/src/client/views/nodes/formattedText/RichTextRules.ts
+++ b/src/client/views/nodes/formattedText/RichTextRules.ts
@@ -1,5 +1,6 @@
 import { ellipsis, emDash, InputRule, smartQuotes, textblockTypeInputRule } from 'prosemirror-inputrules';
 import { NodeSelection, TextSelection } from 'prosemirror-state';
+import { ClientUtils } from '../../../../ClientUtils';
 import { Doc, DocListCast, FieldResult, StrListCast } from '../../../../fields/Doc';
 import { DocData } from '../../../../fields/DocSymbols';
 import { Id } from '../../../../fields/FieldSymbols';
@@ -48,13 +49,9 @@ export class RichTextRules {
                 /^A\.\s$/,
                 schema.nodes.ordered_list,
                 // match => {
-                () => {
-                    return { mapStyle: 'multi', bulletStyle: 1 };
-                    // return ({ order: +match[1] })
-                },
-                (match: any, node: any) => {
-                    return node.childCount + node.attrs.order === +match[1];
-                },
+                () => ({ mapStyle: 'multi', bulletStyle: 1 }),
+                // return ({ order: +match[1] })
+                (match: any, node: any) => node.childCount + node.attrs.order === +match[1],
                 ((type: any) => ({ type: type, attrs: { mapStyle: 'multi', bulletStyle: 1 } })) as any
             ),
 
@@ -70,7 +67,7 @@ export class RichTextRules {
 
             // ``` create code block
             new InputRule(/^```$/, (state, match, start, end) => {
-                let $start = state.doc.resolve(start);
+                const $start = state.doc.resolve(start);
                 if (!$start.node(-1).canReplaceWith($start.index(-1), $start.indexAfter(-1), schema.nodes.code_block)) return null;
 
                 // this enables text with code blocks to be used as a 'paint' function via a styleprovider button that is added to Docs that have an onPaint script
@@ -86,13 +83,13 @@ export class RichTextRules {
             }),
 
             // %<font-size> set the font size
-            new InputRule(new RegExp(/%([0-9]+)\s$/), (state, match, start, end) => {
+            new InputRule(/%([0-9]+)\s$/, (state, match, start, end) => {
                 const size = Number(match[1]);
                 return state.tr.deleteRange(start, end).addStoredMark(schema.marks.pFontSize.create({ fontSize: size }));
             }),
 
-            //Create annotation to a field on the text document
-            new InputRule(new RegExp(/>::$/), (state, match, start, end) => {
+            // Create annotation to a field on the text document
+            new InputRule(/>::$/, (state, match, start, end) => {
                 const creator = (doc: Doc) => {
                     const textDoc = this.Document[DocData];
                     const numInlines = NumCast(textDoc.inlineTextCount);
@@ -107,7 +104,7 @@ export class RichTextRules {
                                   .insert(start, newNode)
                                   .replaceRangeWith(start + 1, end + 2, dashDoc)
                                   .insertText(' ', start + 2)
-                                  .setStoredMarks([...node.marks, ...(sm ? sm : [])])
+                                  .setStoredMarks([...node.marks, ...(sm || [])])
                             : this.TextBox.EditorView.state.tr
                     );
                 };
@@ -117,8 +114,8 @@ export class RichTextRules {
 
                 return null;
             }),
-            //Create annotation to a field on the text document
-            new InputRule(new RegExp(/>>$/), (state, match, start, end) => {
+            // Create annotation to a field on the text document
+            new InputRule(/>>$/, (state, match, start, end) => {
                 const textDoc = this.Document[DocData];
                 const numInlines = NumCast(textDoc.inlineTextCount);
                 textDoc.inlineTextCount = numInlines + 1;
@@ -150,13 +147,13 @@ export class RichTextRules {
                           .insert(start, newNode)
                           .replaceRangeWith(start + 1, end + 1, dashDoc)
                           .insertText(' ', start + 2)
-                          .setStoredMarks([...node.marks, ...(sm ? sm : [])])
+                          .setStoredMarks([...node.marks, ...(sm || [])])
                     : state.tr;
                 return replaced;
             }),
 
             // set the First-line indent node type for the selection's paragraph (assumes % was used to initiate an EnteringStyle mode)
-            new InputRule(new RegExp(/(%d|d)$/), (state, match, start, end) => {
+            new InputRule(/(%d|d)$/, (state, match, start, end) => {
                 if (!match[0].startsWith('%') && !this.EnteringStyle) return null;
                 const pos = state.doc.resolve(start) as any;
                 for (let depth = pos.path.length / 3 - 1; depth >= 0; depth--) {
@@ -171,7 +168,7 @@ export class RichTextRules {
             }),
 
             // set the Hanging indent node type for the current selection's paragraph (assumes % was used to initiate an EnteringStyle mode)
-            new InputRule(new RegExp(/(%h|h)$/), (state, match, start, end) => {
+            new InputRule(/(%h|h)$/, (state, match, start, end) => {
                 if (!match[0].startsWith('%') && !this.EnteringStyle) return null;
                 const pos = state.doc.resolve(start) as any;
                 for (let depth = pos.path.length / 3 - 1; depth >= 0; depth--) {
@@ -186,11 +183,11 @@ export class RichTextRules {
             }),
 
             // set the Quoted indent node type for the current selection's paragraph (assumes % was used to initiate an EnteringStyle mode)
-            new InputRule(new RegExp(/(%q|q)$/), (state, match, start, end) => {
+            new InputRule(/(%q|q)$/, (state, match, start, end) => {
                 if (!match[0].startsWith('%') && !this.EnteringStyle) return null;
                 const pos = state.doc.resolve(start) as any;
                 if (state.selection instanceof NodeSelection && state.selection.node.type === schema.nodes.ordered_list) {
-                    const node = state.selection.node;
+                    const { node } = state.selection;
                     return state.tr.setNodeMarkup(pos.pos, node.type, { ...node.attrs, indent: node.attrs.indent === 30 ? undefined : 30 });
                 }
                 for (let depth = pos.path.length / 3 - 1; depth >= 0; depth--) {
@@ -205,46 +202,43 @@ export class RichTextRules {
             }),
 
             // center justify text
-            new InputRule(new RegExp(/%\^/), (state, match, start, end) => {
+            new InputRule(/%\^/, (state, match, start, end) => {
                 const resolved = state.doc.resolve(start) as any;
                 if (resolved?.parent.type.name === 'paragraph') {
                     return state.tr.deleteRange(start, end).setNodeMarkup(resolved.path[resolved.path.length - 4], schema.nodes.paragraph, { ...resolved.parent.attrs, align: 'center' }, resolved.parent.marks);
-                } else {
-                    const node = resolved.nodeAfter;
-                    const sm = state.storedMarks || undefined;
-                    const replaced = node ? state.tr.replaceRangeWith(start, end, schema.nodes.paragraph.create({ align: 'center' })).setStoredMarks([...node.marks, ...(sm ? sm : [])]) : state.tr;
-                    return replaced.setSelection(new TextSelection(replaced.doc.resolve(end - 2)));
                 }
+                const node = resolved.nodeAfter;
+                const sm = state.storedMarks || undefined;
+                const replaced = node ? state.tr.replaceRangeWith(start, end, schema.nodes.paragraph.create({ align: 'center' })).setStoredMarks([...node.marks, ...(sm || [])]) : state.tr;
+                return replaced.setSelection(new TextSelection(replaced.doc.resolve(end - 2)));
             }),
 
             // left justify text
-            new InputRule(new RegExp(/%\[/), (state, match, start, end) => {
+            new InputRule(/%\[/, (state, match, start, end) => {
                 const resolved = state.doc.resolve(start) as any;
                 if (resolved?.parent.type.name === 'paragraph') {
                     return state.tr.deleteRange(start, end).setNodeMarkup(resolved.path[resolved.path.length - 4], schema.nodes.paragraph, { ...resolved.parent.attrs, align: 'left' }, resolved.parent.marks);
-                } else {
-                    const node = resolved.nodeAfter;
-                    const sm = state.storedMarks || undefined;
-                    const replaced = node ? state.tr.replaceRangeWith(start, end, schema.nodes.paragraph.create({ align: 'left' })).setStoredMarks([...node.marks, ...(sm ? sm : [])]) : state.tr;
-                    return replaced.setSelection(new TextSelection(replaced.doc.resolve(end - 2)));
                 }
+                const node = resolved.nodeAfter;
+                const sm = state.storedMarks || undefined;
+                const replaced = node ? state.tr.replaceRangeWith(start, end, schema.nodes.paragraph.create({ align: 'left' })).setStoredMarks([...node.marks, ...(sm || [])]) : state.tr;
+                return replaced.setSelection(new TextSelection(replaced.doc.resolve(end - 2)));
             }),
 
             // right justify text
-            new InputRule(new RegExp(/%\]/), (state, match, start, end) => {
+            new InputRule(/%\]/, (state, match, start, end) => {
                 const resolved = state.doc.resolve(start) as any;
                 if (resolved?.parent.type.name === 'paragraph') {
                     return state.tr.deleteRange(start, end).setNodeMarkup(resolved.path[resolved.path.length - 4], schema.nodes.paragraph, { ...resolved.parent.attrs, align: 'right' }, resolved.parent.marks);
-                } else {
-                    const node = resolved.nodeAfter;
-                    const sm = state.storedMarks || undefined;
-                    const replaced = node ? state.tr.replaceRangeWith(start, end, schema.nodes.paragraph.create({ align: 'right' })).setStoredMarks([...node.marks, ...(sm ? sm : [])]) : state.tr;
-                    return replaced.setSelection(new TextSelection(replaced.doc.resolve(end - 2)));
                 }
+                const node = resolved.nodeAfter;
+                const sm = state.storedMarks || undefined;
+                const replaced = node ? state.tr.replaceRangeWith(start, end, schema.nodes.paragraph.create({ align: 'right' })).setStoredMarks([...node.marks, ...(sm || [])]) : state.tr;
+                return replaced.setSelection(new TextSelection(replaced.doc.resolve(end - 2)));
             }),
 
             // activate a style by name using prefix '%<color name>'
-            new InputRule(new RegExp(/%[a-zA-Z_]+$/), (state, match, start, end) => {
+            new InputRule(/%[a-zA-Z_]+$/, (state, match, start, end) => {
                 const color = match[0].substring(1, match[0].length);
                 const marks = RichTextMenu.Instance?._brushMap.get(color);
 
@@ -276,13 +270,13 @@ export class RichTextRules {
             }),
 
             // toggle alternate text UI  %/
-            new InputRule(new RegExp(/%\//), (state, match, start, end) => {
+            new InputRule(/%\//, (state, match, start, end) => {
                 setTimeout(() => this.TextBox.cycleAlternateText(true));
                 return state.tr.deleteRange(start, end);
             }),
 
             // stop using active style
-            new InputRule(new RegExp(/%%$/), (state, match, start, end) => {
+            new InputRule(/%%$/, (state, match, start, end) => {
                 const tr = state.tr.deleteRange(start, end);
                 const marks = state.tr.selection.$anchor.nodeBefore?.marks;
 
@@ -295,7 +289,7 @@ export class RichTextRules {
 
             // create a hyperlink to a titled document
             // @(<doctitle>)
-            new InputRule(new RegExp(/@\(([a-zA-Z_@\.\? \-0-9]+)\)/), (state, match, start, end) => {
+            new InputRule(/@\(([a-zA-Z_@.? \-0-9]+)\)/, (state, match, start, end) => {
                 const docTitle = match[1];
                 const prefixLength = '@('.length;
                 if (docTitle) {
@@ -335,7 +329,7 @@ export class RichTextRules {
             // [@{this,doctitle,}.fieldKey{:,=,:=,=:=}value]
             // [@{this,doctitle,}.fieldKey]
             new InputRule(
-                new RegExp(/\[(@|@this\.|@[a-zA-Z_\? \-0-9]+\.)([a-zA-Z_\?\-0-9]+)((:|=|:=|=:=)([a-zA-Z,_\(\)\.@\?\+\-\*\/\ 0-9\(\)]*))?\]/),
+                /\[(@|@this\.|@[a-zA-Z_? \-0-9]+\.)([a-zA-Z_?\-0-9]+)((:|=|:=|=:=)([a-zA-Z,_().@?+\-*/ 0-9()]*))?\]/,
                 (state, match, start, end) => {
                     const docTitle = match[1].substring(1).replace(/\.$/, '');
                     const fieldKey = match[2];
@@ -349,8 +343,17 @@ export class RichTextRules {
                         const strs = values.some(v => !v.match(/^[-]?[0-9.]$/));
                         this.Document[DocData][fieldKey] = strs ? new List<string>(values) : new List<number>(values.map(v => Number(v)));
                     } else if (value) {
-                        KeyValueBox.SetField(this.Document, fieldKey, assign + value, Doc.IsDataProto(this.Document) ? true : undefined, assign.includes(":=") ? undefined:
-                            (gptval: FieldResult) => (dataDoc ? this.Document[DocData]:this.Document)[fieldKey] = gptval as string ); // prettier-ignore
+                        KeyValueBox.SetField(
+                            this.Document,
+                            fieldKey,
+                            assign + value,
+                            Doc.IsDataProto(this.Document) ? true : undefined,
+                            assign.includes(':=')
+                                ? undefined
+                                : (gptval: FieldResult) => {
+                                      (dataDoc ? this.Document[DocData] : this.Document)[fieldKey] = gptval as string;
+                                  }
+                        );
                         if (fieldKey === this.TextBox.fieldKey) return this.TextBox.EditorView!.state.tr;
                     }
                     const target = docTitle ? getTitledDoc(docTitle) : undefined;
@@ -361,8 +364,8 @@ export class RichTextRules {
             ),
 
             // pass the contents between '((' and '))' to chatGPT and append the result
-            new InputRule(new RegExp(/(^|[^=])(\(\(.*\)\))$/), (state, match, start, end) => {
-                var count = 0; // ignore first return value which will be the notation that chat is pending a result
+            new InputRule(/(^|[^=])(\(\(.*\)\))$/, (state, match, start, end) => {
+                let count = 0; // ignore first return value which will be the notation that chat is pending a result
                 KeyValueBox.SetField(this.Document, '', match[2], false, (gptval: FieldResult) => {
                     if (count) {
                         const tr = this.TextBox.EditorView?.state.tr.insertText(' ' + (gptval as string));
@@ -376,7 +379,7 @@ export class RichTextRules {
 
             // create a text display of a metadata field on this or another document, or create a hyperlink portal to another document
             // @(wiki:title)
-            new InputRule(new RegExp(/@\(wiki:([a-zA-Z_@:\.\?\-0-9 ]+)\)$/), (state, match, start, end) => {
+            new InputRule(/@\(wiki:([a-zA-Z_@:.?\-0-9 ]+)\)$/, (state, match, start, end) => {
                 const title = match[1].trim().replace(/ /g, '_');
                 this.TextBox.EditorView?.dispatch(state.tr.setSelection(new TextSelection(state.doc.resolve(start), state.doc.resolve(end))));
 
@@ -392,7 +395,7 @@ export class RichTextRules {
 
             // create an inline equation node
             // %eq
-            new InputRule(new RegExp(/%eq/), (state, match, start, end) => {
+            new InputRule(/%eq/, (state, match, start, end) => {
                 const fieldKey = 'math' + Utils.GenerateGuid();
                 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 }));
@@ -400,10 +403,10 @@ export class RichTextRules {
             }),
 
             // create an inline view of a tag stored under the '#' field
-            new InputRule(new RegExp(/#([a-zA-Z_\-]+[a-zA-Z_\-0-9]*)\s$/), (state, match, start, end) => {
+            new InputRule(/#([a-zA-Z_-]+[a-zA-Z_\-0-9]*)\s$/, (state, match, start, end) => {
                 const tag = match[1];
                 if (!tag) return state.tr;
-                //this.Document[DocData]['#' + tag] = '#' + tag;
+                // this.Document[DocData]['#' + tag] = '#' + tag;
                 const tags = StrListCast(this.Document[DocData].tags);
                 if (!tags.includes(tag)) {
                     tags.push(tag);
@@ -417,29 +420,25 @@ export class RichTextRules {
             }),
 
             // # heading
-            textblockTypeInputRule(new RegExp(/^(#{1,6})\s$/), schema.nodes.heading, match => {
-                return { level: match[1].length };
-            }),
+            textblockTypeInputRule(/^(#{1,6})\s$/, schema.nodes.heading, match => ({ level: match[1].length })),
 
             // set the Todo user-tag on the current selection (assumes % was used to initiate an EnteringStyle mode)
-            new InputRule(new RegExp(/[ti!x]$/), (state, match, start, end) => {
+            new InputRule(/[ti!x]$/, (state, match, start, end) => {
                 if (state.selection.to === state.selection.from || !this.EnteringStyle) return null;
 
                 const tag = match[0] === 't' ? 'todo' : match[0] === 'i' ? 'ignore' : match[0] === 'x' ? 'disagree' : match[0] === '!' ? 'important' : '??';
                 const node = (state.doc.resolve(start) as any).nodeAfter;
 
                 if (node?.marks.findIndex((m: any) => m.type === schema.marks.user_tag) !== -1) return state.tr.removeMark(start, end, schema.marks.user_tag);
-                if (node?.marks.findIndex((m: any) => m.type === schema.marks.user_mark) !== -1) {
-                }
                 return node
                     ? state.tr
                           .removeMark(start, end, schema.marks.user_mark)
-                          .addMark(start, end, schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.floor(Date.now() / 1000) }))
-                          .addMark(start, end, schema.marks.user_tag.create({ userid: Doc.CurrentUserEmail, tag: tag, modified: Math.round(Date.now() / 1000 / 60) }))
+                          .addMark(start, end, schema.marks.user_mark.create({ userid: ClientUtils.CurrentUserEmail, modified: Math.floor(Date.now() / 1000) }))
+                          .addMark(start, end, schema.marks.user_tag.create({ userid: ClientUtils.CurrentUserEmail, tag: tag, modified: Math.round(Date.now() / 1000 / 60) }))
                     : state.tr;
             }),
 
-            new InputRule(new RegExp(/%\(/), (state, match, start, end) => {
+            new InputRule(/%\(/, (state, match, start, end) => {
                 const node = (state.doc.resolve(start) as any).nodeAfter;
                 const sm = state.storedMarks?.slice() || [];
                 const mark = state.schema.marks.summarizeInclusive.create();
@@ -452,9 +451,7 @@ export class RichTextRules {
                 return replaced.setSelection(new TextSelection(replaced.doc.resolve(end))).setStoredMarks([...node.marks, ...sm]);
             }),
 
-            new InputRule(new RegExp(/%\)/), (state, match, start, end) => {
-                return state.tr.deleteRange(start, end).removeStoredMark(state.schema.marks.summarizeInclusive.create());
-            }),
+            new InputRule(/%\)/, (state, match, start, end) => state.tr.deleteRange(start, end).removeStoredMark(state.schema.marks.summarizeInclusive.create())),
         ],
     };
 }
diff --git a/src/client/views/nodes/formattedText/marks_rts.ts b/src/client/views/nodes/formattedText/marks_rts.ts
index ccf7de4a1..8f716ad7a 100644
--- a/src/client/views/nodes/formattedText/marks_rts.ts
+++ b/src/client/views/nodes/formattedText/marks_rts.ts
@@ -1,6 +1,5 @@
-import * as React from 'react';
-import { DOMOutputSpec, Fragment, MarkSpec, Node, NodeSpec, Schema, Slice } from 'prosemirror-model';
-import { Doc } from '../../../../fields/Doc';
+import { DOMOutputSpec, MarkSpec } from 'prosemirror-model';
+import { ClientUtils } from '../../../../ClientUtils';
 import { Utils } from '../../../../Utils';
 
 const emDOM: DOMOutputSpec = ['em', 0];
@@ -336,7 +335,7 @@ export const marks: { [index: string]: MarkSpec } = {
             const min = Math.round(node.attrs.modified / 60);
             const hr = Math.round(min / 60);
             const day = Math.round(hr / 60 / 24);
-            const remote = node.attrs.userid !== Doc.CurrentUserEmail ? ' UM-remote' : '';
+            const remote = node.attrs.userid !== ClientUtils.CurrentUserEmail ? ' UM-remote' : '';
             return ['span', { class: 'UM-' + uid + remote + ' UM-min-' + min + ' UM-hr-' + hr + ' UM-day-' + day }, 0];
         },
     },
diff --git a/src/client/views/nodes/formattedText/nodes_rts.ts b/src/client/views/nodes/formattedText/nodes_rts.ts
index 62b8b03d6..70b6604ab 100644
--- a/src/client/views/nodes/formattedText/nodes_rts.ts
+++ b/src/client/views/nodes/formattedText/nodes_rts.ts
@@ -2,7 +2,7 @@ import { DOMOutputSpec, Node, NodeSpec } from 'prosemirror-model';
 import { listItem, orderedList } from 'prosemirror-schema-list';
 import { ParagraphNodeSpec, toParagraphDOM, getParagraphNodeAttrs } from './ParagraphNodeSpec';
 import { DocServer } from '../../../DocServer';
-import { Doc, Field } from '../../../../fields/Doc';
+import { Doc, Field, FieldType } from '../../../../fields/Doc';
 
 const blockquoteDOM: DOMOutputSpec = ['blockquote', 0],
     hrDOM: DOMOutputSpec = ['hr'],
@@ -266,7 +266,7 @@ export const nodes: { [index: string]: NodeSpec } = {
             hideValue: { default: false },
             editable: { default: true },
         },
-        leafText: node => Field.toString((DocServer.GetCachedRefField(node.attrs.docId as string) as Doc)?.[node.attrs.fieldKey as string] as Field),
+        leafText: node => Field.toString((DocServer.GetCachedRefField(node.attrs.docId as string) as Doc)?.[node.attrs.fieldKey as string] as FieldType),
         group: 'inline',
         draggable: false,
         toDOM(node) {
diff --git a/src/client/views/nodes/generativeFill/GenerativeFill.tsx b/src/client/views/nodes/generativeFill/GenerativeFill.tsx
index a485ea4c3..95eb86720 100644
--- a/src/client/views/nodes/generativeFill/GenerativeFill.tsx
+++ b/src/client/views/nodes/generativeFill/GenerativeFill.tsx
@@ -1,27 +1,31 @@
+/* eslint-disable jsx-a11y/no-noninteractive-element-interactions */
+/* eslint-disable jsx-a11y/img-redundant-alt */
+/* eslint-disable jsx-a11y/click-events-have-key-events */
+/* eslint-disable react/function-component-definition */
 import { Checkbox, FormControlLabel, Slider, TextField } from '@mui/material';
 import { IconButton } from 'browndash-components';
+import * as React from 'react';
 import { useEffect, useRef, useState } from 'react';
 import { CgClose } from 'react-icons/cg';
 import { IoMdRedo, IoMdUndo } from 'react-icons/io';
+import { ClientUtils } from '../../../../ClientUtils';
 import { Doc, DocListCast } from '../../../../fields/Doc';
 import { List } from '../../../../fields/List';
 import { NumCast } from '../../../../fields/Types';
-import { Utils } from '../../../../Utils';
-import { Docs, DocUtils } from '../../../documents/Documents';
 import { Networking } from '../../../Network';
+import { DocUtils, Docs } from '../../../documents/Documents';
 import { DocumentManager } from '../../../util/DocumentManager';
 import { CollectionDockingView } from '../../collections/CollectionDockingView';
 import { CollectionFreeFormView } from '../../collections/collectionFreeForm';
 import { OpenWhereMod } from '../DocumentView';
-import { ImageBox, ImageEditorData } from '../ImageBox';
+import { ImageEditorData } from '../ImageBox';
 import './GenerativeFill.scss';
 import Buttons from './GenerativeFillButtons';
 import { BrushHandler } from './generativeFillUtils/BrushHandler';
-import { activeColor, canvasSize, eraserColor, freeformRenderSize, newCollectionSize, offsetDistanceY, offsetX } from './generativeFillUtils/generativeFillConstants';
-import { CursorData, ImageDimensions, Point } from './generativeFillUtils/generativeFillInterfaces';
 import { APISuccess, ImageUtility } from './generativeFillUtils/ImageHandler';
 import { PointerHandler } from './generativeFillUtils/PointerHandler';
-import * as React from 'react';
+import { activeColor, canvasSize, eraserColor, freeformRenderSize, newCollectionSize, offsetDistanceY, offsetX } from './generativeFillUtils/generativeFillConstants';
+import { CursorData, ImageDimensions, Point } from './generativeFillUtils/generativeFillInterfaces';
 
 enum BrushStyle {
     ADD,
@@ -332,7 +336,7 @@ const GenerativeFill = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addD
         const startY = NumCast(parentDoc.current.y);
         const children = DocListCast(parentDoc.current.gen_fill_children);
         const len = children.length;
-        let initialYPositions: number[] = [];
+        const initialYPositions: number[] = [];
         for (let i = 0; i < len; i++) {
             initialYPositions.push(startY + i * offsetDistanceY);
         }
@@ -348,9 +352,9 @@ const GenerativeFill = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addD
     // creates a new image document and returns its reference
     const createNewImgDoc = async (img: HTMLImageElement, firstDoc: boolean): Promise<Doc | undefined> => {
         if (!imageRootDoc) return;
-        const src = img.src;
+        const { src } = img;
         const [result] = await Networking.PostToServer('/uploadRemoteImage', { sources: [src] });
-        const source = Utils.prepend(result.accessPaths.agnostic.client);
+        const source = ClientUtils.prepend(result.accessPaths.agnostic.client);
 
         if (firstDoc) {
             const x = 0;
@@ -370,51 +374,51 @@ const GenerativeFill = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addD
             }
             parentDoc.current = newImg;
             return newImg;
-        } else {
-            if (!parentDoc.current) return;
-            const x = NumCast(parentDoc.current.x) + freeformRenderSize + offsetX;
-            const initialY = 0;
-
-            const newImg = Docs.Create.ImageDocument(source, {
-                x: x,
-                y: initialY,
-                _height: freeformRenderSize,
-                _width: freeformRenderSize,
-                data_nativeWidth: result.nativeWidth,
-                data_nativeHeight: result.nativeHeight,
-            });
+        }
+        if (!parentDoc.current) return;
+        const x = NumCast(parentDoc.current.x) + freeformRenderSize + offsetX;
+        const initialY = 0;
+
+        const newImg = Docs.Create.ImageDocument(source, {
+            x: x,
+            y: initialY,
+            _height: freeformRenderSize,
+            _width: freeformRenderSize,
+            data_nativeWidth: result.nativeWidth,
+            data_nativeHeight: result.nativeHeight,
+        });
 
-            const parentList = DocListCast(parentDoc.current.gen_fill_children);
-            if (parentList.length > 0) {
-                parentList.push(newImg);
-                parentDoc.current.gen_fill_children = new List<Doc>(parentList);
-            } else {
-                parentDoc.current.gen_fill_children = new List<Doc>([newImg]);
-            }
+        const parentList = DocListCast(parentDoc.current.gen_fill_children);
+        if (parentList.length > 0) {
+            parentList.push(newImg);
+            parentDoc.current.gen_fill_children = new List<Doc>(parentList);
+        } else {
+            parentDoc.current.gen_fill_children = new List<Doc>([newImg]);
+        }
 
-            DocUtils.MakeLink(parentDoc.current, newImg, { link_relationship: `Image edit; Prompt: ${input}`, link_displayLine: true });
-            adjustImgPositions();
+        DocUtils.MakeLink(parentDoc.current, newImg, { link_relationship: `Image edit; Prompt: ${input}`, link_displayLine: true });
+        adjustImgPositions();
 
-            if (isNewCollection && newCollectionRef.current) {
-                Doc.AddDocToList(newCollectionRef.current, undefined, newImg);
-            } else {
-                addDoc?.(newImg);
-            }
-            return newImg;
+        if (isNewCollection && newCollectionRef.current) {
+            Doc.AddDocToList(newCollectionRef.current, undefined, newImg);
+        } else {
+            addDoc?.(newImg);
         }
+        return newImg;
     };
 
     // Saves an image to the collection
     const onSave = async (src: string) => {
         const img = new Image();
         img.src = src;
-        if (!currImg.current || !originalImg.current || !imageRootDoc) return;
+        if (!currImg.current || !originalImg.current || !imageRootDoc) return undefined;
         try {
             const res = await createNewImgDoc(img, false);
             return res;
         } catch (err) {
             console.log(err);
         }
+        return undefined;
     };
 
     // Closes the editor view
@@ -443,12 +447,12 @@ const GenerativeFill = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addD
                                 }}
                             />
                         }
-                        label={'Create New Collection'}
+                        label="Create New Collection"
                         labelPlacement="end"
                         sx={{ whiteSpace: 'nowrap' }}
                     />
                     <Buttons getEdit={getEdit} loading={loading} onReset={handleReset} />
-                    <IconButton color={activeColor} tooltip="close" icon={<CgClose size={'16px'} />} onClick={handleViewClose} />
+                    <IconButton color={activeColor} tooltip="close" icon={<CgClose size="16px" />} onClick={handleViewClose} />
                 </div>
             </div>
             {/* Main canvas for editing */}
@@ -469,7 +473,7 @@ const GenerativeFill = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addD
                         width: cursorData.width,
                         height: cursorData.width,
                     }}>
-                    <div className="innerPointer"></div>
+                    <div className="innerPointer" />
                 </div>
                 {/* Icons */}
                 <div className="iconContainer">
@@ -519,11 +523,13 @@ const GenerativeFill = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addD
                         />
                     </div>
                 </div>
-                {/* Edits thumbnails*/}
+                {/* Edits thumbnails */}
                 <div className="editsBox">
                     {edits.map((edit, i) => (
                         <img
+                            // eslint-disable-next-line react/no-array-index-key
                             key={i}
+                            alt="image edits"
                             width={75}
                             src={edit[0] as string}
                             style={{ cursor: 'pointer' }}
@@ -552,6 +558,7 @@ const GenerativeFill = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addD
                                 Original
                             </label>
                             <img
+                                alt="image stuff"
                                 width={75}
                                 src={originalImg.current?.src}
                                 style={{ cursor: 'pointer' }}
diff --git a/src/client/views/nodes/importBox/ImportElementBox.tsx b/src/client/views/nodes/importBox/ImportElementBox.tsx
index 6e7c3e612..dec9a5019 100644
--- a/src/client/views/nodes/importBox/ImportElementBox.tsx
+++ b/src/client/views/nodes/importBox/ImportElementBox.tsx
@@ -1,7 +1,7 @@
 import { computed, makeObservable } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
-import { returnFalse } from '../../../../Utils';
+import { returnFalse } from '../../../../ClientUtils';
 import { Doc } from '../../../../fields/Doc';
 import { ViewBoxBaseComponent } from '../../DocComponent';
 import { DocumentView } from '../DocumentView';
diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx
index 91fdb90fc..518bf66cd 100644
--- a/src/client/views/nodes/trails/PresBox.tsx
+++ b/src/client/views/nodes/trails/PresBox.tsx
@@ -3,7 +3,8 @@ import { Tooltip } from '@mui/material';
 import { action, computed, IReactionDisposer, makeObservable, observable, ObservableSet, reaction, runInAction } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
-import { Doc, DocListCast, Field, FieldResult, NumListCast, Opt, StrListCast } from '../../../../fields/Doc';
+import { lightOrDark, returnFalse, returnOne, setupMoveUpEvents, StopEvent } from '../../../../ClientUtils';
+import { Doc, DocListCast, Field, FieldType, FieldResult, NumListCast, Opt, StrListCast } from '../../../../fields/Doc';
 import { Animation, DocData, TransitionTimer } from '../../../../fields/DocSymbols';
 import { Copy, Id } from '../../../../fields/FieldSymbols';
 import { InkField } from '../../../../fields/InkField';
@@ -13,23 +14,23 @@ import { listSpec } from '../../../../fields/Schema';
 import { ComputedField, ScriptField } from '../../../../fields/ScriptField';
 import { BoolCast, Cast, DocCast, NumCast, StrCast } from '../../../../fields/Types';
 import { AudioField } from '../../../../fields/URLField';
-import { emptyFunction, emptyPath, lightOrDark, returnFalse, returnOne, setupMoveUpEvents, StopEvent, stringHash } from '../../../../Utils';
+import { emptyFunction, emptyPath, stringHash } from '../../../../Utils';
 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 { dropActionType } from '../../../util/DropActionTypes';
 import { ScriptingGlobals } from '../../../util/ScriptingGlobals';
 import { SelectionManager } from '../../../util/SelectionManager';
 import { SerializationHelper } from '../../../util/SerializationHelper';
-import { SettingsManager } from '../../../util/SettingsManager';
+import { SnappingManager } from '../../../util/SnappingManager';
 import { undoBatch, UndoManager } from '../../../util/UndoManager';
 import { CollectionDockingView } from '../../collections/CollectionDockingView';
-import { CollectionFreeFormView, MarqueeViewBounds } from '../../collections/collectionFreeForm';
+import { CollectionFreeFormView } from '../../collections/collectionFreeForm';
 import { CollectionStackedTimeline } from '../../collections/CollectionStackedTimeline';
 import { CollectionView } from '../../collections/CollectionView';
 import { TreeView } from '../../collections/TreeView';
-import { ViewBoxBaseComponent } from '../../DocComponent';
+import { pinDataTypes, PinProps, ViewBoxBaseComponent } from '../../DocComponent';
 import { Colors } from '../../global/globalEnums';
 import { LightboxView } from '../../LightboxView';
 import { DocumentView, OpenWhere, OpenWhereMod } from '../DocumentView';
@@ -37,32 +38,6 @@ import { FieldView, FieldViewProps, FocusViewOptions } from '../FieldView';
 import { ScriptingBox } from '../ScriptingBox';
 import './PresBox.scss';
 import { PresEffect, PresEffectDirection, PresMovement, PresStatus } from './PresEnums';
-export interface pinDataTypes {
-    scrollable?: boolean;
-    dataviz?: number[];
-    pannable?: boolean;
-    type_collection?: boolean;
-    inkable?: boolean;
-    filters?: boolean;
-    pivot?: boolean;
-    temporal?: boolean;
-    clippable?: boolean;
-    datarange?: boolean;
-    dataview?: boolean;
-    poslayoutview?: boolean;
-    dataannos?: boolean;
-    map?: boolean;
-}
-export interface PinProps {
-    audioRange?: boolean;
-    activeFrame?: number;
-    currentFrame?: number;
-    hidePresBox?: boolean;
-    pinViewport?: MarqueeViewBounds; // pin a specific viewport on a freeform view (use MarqueeView.CurViewBounds to compute if no region has been selected)
-    pinDocLayout?: boolean; // pin layout info (width/height/x/y)
-    pinAudioPlay?: boolean; // pin audio annotation
-    pinData?: pinDataTypes;
-}
 
 @observer
 export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
@@ -443,7 +418,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
             else {
                 const bestTargetData = bestTarget[DocData];
                 const current = bestTargetData[fkey];
-                const hash = bestTargetData[fkey] ? stringHash(Field.toString(bestTargetData[fkey] as Field)) : undefined;
+                const hash = bestTargetData[fkey] ? stringHash(Field.toString(bestTargetData[fkey] as FieldType)) : undefined;
                 if (hash) bestTargetData[fkey + '_' + hash] = current instanceof ObjectField ? current[Copy]() : current;
                 bestTargetData[fkey] = activeItem.config_data instanceof ObjectField ? activeItem.config_data[Copy]() : activeItem.config_data;
             }
@@ -623,7 +598,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
     /// reserved fields on the pinDoc so that those values can be restored to the
     /// target doc when navigating to it.
     @action
-    static pinDocView(pinDoc: Doc, pinProps: PinProps, targetDoc: Doc) {
+    static pinDocView(pinDocIn: Doc, pinProps: PinProps, targetDoc: Doc) {
+        const pinDoc = pinDocIn;
         pinDoc.presentation = true;
         pinDoc.config = '';
         if (pinProps.pinDocLayout) {
@@ -1479,7 +1455,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                 max={max}
                 value={value}
                 readOnly={true}
-                style={{ marginLeft: hmargin, marginRight: hmargin, width: `calc(100% - ${2 * (hmargin ?? 0)}px)`, background: SettingsManager.userColor, color: SettingsManager.userVariantColor }}
+                style={{ marginLeft: hmargin, marginRight: hmargin, width: `calc(100% - ${2 * (hmargin ?? 0)}px)`, background: SnappingManager.userColor, color: SnappingManager.userVariantColor }}
                 className={`toolbar-slider ${active ? '' : 'none'}`}
                 onPointerDown={e => {
                     PresBox._sliderBatch = UndoManager.StartBatch('pres slider');
@@ -1521,7 +1497,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                         <Tooltip title={<div className="dash-tooltip">Hide before presented</div>}>
                             <div
                                 className={`ribbon-toggle ${activeItem.presentation_hideBefore ? 'active' : ''}`}
-                                style={{ border: `solid 1px ${SettingsManager.userColor}`, color: SettingsManager.userColor, background: activeItem.presentation_hideBefore ? SettingsManager.userVariantColor : SettingsManager.userBackgroundColor }}
+                                style={{ border: `solid 1px ${SnappingManager.userColor}`, color: SnappingManager.userColor, background: activeItem.presentation_hideBefore ? SnappingManager.userVariantColor : SnappingManager.userBackgroundColor }}
                                 onClick={() => this.updateHideBefore(activeItem)}>
                                 Hide before
                             </div>
@@ -1529,7 +1505,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                         <Tooltip title={<div className="dash-tooltip">{'Hide while presented'}</div>}>
                             <div
                                 className={`ribbon-toggle ${activeItem.presentation_hide ? 'active' : ''}`}
-                                style={{ border: `solid 1px ${SettingsManager.userColor}`, color: SettingsManager.userColor, background: activeItem.presentation_hide ? SettingsManager.userVariantColor : SettingsManager.userBackgroundColor }}
+                                style={{ border: `solid 1px ${SnappingManager.userColor}`, color: SnappingManager.userColor, background: activeItem.presentation_hide ? SnappingManager.userVariantColor : SnappingManager.userBackgroundColor }}
                                 onClick={() => this.updateHide(activeItem)}>
                                 Hide
                             </div>
@@ -1538,7 +1514,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                         <Tooltip title={<div className="dash-tooltip">{'Hide after presented'}</div>}>
                             <div
                                 className={`ribbon-toggle ${activeItem.presentation_hideAfter ? 'active' : ''}`}
-                                style={{ border: `solid 1px ${SettingsManager.userColor}`, color: SettingsManager.userColor, background: activeItem.presentation_hideAfter ? SettingsManager.userVariantColor : SettingsManager.userBackgroundColor }}
+                                style={{ border: `solid 1px ${SnappingManager.userColor}`, color: SnappingManager.userColor, background: activeItem.presentation_hideAfter ? SnappingManager.userVariantColor : SnappingManager.userBackgroundColor }}
                                 onClick={() => this.updateHideAfter(activeItem)}>
                                 Hide after
                             </div>
@@ -1548,9 +1524,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                             <div
                                 className="ribbon-toggle"
                                 style={{
-                                    border: `solid 1px ${SettingsManager.userColor}`,
-                                    color: SettingsManager.userColor,
-                                    background: activeItem.presentation_openInLightbox ? SettingsManager.userVariantColor : SettingsManager.userBackgroundColor,
+                                    border: `solid 1px ${SnappingManager.userColor}`,
+                                    color: SnappingManager.userColor,
+                                    background: activeItem.presentation_openInLightbox ? SnappingManager.userVariantColor : SnappingManager.userBackgroundColor,
                                 }}
                                 onClick={() => this.updateOpenDoc(activeItem)}>
                                 Lightbox
@@ -1559,7 +1535,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                         <Tooltip title={<div className="dash-tooltip">Transition movement style</div>}>
                             <div
                                 className="ribbon-toggle"
-                                style={{ border: `solid 1px ${SettingsManager.userColor}`, color: SettingsManager.userColor, background: activeItem.presEaseFunc === 'ease' ? SettingsManager.userVariantColor : SettingsManager.userBackgroundColor }}
+                                style={{ border: `solid 1px ${SnappingManager.userColor}`, color: SnappingManager.userColor, background: activeItem.presEaseFunc === 'ease' ? SnappingManager.userVariantColor : SnappingManager.userBackgroundColor }}
                                 onClick={() => this.updateEaseFunc(activeItem)}>
                                 {`${StrCast(activeItem.presEaseFunc, 'ease')}`}
                             </div>
@@ -1569,10 +1545,10 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                         <>
                             <div className="ribbon-doubleButton">
                                 <div className="presBox-subheading">Slide Duration</div>
-                                <div className="ribbon-property" style={{ border: `solid 1px ${SettingsManager.userColor}` }}>
-                                    <input className="presBox-input" type="number" readOnly={true} value={duration} onKeyDown={e => e.stopPropagation()} onChange={e => this.updateDurationTime(e.target.value)} /> s
+                                <div className="ribbon-property" style={{ border: `solid 1px ${SnappingManager.userColor}` }}>
+                                    <input className="presBox-input" type="number" readOnly value={duration} onKeyDown={e => e.stopPropagation()} onChange={e => this.updateDurationTime(e.target.value)} /> s
                                 </div>
-                                <div className="ribbon-propertyUpDown" style={{ color: SettingsManager.userBackgroundColor, background: SettingsManager.userColor }}>
+                                <div className="ribbon-propertyUpDown" style={{ color: SnappingManager.userBackgroundColor, background: SnappingManager.userColor }}>
                                     <div className="ribbon-propertyUpDownItem" onClick={() => this.updateDurationTime(String(duration), 1000)}>
                                         <FontAwesomeIcon icon={'caret-up'} />
                                     </div>
@@ -1611,7 +1587,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                         <div className="presBox-subheading">Progressivize Collection</div>
                         <input
                             className="presBox-checkbox"
-                            style={{ margin: 10, border: `solid 1px ${SettingsManager.userColor}` }}
+                            style={{ margin: 10, border: `solid 1px ${SnappingManager.userColor}` }}
                             type="checkbox"
                             onChange={() => {
                                 activeItem.presentation_indexed = activeItem.presentation_indexed === undefined ? 0 : undefined;
@@ -1634,7 +1610,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                         <div className="presBox-subheading">Progressivize First Bullet</div>
                         <input
                             className="presBox-checkbox"
-                            style={{ margin: 10, border: `solid 1px ${SettingsManager.userColor}` }}
+                            style={{ margin: 10, border: `solid 1px ${SnappingManager.userColor}` }}
                             type="checkbox"
                             onChange={() => (activeItem.presentation_indexedStart = activeItem.presentation_indexedStart ? 0 : 1)}
                             checked={!NumCast(activeItem.presentation_indexedStart)}
@@ -1644,7 +1620,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                         <div className="presBox-subheading">Expand Current Bullet</div>
                         <input
                             className="presBox-checkbox"
-                            style={{ margin: 10, border: `solid 1px ${SettingsManager.userColor}` }}
+                            style={{ margin: 10, border: `solid 1px ${SnappingManager.userColor}` }}
                             type="checkbox"
                             onChange={() => (activeItem.presBulletExpand = !activeItem.presBulletExpand)}
                             checked={BoolCast(activeItem.presBulletExpand)}
@@ -1660,16 +1636,16 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                                 this._openBulletEffectDropdown = !this._openBulletEffectDropdown;
                             })}
                             style={{
-                                color: SettingsManager.userColor,
-                                background: SettingsManager.userVariantColor,
+                                color: SnappingManager.userColor,
+                                background: SnappingManager.userVariantColor,
                                 borderBottomLeftRadius: this._openBulletEffectDropdown ? 0 : 5,
-                                border: this._openBulletEffectDropdown ? `solid 2px ${SettingsManager.userVariantColor}` : `solid 1px ${SettingsManager.userColor}`,
+                                border: this._openBulletEffectDropdown ? `solid 2px ${SnappingManager.userVariantColor}` : `solid 1px ${SnappingManager.userColor}`,
                             }}>
                             {effect?.toString()}
                             <FontAwesomeIcon className="presBox-dropdownIcon" style={{ gridColumn: 2, color: this._openBulletEffectDropdown ? Colors.MEDIUM_BLUE : 'black' }} icon={'angle-down'} />
                             <div
                                 className={'presBox-dropdownOptions'}
-                                style={{ display: this._openBulletEffectDropdown ? 'grid' : 'none', color: SettingsManager.userColor, background: SettingsManager.userBackgroundColor }}
+                                style={{ display: this._openBulletEffectDropdown ? 'grid' : 'none', color: SnappingManager.userColor, background: SnappingManager.userBackgroundColor }}
                                 onPointerDown={e => e.stopPropagation()}>
                                 {Object.values(PresEffect)
                                     .filter(v => isNaN(Number(v)))
@@ -1698,7 +1674,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
             </div>
         );
         const presDirection = (direction: PresEffectDirection, icon: string, gridColumn: number, gridRow: number, opts: object) => {
-            const color = activeItem.presentation_effectDirection === direction || (direction === PresEffectDirection.Center && !activeItem.presentation_effectDirection) ? SettingsManager.userVariantColor : SettingsManager.userColor;
+            const color = activeItem.presentation_effectDirection === direction || (direction === PresEffectDirection.Center && !activeItem.presentation_effectDirection) ? SnappingManager.userVariantColor : SnappingManager.userColor;
             return (
                 <Tooltip title={<div className="dash-tooltip">{direction}</div>}>
                     <div
@@ -1733,10 +1709,10 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                                 this._openMovementDropdown = !this._openMovementDropdown;
                             })}
                             style={{
-                                color: SettingsManager.userColor,
-                                background: SettingsManager.userVariantColor,
+                                color: SnappingManager.userColor,
+                                background: SnappingManager.userVariantColor,
                                 borderBottomLeftRadius: this._openMovementDropdown ? 0 : 5,
-                                border: this._openMovementDropdown ? `solid 2px ${SettingsManager.userVariantColor}` : `solid 1px ${SettingsManager.userColor}`,
+                                border: this._openMovementDropdown ? `solid 2px ${SnappingManager.userVariantColor}` : `solid 1px ${SnappingManager.userColor}`,
                             }}>
                             {this.movementName(activeItem)}
                             <FontAwesomeIcon className="presBox-dropdownIcon" style={{ gridColumn: 2, color: this._openMovementDropdown ? Colors.MEDIUM_BLUE : 'black' }} icon={'angle-down'} />
@@ -1745,8 +1721,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                                 id={'presBoxMovementDropdown'}
                                 onPointerDown={StopEvent}
                                 style={{
-                                    color: SettingsManager.userColor,
-                                    background: SettingsManager.userBackgroundColor,
+                                    color: SnappingManager.userColor,
+                                    background: SnappingManager.userBackgroundColor,
                                     display: this._openMovementDropdown ? 'grid' : 'none',
                                 }}>
                                 {presMovement(PresMovement.None)}
@@ -1758,10 +1734,10 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                         </div>
                         <div className="ribbon-doubleButton" style={{ display: activeItem.presentation_movement === PresMovement.Zoom ? 'inline-flex' : 'none' }}>
                             <div className="presBox-subheading">Zoom (% screen filled)</div>
-                            <div className="ribbon-property" style={{ border: `solid 1px ${SettingsManager.userColor}` }}>
-                                <input className="presBox-input" type="number" readOnly={true} value={zoom} onChange={e => this.updateZoom(e.target.value)} />%
+                            <div className="ribbon-property" style={{ border: `solid 1px ${SnappingManager.userColor}` }}>
+                                <input className="presBox-input" type="number" readOnly value={zoom} onChange={e => this.updateZoom(e.target.value)} />%
                             </div>
-                            <div className="ribbon-propertyUpDown" style={{ color: SettingsManager.userBackgroundColor, background: SettingsManager.userColor }}>
+                            <div className="ribbon-propertyUpDown" style={{ color: SnappingManager.userBackgroundColor, background: SnappingManager.userColor }}>
                                 <div className="ribbon-propertyUpDownItem" onClick={() => this.updateZoom(String(zoom), 0.1)}>
                                     <FontAwesomeIcon icon={'caret-up'} />
                                 </div>
@@ -1773,10 +1749,10 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                         {PresBox.inputter('0', '1', '100', zoom, activeItem.presentation_movement === PresMovement.Zoom, this.updateZoom)}
                         <div className="ribbon-doubleButton" style={{ display: 'inline-flex' }}>
                             <div className="presBox-subheading">Transition Time</div>
-                            <div className="ribbon-property" style={{ border: `solid 1px ${SettingsManager.userColor}` }}>
+                            <div className="ribbon-property" style={{ border: `solid 1px ${SnappingManager.userColor}` }}>
                                 <input className="presBox-input" type="number" readOnly={true} value={transitionSpeed} onKeyDown={e => e.stopPropagation()} onChange={action(e => this.updateTransitionTime(e.target.value))} /> s
                             </div>
-                            <div className="ribbon-propertyUpDown" style={{ color: SettingsManager.userBackgroundColor, background: SettingsManager.userColor }}>
+                            <div className="ribbon-propertyUpDown" style={{ color: SnappingManager.userBackgroundColor, background: SnappingManager.userColor }}>
                                 <div className="ribbon-propertyUpDownItem" onClick={() => this.updateTransitionTime(String(transitionSpeed), 1000)}>
                                     <FontAwesomeIcon icon={'caret-up'} />
                                 </div>
@@ -1798,7 +1774,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                             <div className="presBox-subheading">Play Audio Annotation</div>
                             <input
                                 className="presBox-checkbox"
-                                style={{ margin: 10, border: `solid 1px ${SettingsManager.userColor}` }}
+                                style={{ margin: 10, border: `solid 1px ${SnappingManager.userColor}` }}
                                 type="checkbox"
                                 onChange={() => (activeItem.presPlayAudio = !BoolCast(activeItem.presPlayAudio))}
                                 checked={BoolCast(activeItem.presPlayAudio)}
@@ -1808,7 +1784,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                             <div className="presBox-subheading">Zoom Text Selections</div>
                             <input
                                 className="presBox-checkbox"
-                                style={{ margin: 10, border: `solid 1px ${SettingsManager.userColor}` }}
+                                style={{ margin: 10, border: `solid 1px ${SnappingManager.userColor}` }}
                                 type="checkbox"
                                 onChange={() => (activeItem.presentation_zoomText = !BoolCast(activeItem.presentation_zoomText))}
                                 checked={BoolCast(activeItem.presentation_zoomText)}
@@ -1821,10 +1797,10 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                                 this._openEffectDropdown = !this._openEffectDropdown;
                             })}
                             style={{
-                                color: SettingsManager.userColor,
-                                background: SettingsManager.userVariantColor,
+                                color: SnappingManager.userColor,
+                                background: SnappingManager.userVariantColor,
                                 borderBottomLeftRadius: this._openEffectDropdown ? 0 : 5,
-                                border: this._openEffectDropdown ? `solid 2px ${SettingsManager.userVariantColor}` : `solid 1px ${SettingsManager.userColor}`,
+                                border: this._openEffectDropdown ? `solid 2px ${SettingsSnappingManagerManager.userVariantColor}` : `solid 1px ${SnappingManager.userColor}`,
                             }}>
                             {effect?.toString()}
                             <FontAwesomeIcon className="presBox-dropdownIcon" style={{ gridColumn: 2, color: this._openEffectDropdown ? Colors.MEDIUM_BLUE : 'black' }} icon={'angle-down'} />
@@ -1832,8 +1808,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                                 className="presBox-dropdownOptions"
                                 id={'presBoxMovementDropdown'}
                                 style={{
-                                    color: SettingsManager.userColor,
-                                    background: SettingsManager.userBackgroundColor,
+                                    color: SnappingManager.userColor,
+                                    background: SnappingManager.userBackgroundColor,
                                     display: this._openEffectDropdown ? 'grid' : 'none',
                                 }}
                                 onPointerDown={e => e.stopPropagation()}>
@@ -1844,7 +1820,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                         </div>
                         <div className="ribbon-doubleButton" style={{ display: effect === PresEffectDirection.None ? 'none' : 'inline-flex' }}>
                             <div className="presBox-subheading">Effect direction</div>
-                            <div className="ribbon-property" style={{ border: `solid 1px ${SettingsManager.userColor}` }}>
+                            <div className="ribbon-property" style={{ border: `solid 1px ${SnappingManager.userColor}` }}>
                                 {StrCast(this.activeItem.presentation_effectDirection)}
                             </div>
                         </div>
@@ -1882,7 +1858,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                                     <div className="slider-text" style={{ fontWeight: 500 }}>
                                         Start time (s)
                                     </div>
-                                    <div id="startTime" className="slider-number" style={{ color: SettingsManager.userColor, backgroundColor: SettingsManager.userBackgroundColor }}>
+                                    <div id="startTime" className="slider-number" style={{ color: SnappingManager.userColor, backgroundColor: SnappingManager.userBackgroundColor }}>
                                         <input
                                             className="presBox-input"
                                             style={{ textAlign: 'center', width: '100%', height: 15, fontSize: 10 }}
@@ -1898,7 +1874,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                                     <div className="slider-text" style={{ fontWeight: 500 }}>
                                         Duration (s)
                                     </div>
-                                    <div className="slider-number" style={{ color: SettingsManager.userColor, backgroundColor: SettingsManager.userBackgroundColor }}>
+                                    <div className="slider-number" style={{ color: SnappingManager.userColor, backgroundColor: SnappingManager.userBackgroundColor }}>
                                         {Math.round((config_clipEnd - NumCast(activeItem.config_clipStart)) * 10) / 10}
                                     </div>
                                 </div>
@@ -1906,7 +1882,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                                     <div className="slider-text" style={{ fontWeight: 500 }}>
                                         End time (s)
                                     </div>
-                                    <div id="endTime" className="slider-number" style={{ color: SettingsManager.userColor, backgroundColor: SettingsManager.userBackgroundColor }}>
+                                    <div id="endTime" className="slider-number" style={{ color: SnappingManager.userColor, backgroundColor: SnappingManager.userBackgroundColor }}>
                                         <input
                                             className="presBox-input"
                                             onKeyDown={e => e.stopPropagation()}
@@ -1926,14 +1902,14 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                                     min={clipStart}
                                     max={clipEnd}
                                     value={config_clipEnd}
-                                    style={{ gridColumn: 1, gridRow: 1, background: SettingsManager.userColor, color: SettingsManager.userVariantColor }}
+                                    style={{ gridColumn: 1, gridRow: 1, background: SnappingManager.userColor, color: SnappingManager.userVariantColor }}
                                     className={`toolbar-slider ${'end'}`}
                                     id="toolbar-slider"
                                     onPointerDown={e => {
                                         this._batch = UndoManager.StartBatch('config_clipEnd');
                                         const endBlock = document.getElementById('endTime');
                                         if (endBlock) {
-                                            endBlock.style.backgroundColor = SettingsManager.userVariantColor;
+                                            endBlock.style.backgroundColor = SnappingManager.userVariantColor;
                                         }
                                         e.stopPropagation();
                                     }}
@@ -1941,7 +1917,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                                         this._batch?.end();
                                         const endBlock = document.getElementById('endTime');
                                         if (endBlock) {
-                                            endBlock.style.backgroundColor = SettingsManager.userBackgroundColor;
+                                            endBlock.style.backgroundColor = SnappingManager.userBackgroundColor;
                                         }
                                     }}
                                     onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
@@ -1962,7 +1938,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                                         this._batch = UndoManager.StartBatch('config_clipStart');
                                         const startBlock = document.getElementById('startTime');
                                         if (startBlock) {
-                                            startBlock.style.backgroundColor = SettingsManager.userVariantColor;
+                                            startBlock.style.backgroundColor = SnappingManager.userVariantColor;
                                         }
                                         e.stopPropagation();
                                     }}
@@ -1970,7 +1946,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                                         this._batch?.end();
                                         const startBlock = document.getElementById('startTime');
                                         if (startBlock) {
-                                            startBlock.style.backgroundColor = SettingsManager.userBackgroundColor;
+                                            startBlock.style.backgroundColor = SnappingManager.userBackgroundColor;
                                         }
                                     }}
                                     onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
@@ -1993,7 +1969,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                                     <input
                                         className="presBox-checkbox"
                                         type="checkbox"
-                                        style={{ border: `solid 1px ${SettingsManager.userColor}` }}
+                                        style={{ border: `solid 1px ${SnappingManager.userColor}` }}
                                         onChange={() => (activeItem.presentation_mediaStart = 'manual')}
                                         checked={activeItem.presentation_mediaStart === 'manual'}
                                     />
@@ -2002,7 +1978,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                                 <div className="checkbox-container">
                                     <input
                                         className="presBox-checkbox"
-                                        style={{ border: `solid 1px ${SettingsManager.userColor}` }}
+                                        style={{ border: `solid 1px ${SnappingManager.userColor}` }}
                                         type="checkbox"
                                         onChange={() => (activeItem.presentation_mediaStart = 'auto')}
                                         checked={activeItem.presentation_mediaStart === 'auto'}
@@ -2016,7 +1992,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                                     <input
                                         className="presBox-checkbox"
                                         type="checkbox"
-                                        style={{ border: `solid 1px ${SettingsManager.userColor}` }}
+                                        style={{ border: `solid 1px ${SnappingManager.userColor}` }}
                                         onChange={() => (activeItem.presentation_mediaStop = 'manual')}
                                         checked={activeItem.presentation_mediaStop === 'manual'}
                                     />
@@ -2026,7 +2002,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                                     <input
                                         className="presBox-checkbox"
                                         type="checkbox"
-                                        style={{ border: `solid 1px ${SettingsManager.userColor}` }}
+                                        style={{ border: `solid 1px ${SnappingManager.userColor}` }}
                                         onChange={() => (activeItem.presentation_mediaStop = 'auto')}
                                         checked={activeItem.presentation_mediaStop === 'auto'}
                                     />
@@ -2270,15 +2246,15 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
     }
 
     @action
-    toggleProperties = () => (SettingsManager.Instance.propertiesWidth = SettingsManager.Instance.propertiesWidth > 0 ? 0 : 250);
+    toggleProperties = () => (SnappingManager.Instance.propertiesWidth = SnappingManager.Instance.propertiesWidth > 0 ? 0 : 250);
 
     @computed get toolbar() {
-        const propIcon = SettingsManager.Instance.propertiesWidth > 0 ? 'angle-double-right' : 'angle-double-left';
-        const propTitle = SettingsManager.Instance.propertiesWidth > 0 ? 'Close Presentation Panel' : 'Open Presentation Panel';
+        const propIcon = SnappingManager.Instance.propertiesWidth > 0 ? 'angle-double-right' : 'angle-double-left';
+        const propTitle = SnappingManager.Instance.propertiesWidth > 0 ? 'Close Presentation Panel' : 'Open Presentation Panel';
         const mode = StrCast(this.Document._type_collection) as CollectionViewType;
         const isMini: boolean = this.toolbarWidth <= 100;
-        const activeColor = SettingsManager.userVariantColor;
-        const inactiveColor = lightOrDark(SettingsManager.userBackgroundColor) === Colors.WHITE ? Colors.WHITE : SettingsManager.userBackgroundColor;
+        const activeColor = SnappingManager.userVariantColor;
+        const inactiveColor = lightOrDark(SnappingManager.userBackgroundColor) === Colors.WHITE ? Colors.WHITE : SnappingManager.userBackgroundColor;
         return mode === CollectionViewType.Carousel3D || Doc.IsInMyOverlay(this.Document) ? null : (
             <div id="toolbarContainer" className={'presBox-toolbar'}>
                 {/* <Tooltip title={<><div className="dash-tooltip">{"Add new slide"}</div></>}><div className={`toolbar-button ${this.newDocumentTools ? "active" : ""}`} onClick={action(() => this.newDocumentTools = !this.newDocumentTools)}>
@@ -2303,7 +2279,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                         </Tooltip>
                         <Tooltip title={<div className="dash-tooltip">{propTitle}</div>}>
                             <div className="toolbar-button" style={{ position: 'absolute', right: 4, fontSize: 16 }} onClick={this.toggleProperties}>
-                                <FontAwesomeIcon className={'toolbar-thumbtack'} icon={propIcon} style={{ color: SettingsManager.Instance.propertiesWidth > 0 ? activeColor : inactiveColor }} />
+                                <FontAwesomeIcon className={'toolbar-thumbtack'} icon={propIcon} style={{ color: SnappingManager.Instance.propertiesWidth > 0 ? activeColor : inactiveColor }} />
                             </div>
                         </Tooltip>
                     </>
@@ -2378,12 +2354,24 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         // Case 1: There are still other frames and should go through all frames before going to next slide
         return (
             <div className="presPanelOverlay" style={{ display: this.layoutDoc.presentation_status !== 'edit' ? 'inline-flex' : 'none' }}>
-                <Tooltip title={<div className="dash-tooltip">{'Loop'}</div>}>
+                <Tooltip title={<div className="dash-tooltip">Loop</div>}>
                     <div
                         className="presPanel-button"
                         style={{ color: this.layoutDoc.presLoop ? Colors.MEDIUM_BLUE : 'white' }}
-                        onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, () => (this.layoutDoc.presLoop = !this.layoutDoc.presLoop), false, false)}>
-                        <FontAwesomeIcon icon={'redo-alt'} />
+                        onPointerDown={e =>
+                            setupMoveUpEvents(
+                                this,
+                                e,
+                                returnFalse,
+                                emptyFunction,
+                                () => {
+                                    this.layoutDoc.presLoop = !this.layoutDoc.presLoop;
+                                },
+                                false,
+                                false
+                            )
+                        }>
+                        <FontAwesomeIcon icon="redo-alt" />
                     </div>
                 </Tooltip>
                 <div className="presPanel-divider" />
@@ -2617,12 +2605,12 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                                 childXPadding={Doc.IsComicStyle(this.Document) ? 20 : undefined}
                                 filterAddDocument={this.addDocumentFilter}
                                 removeDocument={returnFalse}
-                                dontRegisterView={true}
+                                dontRegisterView
                                 focus={this.focusElement}
                                 ScreenToLocalTransform={this.getTransform}
                                 AddToMap={this.AddToMap}
                                 RemFromMap={this.RemFromMap}
-                                hierarchyIndex={emptyPath as any as number[]}
+                                hierarchyIndex={emptyPath}
                             />
                         ) : null}
                     </div>
diff --git a/src/client/views/nodes/trails/PresElementBox.tsx b/src/client/views/nodes/trails/PresElementBox.tsx
index 28139eb14..fca5a2770 100644
--- a/src/client/views/nodes/trails/PresElementBox.tsx
+++ b/src/client/views/nodes/trails/PresElementBox.tsx
@@ -7,7 +7,8 @@ import { Doc, DocListCast, Opt } from '../../../../fields/Doc';
 import { Id } from '../../../../fields/FieldSymbols';
 import { List } from '../../../../fields/List';
 import { BoolCast, Cast, DocCast, NumCast, StrCast } from '../../../../fields/Types';
-import { emptyFunction, returnEmptyDoclist, returnFalse, returnTrue, setupMoveUpEvents } from '../../../../Utils';
+import { emptyFunction } from '../../../../Utils';
+import { returnEmptyDoclist, returnFalse, returnTrue, setupMoveUpEvents } from '../../../../ClientUtils';
 import { Docs } from '../../../documents/Documents';
 import { CollectionViewType } from '../../../documents/DocumentTypes';
 import { DocumentManager } from '../../../util/DocumentManager';
diff --git a/src/client/views/pdf/AnchorMenu.tsx b/src/client/views/pdf/AnchorMenu.tsx
index 59f191af0..9c4080154 100644
--- a/src/client/views/pdf/AnchorMenu.tsx
+++ b/src/client/views/pdf/AnchorMenu.tsx
@@ -4,7 +4,8 @@ import { IReactionDisposer, ObservableMap, action, computed, makeObservable, obs
 import { observer } from 'mobx-react';
 import * as React from 'react';
 import { ColorResult } from 'react-color';
-import { Utils, returnFalse, setupMoveUpEvents, unimplementedFunction } from '../../../Utils';
+import { ClientUtils, returnFalse, setupMoveUpEvents } from '../../../ClientUtils';
+import { unimplementedFunction } from '../../../Utils';
 import { Doc, Opt } from '../../../fields/Doc';
 import { GPTCallType, gptAPICall } from '../../apis/gpt/GPT';
 import { DocumentType } from '../../documents/DocumentTypes';
@@ -17,6 +18,7 @@ import { GPTPopup, GPTPopupMode } from './GPTPopup/GPTPopup';
 
 @observer
 export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
+    // eslint-disable-next-line no-use-before-define
     static Instance: AnchorMenu;
 
     private _disposer: IReactionDisposer | undefined;
@@ -37,7 +39,9 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
     // GPT additions
     @observable private selectedText: string = '';
     @action
-    public setSelectedText = (txt: string) => (this.selectedText = txt);
+    public setSelectedText = (txt: string) => {
+        this.selectedText = txt;
+    };
 
     public onMakeAnchor: () => Opt<Doc> = () => undefined; // Method to get anchor from text search
 
@@ -64,7 +68,7 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
     componentDidMount() {
         this._disposer = reaction(
             () => SelectionManager.Views.slice(),
-            sel => AnchorMenu.Instance.fadeOut(true)
+            () => AnchorMenu.Instance.fadeOut(true)
         );
     }
 
@@ -72,7 +76,7 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
      * Invokes the API with the selected text and stores it in the summarized text.
      * @param e pointer down event
      */
-    gptSummarize = async (e: React.PointerEvent) => {
+    gptSummarize = async () => {
         // move this logic to gptpopup, need to implement generate again
         GPTPopup.Instance.setVisible(true);
         GPTPopup.Instance.setMode(GPTPopupMode.SUMMARY);
@@ -128,7 +132,7 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
             <Group>
                 <IconButton
                     icon={<FontAwesomeIcon icon="highlighter" style={{ transition: 'transform 0.1s', transform: 'rotate(-45deg)' }} />}
-                    tooltip={'Click to Highlight'}
+                    tooltip="Click to Highlight"
                     onClick={this.highlightClicked}
                     colorPicker={this.highlightColor}
                     color={SettingsManager.userColor}
@@ -144,7 +148,7 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
             hsl: { a: 0, h: 0, s: 0, l: 0 },
             rgb: { a: 0, r: 0, b: 0, g: 0 },
         };
-        this.highlightColor = Utils.colorString(col);
+        this.highlightColor = ClientUtils.colorString(col);
     };
 
     /**
@@ -167,7 +171,7 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
                             color={SettingsManager.userColor}
                         />
                     </div>
-                    {/* GPT Summarize icon only shows up when text is highlighted, not on marquee selection*/}
+                    {/* GPT Summarize icon only shows up when text is highlighted, not on marquee selection */}
                     {AnchorMenu.Instance.StartCropDrag === unimplementedFunction && this.canSummarize() && (
                         <IconButton
                             tooltip="Summarize with AI" //
@@ -187,7 +191,7 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
                     <Popup
                         tooltip="Find document to link to selected text" //
                         type={Type.PRIM}
-                        icon={<FontAwesomeIcon icon={'search'} />}
+                        icon={<FontAwesomeIcon icon="search" />}
                         popup={<LinkPopup key="popup" linkCreateAnchor={this.onMakeAnchor} />}
                         color={SettingsManager.userColor}
                     />
@@ -230,7 +234,7 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
                     )}
                     {this.IsTargetToggler !== returnFalse && (
                         <Toggle
-                            tooltip={'Make target visibility toggle on click'}
+                            tooltip="Make target visibility toggle on click"
                             type={Type.PRIM}
                             toggleType={ToggleType.BUTTON}
                             toggleStatus={this.IsTargetToggler()}
diff --git a/src/client/views/pdf/GPTPopup/GPTPopup.tsx b/src/client/views/pdf/GPTPopup/GPTPopup.tsx
index da8a88803..cd13d4cbc 100644
--- a/src/client/views/pdf/GPTPopup/GPTPopup.tsx
+++ b/src/client/views/pdf/GPTPopup/GPTPopup.tsx
@@ -6,7 +6,7 @@ import * as React from 'react';
 import { CgClose } from 'react-icons/cg';
 import ReactLoading from 'react-loading';
 import { TypeAnimation } from 'react-type-animation';
-import { Utils } from '../../../../Utils';
+import { ClientUtils } from '../../../../ClientUtils';
 import { Doc } from '../../../../fields/Doc';
 import { NumCast, StrCast } from '../../../../fields/Types';
 import { Networking } from '../../../Network';
@@ -26,6 +26,7 @@ interface GPTPopupProps {}
 
 @observer
 export class GPTPopup extends ObservableReactComponent<GPTPopupProps> {
+    // eslint-disable-next-line no-use-before-define
     static Instance: GPTPopup;
 
     @observable
@@ -71,8 +72,6 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> {
     @observable
     public highlightRange: number[] = [];
     @action callSummaryApi = () => {};
-    @action callEditApi = () => {};
-    @action replaceText = (replacement: string) => {};
 
     @observable
     private done: boolean = false;
@@ -110,24 +109,25 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> {
      * Generates a Dalle image and uploads it to the server.
      */
     generateImage = async () => {
-        if (this.imgDesc === '') return;
+        if (this.imgDesc === '') return undefined;
         this.setImgUrls([]);
         this.setMode(GPTPopupMode.IMAGE);
         this.setVisible(true);
         this.setLoading(true);
 
         try {
-            let image_urls = await gptImageCall(this.imgDesc);
-            if (image_urls && image_urls[0]) {
-                const [result] = await Networking.PostToServer('/uploadRemoteImage', { sources: [image_urls[0]] });
-                const source = Utils.prepend(result.accessPaths.agnostic.client);
-                this.setImgUrls([[image_urls[0], source]]);
+            const imageUrls = await gptImageCall(this.imgDesc);
+            if (imageUrls && imageUrls[0]) {
+                const [result] = await Networking.PostToServer('/uploadRemoteImage', { sources: [imageUrls[0]] });
+                const source = ClientUtils.prepend(result.accessPaths.agnostic.client);
+                this.setImgUrls([[imageUrls[0], source]]);
             }
         } catch (err) {
             console.log(err);
             return '';
         }
         this.setLoading(false);
+        return undefined;
     };
 
     /**
@@ -188,55 +188,43 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> {
         }
     };
 
-    imageBox = () => {
-        return (
-            <div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
-                {this.heading('GENERATED IMAGE')}
-                <div className="image-content-wrapper">
-                    {this.imgUrls.map(rawSrc => (
-                        <div className="img-wrapper">
-                            <div className="img-container">
-                                <img key={rawSrc[0]} src={rawSrc[0]} width={150} height={150} alt="dalle generation" />
-                            </div>
-                            <div className="btn-container">
-                                <Button text="Save Image" onClick={() => this.transferToImage(rawSrc[1])} color={StrCast(Doc.UserDoc().userColor)} type={Type.TERT} />
-                            </div>
+    imageBox = () => (
+        <div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
+            {this.heading('GENERATED IMAGE')}
+            <div className="image-content-wrapper">
+                {this.imgUrls.map(rawSrc => (
+                    <div className="img-wrapper">
+                        <div className="img-container">
+                            <img key={rawSrc[0]} src={rawSrc[0]} width={150} height={150} alt="dalle generation" />
                         </div>
-                    ))}
-                </div>
-                {!this.loading && (
-                    <>
-                        <IconButton tooltip="Generate Again" onClick={this.generateImage} icon={<FontAwesomeIcon icon="redo-alt" size="lg" />} color={StrCast(Doc.UserDoc().userVariantColor)} />
-                    </>
-                )}
+                        <div className="btn-container">
+                            <Button text="Save Image" onClick={() => this.transferToImage(rawSrc[1])} color={StrCast(Doc.UserDoc().userColor)} type={Type.TERT} />
+                        </div>
+                    </div>
+                ))}
             </div>
-        );
-    };
+            {!this.loading && <IconButton tooltip="Generate Again" onClick={this.generateImage} icon={<FontAwesomeIcon icon="redo-alt" size="lg" />} color={StrCast(Doc.UserDoc().userVariantColor)} />}
+        </div>
+    );
 
-    data = () => {
-        return (
-            <div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
-                {this.heading('GENERATED IMAGE')}
-                <div className="image-content-wrapper">
-                    {this.imgUrls.map(rawSrc => (
-                        <div className="img-wrapper">
-                            <div className="img-container">
-                                <img key={rawSrc[0]} src={rawSrc[0]} width={150} height={150} alt="dalle generation" />
-                            </div>
-                            <div className="btn-container">
-                                <Button text="Save Image" onClick={() => this.transferToImage(rawSrc[1])} color={StrCast(Doc.UserDoc().userColor)} type={Type.TERT} />
-                            </div>
+    data = () => (
+        <div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
+            {this.heading('GENERATED IMAGE')}
+            <div className="image-content-wrapper">
+                {this.imgUrls.map(rawSrc => (
+                    <div className="img-wrapper">
+                        <div className="img-container">
+                            <img key={rawSrc[0]} src={rawSrc[0]} width={150} height={150} alt="dalle generation" />
                         </div>
-                    ))}
-                </div>
-                {!this.loading && (
-                    <>
-                        <IconButton tooltip="Generate Again" onClick={this.generateImage} icon={<FontAwesomeIcon icon="redo-alt" size="lg" />} color={StrCast(Doc.UserDoc().userVariantColor)} />
-                    </>
-                )}
+                        <div className="btn-container">
+                            <Button text="Save Image" onClick={() => this.transferToImage(rawSrc[1])} color={StrCast(Doc.UserDoc().userColor)} type={Type.TERT} />
+                        </div>
+                    </div>
+                ))}
             </div>
-        );
-    };
+            {!this.loading && <IconButton tooltip="Generate Again" onClick={this.generateImage} icon={<FontAwesomeIcon icon="redo-alt" size="lg" />} color={StrCast(Doc.UserDoc().userVariantColor)} />}
+        </div>
+    );
 
     summaryBox = () => (
         <>
@@ -255,7 +243,7 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> {
                                         }, 500);
                                     },
                                 ]}
-                                //cursor={{ hideWhenDone: true }}
+                                // cursor={{ hideWhenDone: true }}
                             />
                         ) : (
                             this.text
@@ -294,9 +282,7 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> {
                 <FontAwesomeIcon icon="exclamation-circle" size="sm" style={{ paddingRight: '5px' }} />
                 AI generated responses can contain inaccurate or misleading content.
             </div>
-        ) : (
-            <></>
-        );
+        ) : null;
 
     heading = (headingText: string) => (
         <div className="summary-heading">
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index aaff2a342..0ab952e84 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -10,7 +10,8 @@ import { Id } from '../../../fields/FieldSymbols';
 import { InkTool } from '../../../fields/InkField';
 import { Cast, NumCast, StrCast } from '../../../fields/Types';
 import { TraceMobx } from '../../../fields/util';
-import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, emptyFunction, returnAll, returnFalse, returnNone, returnZero, smoothScroll, Utils } from '../../../Utils';
+import { emptyFunction, Utils } from '../../../Utils';
+import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, ClientUtils, returnAll, returnFalse, returnNone, returnZero, smoothScroll } from '../../../ClientUtils';
 import { DocUtils } from '../../documents/Documents';
 import { SelectionManager } from '../../util/SelectionManager';
 import { SnappingManager } from '../../util/SnappingManager';
@@ -98,9 +99,13 @@ export class PDFViewer extends ObservableReactComponent<IViewerProps> {
     }
 
     componentDidMount() {
-        runInAction(() => (this._showWaiting = true));
+        runInAction(() => {
+            this._showWaiting = true;
+        });
         this.setupPdfJsViewer();
-        this._mainCont.current?.addEventListener('scroll', e => ((e.target as any).scrollLeft = 0));
+        this._mainCont.current?.addEventListener('scroll', e => {
+            (e.target as any).scrollLeft = 0;
+        });
 
         this._disposers.layout_autoHeight = reaction(
             () => this._props.layoutDoc._layout_autoHeight,
@@ -176,7 +181,7 @@ export class PDFViewer extends ObservableReactComponent<IViewerProps> {
         let focusSpeed: Opt<number>;
         if (doc !== this._props.Document && mainCont) {
             const windowHeight = this._props.PanelHeight() / (this._props.NativeDimScaling?.() || 1);
-            const scrollTo = Utils.scrollIntoView(scrollTop, doc[Height](), NumCast(this._props.layoutDoc._layout_scrollTop), windowHeight, windowHeight * 0.1, this._scrollHeight);
+            const scrollTo = ClientUtils.scrollIntoView(scrollTop, doc[Height](), NumCast(this._props.layoutDoc._layout_scrollTop), windowHeight, windowHeight * 0.1, this._scrollHeight);
             if (scrollTo !== undefined && scrollTo !== this._props.layoutDoc._layout_scrollTop) {
                 if (!this._pdfViewer) this._initialScroll = { loc: scrollTo, easeFunc: options.easeFunc };
                 else if (!options.instant) this._scrollStopper = smoothScroll((focusSpeed = options.zoomTime ?? 500), mainCont, scrollTo, options.easeFunc, this._scrollStopper);
@@ -456,7 +461,7 @@ export class PDFViewer extends ObservableReactComponent<IViewerProps> {
 
     onClick = (e: React.MouseEvent) => {
         this._scrollStopper?.();
-        if (this._setPreviewCursor && e.button === 0 && Math.abs(e.clientX - this._downX) < Utils.DRAG_THRESHOLD && Math.abs(e.clientY - this._downY) < Utils.DRAG_THRESHOLD) {
+        if (this._setPreviewCursor && e.button === 0 && Math.abs(e.clientX - this._downX) < ClientUtils.DRAG_THRESHOLD && Math.abs(e.clientY - this._downY) < ClientUtils.DRAG_THRESHOLD) {
             this._setPreviewCursor(e.clientX, e.clientY, false, false, this._props.Document);
         }
         // e.stopPropagation();  // bcz: not sure why this was here.  We need to allow the DocumentView to get clicks to process doubleClicks
@@ -496,8 +501,8 @@ export class PDFViewer extends ObservableReactComponent<IViewerProps> {
     overlayTransform = () => this.scrollXf().scale(1 / NumCast(this._props.layoutDoc._freeform_scale, 1));
     panelWidth = () => this._props.PanelWidth() / (this._props.NativeDimScaling?.() || 1);
     panelHeight = () => this._props.PanelHeight() / (this._props.NativeDimScaling?.() || 1);
-    transparentFilter = () => [...this._props.childFilters(), Utils.TransparentBackgroundFilter];
-    opaqueFilter = () => [...this._props.childFilters(), Utils.noDragDocsFilter, ...(SnappingManager.CanEmbed && this._props.isContentActive() ? [] : [Utils.OpaqueBackgroundFilter])];
+    transparentFilter = () => [...this._props.childFilters(), ClientUtils.TransparentBackgroundFilter];
+    opaqueFilter = () => [...this._props.childFilters(), ClientUtils.noDragDocsFilter, ...(SnappingManager.CanEmbed && this._props.isContentActive() ? [] : [ClientUtils.OpaqueBackgroundFilter])];
     childStyleProvider = (doc: Doc | undefined, props: Opt<FieldViewProps>, property: string): any => {
         if (doc instanceof Doc && property === StyleProp.PointerEvents) {
             if (this.inlineTextAnnotations.includes(doc) || this._props.isContentActive() === false) return 'none';
@@ -532,7 +537,7 @@ export class PDFViewer extends ObservableReactComponent<IViewerProps> {
                 PanelWidth={this.panelWidth}
                 ScreenToLocalTransform={this.overlayTransform}
                 isAnyChildContentActive={returnFalse}
-                isAnnotationOverlayScrollable={true}
+                isAnnotationOverlayScrollable
                 childFilters={childFilters}
                 select={emptyFunction}
                 styleProvider={this.childStyleProvider}
diff --git a/src/client/views/search/SearchBox.tsx b/src/client/views/search/SearchBox.tsx
index 9f153e86d..af9f05a14 100644
--- a/src/client/views/search/SearchBox.tsx
+++ b/src/client/views/search/SearchBox.tsx
@@ -2,7 +2,7 @@ import { Tooltip } from '@mui/material';
 import { action, computed, makeObservable, observable } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
-import { Doc, DocListCastAsync, Field } from '../../../fields/Doc';
+import { Doc, DocListCastAsync, Field, FieldType } from '../../../fields/Doc';
 import { DirectLinks, DocData } from '../../../fields/DocSymbols';
 import { Id } from '../../../fields/FieldSymbols';
 import { DocCast, StrCast } from '../../../fields/Types';
@@ -145,7 +145,7 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() {
                     .filter(d => d)
                     .map(async d => {
                         const fieldKey = Doc.LayoutFieldKey(d);
-                        const annos = !Field.toString(Doc.LayoutField(d) as Field).includes('CollectionView');
+                        const annos = !Field.toString(Doc.LayoutField(d) as FieldType).includes('CollectionView');
                         const data = d[annos ? fieldKey + '_annotations' : fieldKey];
                         const docs = await DocListCastAsync(data);
                         docs && newarray.push(...docs);
diff --git a/src/client/views/topbar/TopBar.tsx b/src/client/views/topbar/TopBar.tsx
index eab33114e..b87e5cdde 100644
--- a/src/client/views/topbar/TopBar.tsx
+++ b/src/client/views/topbar/TopBar.tsx
@@ -5,14 +5,14 @@ import { observer } from 'mobx-react';
 import * as React from 'react';
 import { Flip } from 'react-awesome-reveal';
 import { FaBug } from 'react-icons/fa';
+import { returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue } from '../../../ClientUtils';
 import { Doc, DocListCast } from '../../../fields/Doc';
 import { AclAdmin, DashVersion } from '../../../fields/DocSymbols';
 import { StrCast } from '../../../fields/Types';
 import { GetEffectiveAcl } from '../../../fields/util';
-import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue } from '../../../Utils';
+import { emptyFunction } 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';
@@ -28,6 +28,7 @@ import { DocumentViewInternal, returnEmptyDocViewList } from '../nodes/DocumentV
 import { ObservableReactComponent } from '../ObservableReactComponent';
 import { DefaultStyleProvider } from '../StyleProvider';
 import './TopBar.scss';
+import { dropActionType } from '../../util/DropActionTypes';
 
 /**
  * ABOUT: This is the topbar in Dash, which included the current Dashboard as well as access to information on the user
diff --git a/src/debug/Viewer.tsx b/src/debug/Viewer.tsx
index ceff01fc2..fc62463e6 100644
--- a/src/debug/Viewer.tsx
+++ b/src/debug/Viewer.tsx
@@ -8,7 +8,7 @@ import { CompileScript } from '../client/util/Scripting';
 import { EditableView } from '../client/views/EditableView';
 import CursorField from '../fields/CursorField';
 import { DateField } from '../fields/DateField';
-import { Doc, Field, FieldResult } from '../fields/Doc';
+import { Doc, Field, FieldType, FieldResult } from '../fields/Doc';
 import { Id } from '../fields/FieldSymbols';
 import { List } from '../fields/List';
 import { RichTextField } from '../fields/RichTextField';
@@ -39,7 +39,7 @@ configure({
 });
 
 @observer
-class ListViewer extends React.Component<{ field: List<Field> }> {
+class ListViewer extends React.Component<{ field: List<FieldType> }> {
     @observable
     expanded = false;
 
@@ -144,7 +144,7 @@ class Viewer extends React.Component {
     private idToAdd: string = '';
 
     @observable
-    private fields: Field[] = [];
+    private fields: FieldType[] = [];
 
     @action
     inputOnChange = (e: React.ChangeEvent<HTMLInputElement>) => {
diff --git a/src/fields/DateField.ts b/src/fields/DateField.ts
index 1e5222fb6..56a3177f8 100644
--- a/src/fields/DateField.ts
+++ b/src/fields/DateField.ts
@@ -1,5 +1,5 @@
-import { Deserializable } from '../client/util/SerializationHelper';
 import { serializable, date } from 'serializr';
+import { Deserializable } from '../client/util/SerializationHelper';
 import { ObjectField } from './ObjectField';
 import { Copy, ToJavascriptString, ToScriptString, ToString } from './FieldSymbols';
 import { scriptingGlobal, ScriptingGlobals } from '../client/util/ScriptingGlobals';
@@ -38,6 +38,7 @@ export class DateField extends ObjectField {
     }
 }
 
+// eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function d(...dateArgs: any[]) {
     return new DateField(new (Date as any)(...dateArgs));
 });
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index 48214cf25..60c6402d4 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -1,26 +1,20 @@
-import { saveAs } from 'file-saver';
 import { action, computed, makeObservable, observable, ObservableMap, ObservableSet, runInAction } from 'mobx';
 import { computedFn } from 'mobx-utils';
 import { alias, map, serializable } from 'serializr';
 import { DocServer } from '../client/DocServer';
 import { CollectionViewType, DocumentType } from '../client/documents/DocumentTypes';
-import { LinkManager } from '../client/util/LinkManager';
 import { scriptingGlobal, ScriptingGlobals } from '../client/util/ScriptingGlobals';
 import { afterDocDeserialize, autoObject, Deserializable, SerializationHelper } from '../client/util/SerializationHelper';
 import { undoable } from '../client/util/UndoManager';
-import { DocumentView } from '../client/views/nodes/DocumentView';
-import { decycle } from '../decycler/decycler';
-import * as JSZipUtils from '../JSZipUtils';
-import { incrementTitleCopy, Utils } from '../Utils';
-import { DateField } from './DateField';
+import { ClientUtils, incrementTitleCopy } from '../ClientUtils';
 import {
     AclAdmin, AclAugment, AclEdit, AclPrivate, AclReadonly, Animation, AudioPlay, Brushed, CachedUpdates, DirectLinks,
     DocAcl, DocCss, DocData, DocLayout, DocViews, FieldKeys, FieldTuples, ForceServerWrite, Height, Highlight,
     Initializing, Self, SelfProxy, TransitionTimer, UpdatingFromServer, Width
 } from './DocSymbols'; // prettier-ignore
 import { Copy, FieldChanged, HandleUpdate, Id, Parent, ToJavascriptString, ToScriptString, ToString } from './FieldSymbols';
-import { InkField, InkTool } from './InkField';
-import { List, ListFieldName } from './List';
+import { InkTool } from './InkField';
+import { List } from './List';
 import { ObjectField } from './ObjectField';
 import { PrefetchProxy, ProxyField } from './Proxy';
 import { FieldId, RefField } from './RefField';
@@ -28,10 +22,8 @@ import { RichTextField } from './RichTextField';
 import { listSpec } from './Schema';
 import { ComputedField, ScriptField } from './ScriptField';
 import { BoolCast, Cast, DocCast, FieldValue, NumCast, StrCast, ToConstructor } from './Types';
-import { AudioField, CsvField, ImageField, PdfField, VideoField, WebField } from './URLField';
 import { containedFieldChangedHandler, deleteProperty, GetEffectiveAcl, getField, getter, makeEditable, makeReadOnly, setter, SharingPermissions } from './util';
-import * as JSZip from 'jszip';
-import { FieldViewProps } from '../client/views/nodes/FieldView';
+
 export const LinkedTo = '-linkedTo';
 export namespace Field {
     /**
@@ -45,7 +37,7 @@ 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]));
-        const valFunc = (field: Field): string => {
+        const valFunc = (field: FieldType): string => {
             const res =
                 field instanceof ComputedField && showComputedValue
                     ? field.value(doc)
@@ -63,7 +55,7 @@ export namespace Field {
         };
         return !Field.IsField(field) ? (key.startsWith('_') ? '=' : '') : (onDelegate ? '=' : '') + valFunc(field);
     }
-    export function toScriptString(field: Field) {
+    export function toScriptString(field: FieldType) {
         switch (typeof field) {
             case 'string': if (field.startsWith('{"')) return `'${field}'`; // bcz: hack ... want to quote the string the right way. if there are nested "'s, then use ' instead of ".  In this case, test for the start of a JSON string of the format {"property": ... } and use outer 's instead of "s
                            return !field.includes('`') ? `\`${field}\`` : `"${field}"`;
@@ -72,8 +64,8 @@ export namespace Field {
             default:       return field?.[ToScriptString]?.() ?? 'null';
         } // prettier-ignore
     }
-    export function toJavascriptString(field: Field) {
-        var rawjava = '';
+    export function toJavascriptString(field: FieldType) {
+        let rawjava = '';
 
         switch (typeof field) {
             case 'string':
@@ -82,7 +74,7 @@ export namespace Field {
                            break;
             default:       rawjava = field?.[ToJavascriptString]?.() ?? '';
         } // prettier-ignore
-        var script = rawjava;
+        let script = rawjava;
         // this is a bit hacky, but we treat '^@' references to a published document
         // as a kind of macro to include the content of those documents
         Doc.MyPublishedDocs.forEach(doc => {
@@ -95,23 +87,23 @@ export namespace Field {
         });
         return script;
     }
-    export function toString(field: Field) {
+    export function toString(field: FieldType) {
         if (typeof field === 'string' || typeof field === 'number' || typeof field === 'boolean') return String(field);
         return field?.[ToString]?.() || '';
     }
-    export function IsField(field: any): field is Field;
-    export function IsField(field: any, includeUndefined: true): field is Field | undefined;
-    export function IsField(field: any, includeUndefined: boolean = false): field is Field | undefined {
+    export function IsField(field: any): field is FieldType;
+    export function IsField(field: any, includeUndefined: true): field is FieldType | undefined;
+    export function IsField(field: any, includeUndefined: boolean = false): field is FieldType | undefined {
         return ['string', 'number', 'boolean'].includes(typeof field) || field instanceof ObjectField || field instanceof RefField || (includeUndefined && field === undefined);
     }
     export function Copy(field: any) {
         return field instanceof ObjectField ? ObjectField.MakeCopy(field) : field;
     }
 }
-export type Field = number | string | boolean | ObjectField | RefField;
+export type FieldType = number | string | boolean | ObjectField | RefField;
 export type Opt<T> = T | undefined;
 export type FieldWaiting<T extends RefField = RefField> = T extends undefined ? never : Promise<T | undefined>;
-export type FieldResult<T extends Field = Field> = Opt<T> | FieldWaiting<Extract<T, RefField>>;
+export type FieldResult<T extends FieldType = FieldType> = Opt<T> | FieldWaiting<Extract<T, RefField>>;
 
 /**
  * Cast any field to either a List of Docs or undefined if the given field isn't a List of Docs.
@@ -159,7 +151,7 @@ export function updateCachedAcls(doc: Doc) {
     if (!doc) return;
 
     const target = (doc as any)?.__fieldTuples ?? doc;
-    const permissions: { [key: string]: symbol } = !target.author || target.author === Doc.CurrentUserEmail ? { 'acl-Me': AclAdmin } : {};
+    const permissions: { [key: string]: symbol } = !target.author || target.author === ClientUtils.CurrentUserEmail ? { 'acl-Me': AclAdmin } : {};
     Object.keys(target).filter(key => key.startsWith('acl') && (permissions[key] = ReverseHierarchyMap.get(StrCast(target[key]))!.acl));
     if (Object.keys(permissions).length || doc[DocAcl]?.length) {
         runInAction(() => (doc[DocAcl] = permissions));
@@ -178,7 +170,8 @@ export class Doc extends RefField {
     @observable public static GuestDashboard: Doc | undefined = undefined;
     @observable public static GuestTarget: Doc | undefined = undefined;
     @observable public static GuestMobile: Doc | undefined = undefined;
-    public static CurrentUserEmail: string = '';
+
+    public static AddLink: undefined | ((link: Doc, checkExists?: boolean) => void);
 
     public static get MySharedDocs()          { return DocCast(Doc.UserDoc().mySharedDocs); } // prettier-ignore
     public static get MyUserDocView()         { return DocCast(Doc.UserDoc().myUserDocView); } // prettier-ignore
@@ -320,7 +313,7 @@ export class Doc extends RefField {
     @observable public [Animation]: Opt<Doc> = undefined;
     @observable public [Highlight]: boolean = false;
     @observable public [Brushed]: boolean = false;
-    @observable public [DocViews] = new ObservableSet<DocumentView>();
+    @observable public [DocViews] = new ObservableSet<any /* DocumentView */>();
 
     private [Self] = this;
     private [SelfProxy]: any;
@@ -329,7 +322,7 @@ export class Doc extends RefField {
     private [CachedUpdates]: { [key: string]: () => void | Promise<any> } = {};
 
     public [Initializing]: boolean = false;
-    public [FieldChanged] = (diff: undefined | { op: '$addToSet' | '$remFromSet' | '$set'; items: Field[] | undefined; length: number | undefined; hint?: any }, serverOp: any) => {
+    public [FieldChanged] = (diff: undefined | { op: '$addToSet' | '$remFromSet' | '$set'; items: FieldType[] | undefined; length: number | undefined; hint?: any }, serverOp: any) => {
         if (!this[UpdatingFromServer] || this[ForceServerWrite]) {
             DocServer.UpdateField(this[Id], serverOp);
         }
@@ -363,7 +356,7 @@ export class Doc extends RefField {
 
     public async [HandleUpdate](diff: any) {
         const set = diff.$set;
-        const sameAuthor = this.author === Doc.CurrentUserEmail;
+        const sameAuthor = this.author === ClientUtils.CurrentUserEmail;
         if (set) {
             for (const key in set) {
                 const fprefix = 'fields.';
@@ -454,7 +447,7 @@ export namespace Doc {
             return doc;
         }
     }
-    export function GetT<T extends Field>(doc: Doc, key: string, ctor: ToConstructor<T>, ignoreProto: boolean = false): FieldResult<T> {
+    export function GetT<T extends FieldType>(doc: Doc, key: string, ctor: ToConstructor<T>, ignoreProto: boolean = false): FieldResult<T> {
         return Cast(Get(doc, key, ignoreProto), ctor) as FieldResult<T>;
     }
     export function isTemplateDoc(doc: Doc) {
@@ -482,7 +475,7 @@ export namespace Doc {
     // 2) if the data doc has the field, then it's written there.
     // 3) if neither already has the field, then 'defaultProto' determines whether to write it to the data doc (or the embedding)
     //
-    export async function SetInPlace(doc: Doc, key: string, value: Field | undefined, defaultProto: boolean) {
+    export async function SetInPlace(doc: Doc, key: string, value: FieldType | undefined, defaultProto: boolean) {
         if (key.startsWith('_')) key = key.substring(1);
         const hasProto = doc[DocData] !== doc ? doc[DocData] : undefined;
         const onDeleg = Object.getOwnPropertyNames(doc).indexOf(key) !== -1;
@@ -500,7 +493,6 @@ export namespace Doc {
         }
         return protos;
     }
-
     /**
      * This function is intended to model Object.assign({}, {}) [https://mzl.la/1Mo3l21], which copies
      * the values of the properties of a source object into the target.
@@ -512,7 +504,7 @@ export namespace Doc {
      * @param fields the fields to project onto the target. Its type signature defines a mapping from some string key
      * to a potentially undefined field, where each entry in this mapping is optional.
      */
-    export function assign<K extends string>(doc: Doc, fields: Partial<Record<K, Opt<Field>>>, skipUndefineds: boolean = false, isInitializing = false) {
+    export function assign<K extends string>(doc: Doc, fields: Partial<Record<K, Opt<FieldType>>>, skipUndefineds: boolean = false, isInitializing = false) {
         isInitializing && (doc[Initializing] = true);
         for (const key in fields) {
             if (fields.hasOwnProperty(key)) {
@@ -634,7 +626,7 @@ export namespace Doc {
         embedding.createdFrom = doc;
         embedding.proto_embeddingId = doc[DocData].proto_embeddingId = Doc.GetEmbeddings(doc).length - 1;
         !Doc.GetT(embedding, 'title', 'string', true) && (embedding.title = ComputedField.MakeFunction(`renameEmbedding(this)`));
-        embedding.author = Doc.CurrentUserEmail;
+        embedding.author = ClientUtils.CurrentUserEmail;
 
         return embedding;
     }
@@ -642,7 +634,7 @@ export namespace Doc {
     export function BestEmbedding(doc: Doc) {
         const dataDoc = doc[DocData];
         const availableEmbeddings = Doc.GetEmbeddings(dataDoc);
-        const bestEmbedding = [...(dataDoc !== doc ? [doc] : []), ...availableEmbeddings].find(doc => !doc.embedContainer && doc.author === Doc.CurrentUserEmail);
+        const bestEmbedding = [...(dataDoc !== doc ? [doc] : []), ...availableEmbeddings].find(doc => !doc.embedContainer && doc.author === ClientUtils.CurrentUserEmail);
         bestEmbedding && Doc.AddEmbedding(doc, doc);
         return bestEmbedding ?? Doc.MakeEmbedding(doc);
     }
@@ -700,7 +692,7 @@ export namespace Doc {
                 };
                 const docAtKey = doc[key];
                 if (key === 'author') {
-                    assignKey(Doc.CurrentUserEmail);
+                    assignKey(ClientUtils.CurrentUserEmail);
                 } else if (docAtKey instanceof Doc) {
                     if (pruneDocs.includes(docAtKey)) {
                         // prune doc and do nothing
@@ -709,7 +701,7 @@ export namespace Doc {
                         (key.includes('layout[') ||
                             key.startsWith('layout') || //
                             ['embedContainer', 'annotationOn', 'proto'].includes(key) ||
-                            (['link_anchor_1', 'link_anchor_2'].includes(key) && doc.author === Doc.CurrentUserEmail))
+                            (['link_anchor_1', 'link_anchor_2'].includes(key) && doc.author === ClientUtils.CurrentUserEmail))
                     ) {
                         assignKey(await Doc.makeClone(docAtKey, cloneMap, linkMap, rtfs, exclusions, pruneDocs, cloneLinks, cloneTemplates));
                     } else {
@@ -771,7 +763,7 @@ export namespace Doc {
         const copy = await Doc.makeClone(doc, cloneMap, linkMap, rtfMap, ['cloneOf'], doc.embedContainer ? [DocCast(doc.embedContainer)] : [], cloneLinks, cloneTemplates);
         const repaired = new Set<Doc>();
         const linkedDocs = Array.from(linkMap.values());
-        linkedDocs.map((link: Doc) => LinkManager.Instance.addLink(link, true));
+        linkedDocs.map(link => Doc.AddLink?.(link, true));
         rtfMap.map(({ copy, key, field }) => {
             const replacer = (match: any, attr: string, id: string, offset: any, string: any) => {
                 const mapped = cloneMap.get(id);
@@ -790,84 +782,6 @@ export namespace Doc {
         return { clone: copy, map: cloneMap, linkMap };
     }
 
-    export async function Zip(doc: Doc, zipFilename = 'dashExport.zip') {
-        const { clone, map, linkMap } = await Doc.MakeClone(doc);
-        const proms = new Set<string>();
-        function replacer(key: any, value: any) {
-            if (key && ['branchOf', 'cloneOf', 'cursors'].includes(key)) return undefined;
-            if (value?.__type === 'image') {
-                const extension = value.url.replace(/.*\./, '');
-                proms.add(value.url.replace('.' + extension, '_o.' + extension));
-                return SerializationHelper.Serialize(new ImageField(value.url));
-            }
-            if (value?.__type === 'pdf') {
-                proms.add(value.url);
-                return SerializationHelper.Serialize(new PdfField(value.url));
-            }
-            if (value?.__type === 'audio') {
-                proms.add(value.url);
-                return SerializationHelper.Serialize(new AudioField(value.url));
-            }
-            if (value?.__type === 'video') {
-                proms.add(value.url);
-                return SerializationHelper.Serialize(new VideoField(value.url));
-            }
-            if (
-                value instanceof Doc ||
-                value instanceof ScriptField ||
-                value instanceof RichTextField ||
-                value instanceof InkField ||
-                value instanceof CsvField ||
-                value instanceof WebField ||
-                value instanceof DateField ||
-                value instanceof ProxyField ||
-                value instanceof ComputedField
-            ) {
-                return SerializationHelper.Serialize(value);
-            }
-            if (value instanceof Array && key !== ListFieldName && key !== InkField.InkDataFieldName) return { fields: value, __type: 'list' };
-            return value;
-        }
-
-        const docs: { [id: string]: any } = {};
-        const links: { [id: string]: any } = {};
-        Array.from(map.entries()).forEach(f => (docs[f[0]] = f[1]));
-        Array.from(linkMap.entries()).forEach(l => (links[l[0]] = l[1]));
-        const jsonDocs = JSON.stringify({ id: clone[Id], docs, links }, decycle(replacer));
-
-        const zip = new JSZip();
-        var count = 0;
-        const promArr = Array.from(proms)
-            .filter(url => url?.startsWith('/files'))
-            .map(url => url.replace('/', '')); // window.location.origin));
-        console.log(promArr.length);
-        if (!promArr.length) {
-            zip.file('docs.json', jsonDocs);
-            zip.generateAsync({ type: 'blob' }).then(content => saveAs(content, zipFilename));
-        } else
-            promArr.forEach((url, i) => {
-                // loading a file and add it in a zip file
-                JSZipUtils.getBinaryContent(window.location.origin + '/' + url, (err: any, data: any) => {
-                    if (err) throw err; // or handle the error
-                    // // Generate a directory within the Zip file structure
-                    // const assets = zip.folder("assets");
-                    // assets.file(filename, data, {binary: true});
-                    const assetPathOnServer = promArr[i].replace(window.location.origin + '/', '').replace(/\//g, '%%%');
-                    zip.file(assetPathOnServer, data, { binary: true });
-                    console.log(' => ' + url);
-                    if (++count === promArr.length) {
-                        zip.file('docs.json', jsonDocs);
-                        zip.generateAsync({ type: 'blob' }).then(content => saveAs(content, zipFilename));
-                        // const a = document.createElement("a");
-                        // const url = Utils.prepend(`/downloadId/${this.props.Document[Id]}`);
-                        // a.href = url;
-                        // a.download = `DocExport-${this.props.Document[Id]}.zip`;
-                        // a.click();
-                    }
-                });
-            });
-    }
-
     const _pendingMap = new Set<string>();
     //
     // Returns an expanded template layout for a target data document if there is a template relationship
@@ -954,7 +868,7 @@ export namespace Doc {
                 }
             } else {
                 const cfield = ComputedField.WithoutComputed(() => FieldValue(doc[key]));
-                const field = key === 'author' ? Doc.CurrentUserEmail : ProxyField.WithoutProxy(() => doc[key]);
+                const field = key === 'author' ? ClientUtils.CurrentUserEmail : ProxyField.WithoutProxy(() => doc[key]);
                 if (field instanceof RefField) {
                     if (field instanceof Doc) {
                         if (key === 'myLinkDatabase') {
@@ -1000,7 +914,7 @@ export namespace Doc {
                     }
                 } else {
                     const cfield = ComputedField.WithoutComputed(() => FieldValue(doc[key]));
-                    const field = key === 'author' ? Doc.CurrentUserEmail : ProxyField.WithoutProxy(() => doc[key]);
+                    const field = key === 'author' ? ClientUtils.CurrentUserEmail : ProxyField.WithoutProxy(() => doc[key]);
                     if (field instanceof RefField) {
                         copy[key] = field;
                     } else if (cfield instanceof ComputedField) {
@@ -1038,7 +952,7 @@ export namespace Doc {
             delegate[Initializing] = true;
             updateCachedAcls(delegate);
             delegate.proto = doc;
-            delegate.author = Doc.CurrentUserEmail;
+            delegate.author = ClientUtils.CurrentUserEmail;
             Object.keys(doc)
                 .filter(key => key.startsWith('acl'))
                 .forEach(key => (delegate[key] = doc[key]));
@@ -1067,7 +981,7 @@ export namespace Doc {
     export function ApplyTemplate(templateDoc: Doc) {
         if (templateDoc) {
             const proto = new Doc();
-            proto.author = Doc.CurrentUserEmail;
+            proto.author = ClientUtils.CurrentUserEmail;
             const target = Doc.MakeDelegate(proto);
             const targetKey = StrCast(templateDoc.layout_fieldKey, 'layout');
             const applied = ApplyTemplateTo(templateDoc, target, targetKey, templateDoc.title + '(...' + _applyCount++ + ')');
@@ -1123,7 +1037,7 @@ export namespace Doc {
 
     // converts a document id to a url path on the server
     export function globalServerPath(doc: Doc | string = ''): string {
-        return Utils.prepend('/doc/' + (doc instanceof Doc ? doc[Id] : doc));
+        return ClientUtils.prepend('/doc/' + (doc instanceof Doc ? doc[Id] : doc));
     }
     // converts a document id to a url path on the server
     export function localServerPath(doc?: Doc): string {
@@ -1426,20 +1340,6 @@ export namespace Doc {
         });
     }
 
-    export function styleFromLayoutString(doc: Doc, props: FieldViewProps, scale: number) {
-        const style: { [key: string]: any } = {};
-        const divKeys = ['width', 'height', 'fontSize', 'transform', 'left', 'backgroundColor', 'left', 'right', 'top', 'bottom', 'pointerEvents', 'position'];
-        const replacer = (match: any, expr: string, offset: any, string: any) => {
-            // bcz: this executes a script to convert a property expression string:  { script }  into a value
-            return ScriptField.MakeFunction(expr, { self: Doc.name, this: Doc.name, scale: 'number' })?.script.run({ this: doc, self: doc, scale }).result?.toString() ?? '';
-        };
-        divKeys.map((prop: string) => {
-            const p = (props as any)[prop];
-            typeof p === 'string' && (style[prop] = p?.replace(/{([^.'][^}']+)}/g, replacer));
-        });
-        return style;
-    }
-
     export function Paste(docids: string[], clone: boolean, addDocument: (doc: Doc | Doc[]) => boolean, ptx?: number, pty?: number, newPoint?: number[]) {
         DocServer.GetRefFields(docids).then(async fieldlist => {
             const list = Array.from(Object.values(fieldlist))
@@ -1505,7 +1405,7 @@ export namespace Doc {
     // If they are not remapped, loading the file will overwrite any existing documents with those ids
     //
     export async function importDocument(file: File, remap = false) {
-        const upload = Utils.prepend('/uploadDoc');
+        const upload = ClientUtils.prepend('/uploadDoc');
         const formData = new FormData();
         if (file) {
             formData.append('file', file);
@@ -1518,7 +1418,7 @@ export namespace Doc {
                 const links = await DocServer.GetRefFields(json.linkids as string[]);
                 Array.from(Object.keys(links))
                     .map(key => links[key])
-                    .forEach(link => link instanceof Doc && LinkManager.Instance.addLink(link));
+                    .forEach(link => link instanceof Doc && Doc.AddLink?.(link));
                 return doc;
             }
         }
@@ -1617,7 +1517,7 @@ export namespace Doc {
             if (hasEntries || !excludeEmptyObjects) {
                 const resolved = target ?? new Doc();
                 if (hasEntries) {
-                    let result: Opt<Field>;
+                    let result: Opt<FieldType>;
                     Object.keys(object).map(key => {
                         // if excludeEmptyObjects is true, any qualifying conversions from toField will
                         // be undefined, and thus the results that would have
@@ -1639,9 +1539,9 @@ export namespace Doc {
          * @returns the list mapped from JSON to field values, where each mapping
          * might involve arbitrary recursion (since toField might itself call convertList)
          */
-        const convertList = (list: Array<any>, excludeEmptyObjects: boolean): Opt<List<Field>> => {
+        const convertList = (list: Array<any>, excludeEmptyObjects: boolean): Opt<List<FieldType>> => {
             const target = new List();
-            let result: Opt<Field>;
+            let result: Opt<FieldType>;
             // if excludeEmptyObjects is true, any qualifying conversions from toField will
             // be undefined, and thus the results that would have
             // otherwise been empty (List or Doc)s will just not be written
@@ -1651,7 +1551,7 @@ export namespace Doc {
             }
         };
 
-        const toField = (data: any, excludeEmptyObjects: boolean, title?: string): Opt<Field> => {
+        const toField = (data: any, excludeEmptyObjects: boolean, title?: string): Opt<FieldType> => {
             if (data === null || data === undefined) {
                 return undefined;
             }
@@ -1719,7 +1619,7 @@ ScriptingGlobals.add(function setDocRangeFilter(container: Doc, key: string, ran
     Doc.setDocRangeFilter(container, key, range);
 });
 ScriptingGlobals.add(function toJavascriptString(str: string) {
-    return Field.toJavascriptString(str as Field);
+    return Field.toJavascriptString(str as FieldType);
 });
 ScriptingGlobals.add(function RtfField() {
     return RichTextField.RTFfield();
diff --git a/src/fields/InkField.ts b/src/fields/InkField.ts
index b3e01229a..c4f5f7a24 100644
--- a/src/fields/InkField.ts
+++ b/src/fields/InkField.ts
@@ -2,6 +2,7 @@ import { Bezier } from 'bezier-js';
 import { alias, createSimpleSchema, list, object, serializable } from 'serializr';
 import { ScriptingGlobals } from '../client/util/ScriptingGlobals';
 import { Deserializable } from '../client/util/SerializationHelper';
+import { PointData } from '../pen-gestures/GestureTypes';
 import { Copy, ToJavascriptString, ToScriptString, ToString } from './FieldSymbols';
 import { ObjectField } from './ObjectField';
 
@@ -16,12 +17,6 @@ export enum InkTool {
     PresentationPin = 'presentationpin',
 }
 
-// Defines a point in an ink as a pair of x- and y-coordinates.
-export interface PointData {
-    X: number;
-    Y: number;
-}
-
 export type Segment = Array<Bezier>;
 
 // Defines an ink as an array of points.
@@ -62,10 +57,10 @@ const strokeDataSchema = createSimpleSchema({
     '*': true,
 });
 
+export const InkDataFieldName = '__inkData';
 @Deserializable('ink')
 export class InkField extends ObjectField {
-    public static InkDataFieldName = '__inkData';
-    @serializable(alias(InkField.InkDataFieldName, list(object(strokeDataSchema))))
+    @serializable(alias(InkDataFieldName, list(object(strokeDataSchema))))
     readonly inkData: InkData;
 
     constructor(data: InkData) {
diff --git a/src/fields/List.ts b/src/fields/List.ts
index ec31f7dae..f6e0473ea 100644
--- a/src/fields/List.ts
+++ b/src/fields/List.ts
@@ -1,31 +1,30 @@
 import { action, computed, makeObservable, observable } from 'mobx';
 import { alias, list, serializable } from 'serializr';
-import { DocServer } from '../client/DocServer';
 import { ScriptingGlobals } from '../client/util/ScriptingGlobals';
 import { Deserializable, afterDocDeserialize, autoObject } from '../client/util/SerializationHelper';
-import { Field } from './Doc';
+import { Field, FieldType } from './Doc';
 import { FieldTuples, Self, SelfProxy } from './DocSymbols';
 import { Copy, FieldChanged, Parent, ToJavascriptString, ToScriptString, ToString } from './FieldSymbols';
-import { ObjectField } from './ObjectField';
+import { ObjGetRefFields, ObjectField } from './ObjectField';
 import { ProxyField } from './Proxy';
 import { RefField } from './RefField';
 import { listSpec } from './Schema';
 import { Cast } from './Types';
 import { containedFieldChangedHandler, deleteProperty, getter, setter } from './util';
 
-function toObjectField(field: Field) {
+function toObjectField(field: FieldType) {
     return field instanceof RefField ? new ProxyField(field) : field;
 }
 
-function toRealField(field: Field) {
+function toRealField(field: FieldType) {
     return field instanceof ProxyField ? field.value : field;
 }
 
-type StoredType<T extends Field> = T extends RefField ? ProxyField<T> : T;
+type StoredType<T extends FieldType> = T extends RefField ? ProxyField<T> : T;
 
 export const ListFieldName = 'fields';
 @Deserializable('list')
-class ListImpl<T extends Field> extends ObjectField {
+class ListImpl<T extends FieldType> extends ObjectField {
     static listHandlers: any = {
         /// Mutator methods
         copyWithin() {
@@ -44,14 +43,14 @@ class ListImpl<T extends Field> extends ObjectField {
             this[SelfProxy][FieldChanged]?.();
             return field;
         },
-        push: action(function (this: ListImpl<any>, ...items: any[]) {
-            items = items.map(toObjectField);
+        push: action(function (this: ListImpl<any>, ...itemsIn: any[]) {
+            const items = itemsIn.map(toObjectField);
 
             const list = this[Self];
-            const length = list.__fieldTuples.length;
+            const { length } = list.__fieldTuples;
             for (let i = 0; i < items.length; i++) {
                 const item = items[i];
-                //TODO Error checking to make sure parent doesn't already exist
+                // TODO Error checking to make sure parent doesn't already exist
                 if (item instanceof ObjectField) {
                     item[Parent] = list;
                     item[FieldChanged] = containedFieldChangedHandler(this[SelfProxy], i + length, item);
@@ -77,21 +76,21 @@ class ListImpl<T extends Field> extends ObjectField {
             this[SelfProxy][FieldChanged]?.();
             return res;
         },
-        splice: action(function (this: any, start: number, deleteCount: number, ...items: any[]) {
+        splice: action(function (this: any, start: number, deleteCount: number, ...itemsIn: any[]) {
             this[Self].__realFields; // coerce retrieving entire array
-            items = items.map(toObjectField);
+            const items = itemsIn.map(toObjectField);
             const list = this[Self];
             const removed = list.__fieldTuples.filter((item: any, i: number) => i >= start && i < start + deleteCount);
             for (let i = 0; i < items.length; i++) {
                 const item = items[i];
-                //TODO Error checking to make sure parent doesn't already exist
-                //TODO Need to change indices of other fields in array
+                // TODO Error checking to make sure parent doesn't already exist
+                // TODO Need to change indices of other fields in array
                 if (item instanceof ObjectField) {
                     item[Parent] = list;
                     item[FieldChanged] = containedFieldChangedHandler(this, i + start, item);
                 }
             }
-            let hintArray: { val: any; index: number }[] = [];
+            const hintArray: { val: any; index: number }[] = [];
             for (let i = start; i < start + deleteCount; i++) {
                 hintArray.push({ val: list.__fieldTuples[i], index: i });
             }
@@ -107,13 +106,13 @@ class ListImpl<T extends Field> extends ObjectField {
             );
             return res.map(toRealField);
         }),
-        unshift(...items: any[]) {
-            items = items.map(toObjectField);
+        unshift(...itemsIn: any[]) {
+            const items = itemsIn.map(toObjectField);
             const list = this[Self];
             for (let i = 0; i < items.length; i++) {
                 const item = items[i];
-                //TODO Error checking to make sure parent doesn't already exist
-                //TODO Need to change indices of other fields in array
+                // TODO Error checking to make sure parent doesn't already exist
+                // TODO Need to change indices of other fields in array
                 if (item instanceof ObjectField) {
                     item[Parent] = list;
                     item[FieldChanged] = containedFieldChangedHandler(this, i, item);
@@ -131,9 +130,8 @@ class ListImpl<T extends Field> extends ObjectField {
         includes(valueToFind: any, fromIndex: number) {
             if (valueToFind instanceof RefField) {
                 return this[Self].__realFields.includes(valueToFind, fromIndex);
-            } else {
-                return this[Self].__fieldTuples.includes(valueToFind, fromIndex);
             }
+            return this[Self].__fieldTuples.includes(valueToFind, fromIndex);
         },
         indexOf(valueToFind: any, fromIndex: number) {
             if (valueToFind instanceof RefField) {
@@ -151,9 +149,8 @@ class ListImpl<T extends Field> extends ObjectField {
         lastIndexOf(valueToFind: any, fromIndex: number) {
             if (valueToFind instanceof RefField) {
                 return this[Self].__realFields.lastIndexOf(valueToFind, fromIndex);
-            } else {
-                return this[Self].__fieldTuples.lastIndexOf(valueToFind, fromIndex);
             }
+            return this[Self].__fieldTuples.lastIndexOf(valueToFind, fromIndex);
         },
         slice(begin: number, end: number) {
             this[Self].__realFields;
@@ -244,7 +241,7 @@ class ListImpl<T extends Field> extends ObjectField {
             getOwnPropertyDescriptor: (target, prop) => {
                 if (prop in target[FieldTuples]) {
                     return {
-                        configurable: true, //TODO Should configurable be true?
+                        configurable: true, // TODO Should configurable be true?
                         enumerable: true,
                     };
                 }
@@ -255,11 +252,12 @@ class ListImpl<T extends Field> extends ObjectField {
                 throw new Error("Currently properties can't be defined on documents using Object.defineProperty");
             },
         });
-        this[SelfProxy] = list as any as List<Field>; // bcz: ugh .. don't know how to convince typesecript that list is a List
+        this[SelfProxy] = list as any as List<FieldType>; // bcz: ugh .. don't know how to convince typesecript that list is a List
         if (fields) {
             this[SelfProxy].push(...fields);
         }
-        return list;
+        // eslint-disable-next-line no-constructor-return
+        return list; // need to return the proxy here, otherwise we don't get any of our list handler functions
     }
 
     [key: number]: T | (T extends RefField ? Promise<T> : never);
@@ -271,7 +269,7 @@ class ListImpl<T extends Field> extends ObjectField {
         // if we find any ProxyFields that don't have a current value, then
         // start the server request for all of them
         if (unrequested.length) {
-            const batchPromise = DocServer.GetRefFields(unrequested.map(p => p.fieldId));
+            const batchPromise = ObjGetRefFields(unrequested.map(p => p.fieldId));
             // as soon as we get the fields from the server, set all the list values in one
             // action to generate one React dom update.
             const allSetPromise = batchPromise.then(action(pfields => unrequested.map(toReq => toReq.setValue(pfields[toReq.fieldId]))));
@@ -308,7 +306,7 @@ class ListImpl<T extends Field> extends ObjectField {
     @observable
     private [FieldTuples]: StoredType<T>[] = [];
     private [Self] = this;
-    private [SelfProxy]: List<Field>; // also used in utils.ts even though it won't be found using find all references
+    private [SelfProxy]: List<FieldType>; // also used in utils.ts even though it won't be found using find all references
 
     [ToJavascriptString]() {
         return `[${(this as any).map((field: any) => Field.toScriptString(field))}]`;
@@ -320,10 +318,11 @@ class ListImpl<T extends Field> extends ObjectField {
         return `[${(this as any).map((field: any) => Field.toString(field))}]`;
     }
 }
-export type List<T extends Field> = ListImpl<T> & (T | (T extends RefField ? Promise<T> : never))[];
-export const List: { new <T extends Field>(fields?: T[]): List<T> } = ListImpl as any;
+export type List<T extends FieldType> = ListImpl<T> & (T | (T extends RefField ? Promise<T> : never))[];
+export const List: { new <T extends FieldType>(fields?: T[]): List<T> } = ListImpl as any;
 
 ScriptingGlobals.add('List', List);
+// eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function compareLists(l1: any, l2: any) {
     const L1 = Cast(l1, listSpec('string'), []);
     const L2 = Cast(l2, listSpec('string'), []);
diff --git a/src/fields/ObjectField.ts b/src/fields/ObjectField.ts
index e1b5b036c..6c70adc1d 100644
--- a/src/fields/ObjectField.ts
+++ b/src/fields/ObjectField.ts
@@ -1,26 +1,35 @@
-import { RefField } from './RefField';
-import { FieldChanged, Parent, Copy, ToScriptString, ToString, ToJavascriptString } from './FieldSymbols';
 import { ScriptingGlobals } from '../client/util/ScriptingGlobals';
-import { Field } from './Doc';
+import { Copy, FieldChanged, Parent, ToJavascriptString, ToScriptString, ToString } from './FieldSymbols';
+import { RefField } from './RefField';
 
 export abstract class ObjectField {
     // prettier-ignore
     public [FieldChanged]?: (diff?: { op: '$addToSet' | '$remFromSet' | '$set'; 
-                             items: Field[] | undefined; 
+                             items: FieldType[] | undefined; 
                              length: number | undefined; 
                              hint?: any }, serverOp?: any) => void;
+    // eslint-disable-next-line no-use-before-define
     public [Parent]?: RefField | ObjectField;
     abstract [Copy](): ObjectField;
 
     abstract [ToJavascriptString](): string;
     abstract [ToScriptString](): string;
     abstract [ToString](): string;
-}
-
-export namespace ObjectField {
-    export function MakeCopy<T extends ObjectField>(field: T) {
+    static MakeCopy<T extends ObjectField>(field: T) {
         return field?.[Copy]();
     }
 }
+export type FieldType = number | string | boolean | ObjectField | RefField; // bcz: hack for now .. must match the type definition in Doc.ts .. put here to avoid import cycles
 
+// eslint-disable-next-line import/no-mutable-exports
+export let ObjGetRefField: (id: string, force?: boolean) => Promise<RefField | undefined>;
+// eslint-disable-next-line import/no-mutable-exports
+export let ObjGetRefFields: (ids: string[]) => Promise<{ [id: string]: RefField | undefined }>;
+
+export function SetObjGetRefField(func: (id: string, force?: boolean) => Promise<RefField | undefined>) {
+    ObjGetRefField = func;
+}
+export function SetObjGetRefFields(func: (ids: string[]) => Promise<{ [id: string]: RefField | undefined }>) {
+    ObjGetRefFields = func;
+}
 ScriptingGlobals.add(ObjectField);
diff --git a/src/fields/RefField.ts b/src/fields/RefField.ts
index 01828dd14..1ce81368a 100644
--- a/src/fields/RefField.ts
+++ b/src/fields/RefField.ts
@@ -1,7 +1,7 @@
-import { serializable, primitive, alias } from 'serializr';
+import { alias, primitive, serializable } from 'serializr';
 import { Utils } from '../Utils';
-import { Id, HandleUpdate, ToScriptString, ToString, ToJavascriptString } from './FieldSymbols';
 import { afterDocDeserialize } from '../client/util/SerializationHelper';
+import { HandleUpdate, Id, ToJavascriptString, ToScriptString, ToString } from './FieldSymbols';
 
 export type FieldId = string;
 export abstract class RefField {
diff --git a/src/fields/RichTextUtils.ts b/src/fields/RichTextUtils.ts
index b84a91709..d75d66bf8 100644
--- a/src/fields/RichTextUtils.ts
+++ b/src/fields/RichTextUtils.ts
@@ -1,21 +1,22 @@
 import { AssertionError } from 'assert';
+import * as Color from 'color';
 import { docs_v1 } from 'googleapis';
 import { Fragment, Mark, Node } from 'prosemirror-model';
 import { sinkListItem } from 'prosemirror-schema-list';
 import { EditorState, TextSelection, Transaction } from 'prosemirror-state';
-import { GoogleApiClientUtils } from '../client/apis/google_docs/GoogleApiClientUtils';
-import { GooglePhotos } from '../client/apis/google_docs/GooglePhotosClientUtils';
+import { ClientUtils, DashColor } from '../ClientUtils';
+import { Utils } from '../Utils';
 import { DocServer } from '../client/DocServer';
-import { Docs, DocUtils } from '../client/documents/Documents';
 import { Networking } from '../client/Network';
+import { GoogleApiClientUtils } from '../client/apis/google_docs/GoogleApiClientUtils';
+import { GooglePhotos } from '../client/apis/google_docs/GooglePhotosClientUtils';
+import { DocUtils, Docs } from '../client/documents/Documents';
 import { FormattedTextBox } from '../client/views/nodes/formattedText/FormattedTextBox';
 import { schema } from '../client/views/nodes/formattedText/schema_rts';
-import { DashColor, Utils } from '../Utils';
 import { Doc, Opt } from './Doc';
 import { Id } from './FieldSymbols';
 import { RichTextField } from './RichTextField';
 import { Cast, StrCast } from './Types';
-import * as Color from 'color';
 
 export namespace RichTextUtils {
     const delimiter = '\n';
@@ -140,8 +141,8 @@ export namespace RichTextUtils {
                     inlineObjectMap.set(object.objectId!, {
                         title: embeddedObject.title || `Imported Image from ${document.title}`,
                         width,
-                        url: Utils.prepend(_m.client),
-                        agnostic: Utils.prepend(agnostic.client),
+                        url: ClientUtils.prepend(_m.client),
+                        agnostic: ClientUtils.prepend(agnostic.client),
                     });
                 }
             }
@@ -401,7 +402,7 @@ export namespace RichTextUtils {
                                         DocUtils.makeCustomViewClicked(exported, Docs.Create.FreeformDocument);
                                         linkDoc.link_anchor_2 = exported;
                                     }
-                                    url = Utils.shareUrl(exported[Id]);
+                                    url = ClientUtils.shareUrl(exported[Id]);
                                 }
                             }
                             value = { url };
diff --git a/src/fields/Schema.ts b/src/fields/Schema.ts
index f5e64ae1f..364899dc7 100644
--- a/src/fields/Schema.ts
+++ b/src/fields/Schema.ts
@@ -1,5 +1,5 @@
 import { Interface, ToInterface, Cast, ToConstructor, HasTail, Head, Tail, ListSpec, ToType, DefaultFieldConstructor } from './Types';
-import { Doc, Field } from './Doc';
+import { Doc, FieldType } from './Doc';
 import { ObjectField } from './ObjectField';
 import { RefField } from './RefField';
 import { SelfProxy } from './DocSymbols';
@@ -99,11 +99,11 @@ export function createSchema<T extends Interface>(schema: T): T & { proto: ToCon
     return schema as any;
 }
 
-export function listSpec<U extends ToConstructor<Field>>(type: U): ListSpec<ToType<U>> {
+export function listSpec<U extends ToConstructor<FieldType>>(type: U): ListSpec<ToType<U>> {
     return { List: type as any }; //TODO Types
 }
 
-export function defaultSpec<T extends ToConstructor<Field>>(type: T, defaultVal: ToType<T>): DefaultFieldConstructor<ToType<T>> {
+export function defaultSpec<T extends ToConstructor<FieldType>>(type: T, defaultVal: ToType<T>): DefaultFieldConstructor<ToType<T>> {
     return {
         type: type as any,
         defaultVal,
diff --git a/src/fields/SchemaHeaderField.ts b/src/fields/SchemaHeaderField.ts
index fb4dc4e5b..0a8dd1d9e 100644
--- a/src/fields/SchemaHeaderField.ts
+++ b/src/fields/SchemaHeaderField.ts
@@ -1,9 +1,19 @@
+import { primitive, serializable } from 'serializr';
+import { ScriptingGlobals, scriptingGlobal } from '../client/util/ScriptingGlobals';
 import { Deserializable } from '../client/util/SerializationHelper';
-import { serializable, primitive } from 'serializr';
+import { Copy, FieldChanged, ToJavascriptString, ToScriptString, ToString } from './FieldSymbols';
 import { ObjectField } from './ObjectField';
-import { Copy, ToScriptString, ToString, FieldChanged, ToJavascriptString } from './FieldSymbols';
-import { scriptingGlobal, ScriptingGlobals } from '../client/util/ScriptingGlobals';
-import { ColumnType } from '../client/views/collections/collectionSchema/CollectionSchemaView';
+
+export enum ColumnType {
+    Number,
+    String,
+    Boolean,
+    Date,
+    Image,
+    RTF,
+    Enumeration,
+    Any,
+}
 
 export const PastelSchemaPalette = new Map<string, string>([
     // ["pink1", "#FFB4E8"],
@@ -69,13 +79,14 @@ export class SchemaHeaderField extends ObjectField {
     @serializable(primitive())
     desc: boolean | undefined; // boolean determines sort order, undefined when no sort
 
+    // eslint-disable-next-line default-param-last
     constructor(heading: string = '', color: string = RandomPastel(), type?: ColumnType, width?: number, desc?: boolean, collapsed?: boolean) {
         super();
 
         this.heading = heading;
         this.color = color;
-        this.type = type ? type : 0;
-        this.width = width ? width : -1;
+        this.type = type ?? 0;
+        this.width = width ?? -1;
         this.desc = desc;
         this.collapsed = collapsed;
     }
@@ -124,6 +135,8 @@ export class SchemaHeaderField extends ObjectField {
         return `SchemaHeaderField`;
     }
 }
+
+// eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function schemaHeaderField(heading: string, color: string, type: number, width: number, desc?: boolean, collapsed?: boolean) {
     return new SchemaHeaderField(heading, color, type, width, desc, collapsed);
 });
diff --git a/src/fields/ScriptField.ts b/src/fields/ScriptField.ts
index 8b51088b2..8a3787768 100644
--- a/src/fields/ScriptField.ts
+++ b/src/fields/ScriptField.ts
@@ -1,17 +1,16 @@
 import { action, makeObservable, observable } from 'mobx';
 import { computedFn } from 'mobx-utils';
-import { createSimpleSchema, custom, map, object, primitive, PropSchema, serializable, SKIP } from 'serializr';
-import { DocServer } from '../client/DocServer';
-import { CompiledScript, CompileScript, ScriptOptions, Transformer } from '../client/util/Scripting';
-import { scriptingGlobal, ScriptingGlobals } from '../client/util/ScriptingGlobals';
-import { autoObject, Deserializable } from '../client/util/SerializationHelper';
+import { PropSchema, SKIP, createSimpleSchema, custom, map, object, primitive, serializable } from 'serializr';
 import { numberRange } from '../Utils';
-import { Doc, Field, FieldResult, Opt } from './Doc';
+import { GPTCallType, gptAPICall } from '../client/apis/gpt/GPT';
+import { CompileScript, CompiledScript, ScriptOptions, Transformer } from '../client/util/Scripting';
+import { ScriptingGlobals, scriptingGlobal } from '../client/util/ScriptingGlobals';
+import { Deserializable, autoObject } from '../client/util/SerializationHelper';
+import { Doc, Field, FieldType, FieldResult, Opt } from './Doc';
 import { Copy, FieldChanged, Id, ToJavascriptString, ToScriptString, ToString, ToValue } from './FieldSymbols';
 import { List } from './List';
-import { ObjectField } from './ObjectField';
+import { ObjGetRefField, ObjectField } from './ObjectField';
 import { Cast, StrCast } from './Types';
-import { GPTCallType, gptAPICall } from '../client/apis/gpt/GPT';
 
 function optional(propSchema: PropSchema) {
     return custom(
@@ -44,7 +43,9 @@ const scriptSchema = createSimpleSchema({
     originalScript: true,
 });
 
-function finalizeScript(script: ScriptField) {
+// eslint-disable-next-line no-use-before-define
+function finalizeScript(scriptIn: ScriptField) {
+    const script = scriptIn;
     const comp = CompileScript(script.script.originalScript, script.script.options);
     if (!comp.compiled) {
         throw new Error("Couldn't compile loaded script");
@@ -54,11 +55,13 @@ function finalizeScript(script: ScriptField) {
         if (!compset.compiled) {
             throw new Error("Couldn't compile setter script");
         }
-        (script as any).setterscript = compset;
+        script.setterscript = compset;
     }
     return comp;
 }
-async function deserializeScript(script: ScriptField) {
+// eslint-disable-next-line no-use-before-define
+async function deserializeScript(scriptIn: ScriptField) {
+    const script = scriptIn;
     if (script.captures) {
         const captured: any = {};
         (script.script.options as ScriptOptions).capturedVariables = captured;
@@ -68,13 +71,16 @@ async function deserializeScript(script: ScriptField) {
                 const val = capture.split(':')[1];
                 if (val === 'true') captured[key] = true;
                 else if (val === 'false') captured[key] = false;
-                else if (val.startsWith('ID->')) captured[key] = await DocServer.GetRefField(val.replace('ID->', ''));
+                else if (val.startsWith('ID->')) captured[key] = await ObjGetRefField(val.replace('ID->', ''));
                 else if (!isNaN(Number(val))) captured[key] = Number(val);
                 else captured[key] = val;
             })
-        ).then(() => ((script as any).script = finalizeScript(script)));
+        ).then(() => {
+            script.script = finalizeScript(script);
+        });
     } else {
-        (script as any).script = ScriptField.GetScriptFieldCache(script.script.originalScript) ?? finalizeScript(script);
+        // eslint-disable-next-line no-use-before-define
+        script.script = ScriptField.GetScriptFieldCache(script.script.originalScript) ?? finalizeScript(script);
     }
 }
 
@@ -84,9 +90,9 @@ export class ScriptField extends ObjectField {
     @serializable
     readonly rawscript: string | undefined;
     @serializable(object(scriptSchema))
-    readonly script: CompiledScript;
+    script: CompiledScript;
     @serializable(object(scriptSchema))
-    readonly setterscript: CompiledScript | undefined;
+    setterscript: CompiledScript | undefined;
     @serializable
     @observable
     _cachedResult: FieldResult = undefined;
@@ -131,6 +137,7 @@ export class ScriptField extends ObjectField {
     [ToString]() {
         return this.script.originalScript;
     }
+    // eslint-disable-next-line default-param-last
     public static CompileScript(script: string, params: object = {}, addReturn = false, capturedVariables?: { [name: string]: Doc | string | number | boolean }, transformer?: Transformer) {
         return CompileScript(script, {
             params: {
@@ -150,20 +157,23 @@ export class ScriptField extends ObjectField {
         });
     }
 
+    // eslint-disable-next-line default-param-last
     public static MakeFunction(script: string, params: object = {}, capturedVariables?: { [name: string]: Doc | string | number | boolean }) {
         const compiled = ScriptField.CompileScript(script, params, true, capturedVariables);
         return compiled.compiled ? new ScriptField(compiled) : undefined;
     }
 
+    // eslint-disable-next-line default-param-last
     public static MakeScript(script: string, params: object = {}, capturedVariables?: { [name: string]: Doc | string | number | boolean }) {
         const compiled = ScriptField.CompileScript(script, params, false, capturedVariables);
         return compiled.compiled ? new ScriptField(compiled) : undefined;
     }
-    public static CallGpt(queryText: string, setVal: (val: FieldResult) => void, target: Doc) {
+    public static CallGpt(queryTextIn: string, setVal: (val: FieldResult) => void, target: Doc) {
+        let queryText = queryTextIn;
         if (typeof queryText === 'string' && setVal) {
             while (queryText.match(/\(this\.[a-zA-Z_]*\)/)?.length) {
                 const fieldRef = queryText.split('(this.')[1].replace(/\).*/, '');
-                queryText = queryText.replace(/\(this\.[a-zA-Z_]*\)/, Field.toString(target[fieldRef] as Field));
+                queryText = queryText.replace(/\(this\.[a-zA-Z_]*\)/, Field.toString(target[fieldRef] as FieldType));
             }
             setVal(`Chat Pending: ${queryText}`);
             gptAPICall(queryText, GPTCallType.COMPLETION).then(result => {
@@ -198,24 +208,29 @@ export class ComputedField extends ScriptField {
     }
 
     _lastComputedResult: FieldResult;
-    value = (doc:Doc) => (this._lastComputedResult = this._cachedResult ?? 
-        computedFn((doc: Doc) =>
-            this.script.compiled &&
-            this.script.run( {
-                    this: doc, 
-                    //value: '',
-                    _setCacheResult_: this.setCacheResult,
-                    _last_: this._lastComputedResult,
-                    _readOnly_: true,
-                },
-                console.log
-            ).result
-        )(doc)
-    ); // prettier-ignore
+    value = (doc: Doc) => {
+        this._lastComputedResult =
+            this._cachedResult ??
+            computedFn(() =>
+                this.script.compiled &&
+                this.script.run(
+                    {
+                        this: doc,
+                        // value: '',
+                        _setCacheResult_: this.setCacheResult,
+                        _last_: this._lastComputedResult,
+                        _readOnly_: true,
+                    },
+                    console.log
+                ).result
+            )(); // prettier-ignore
+        return this._lastComputedResult;
+    };
 
-    [ToValue](doc: Doc)   { if (ComputedField.useComputed) return { value: this.value(doc) };  } // prettier-ignore
+    [ToValue](doc: Doc)   { return ComputedField.useComputed ? { value: this.value(doc) } : undefined; } // prettier-ignore
     [Copy](): ObjectField { return new ComputedField(this.script, this.setterscript, this.rawscript); } // prettier-ignore
 
+    // eslint-disable-next-line default-param-last
     public static MakeFunction(script: string, params: object = {}, capturedVariables?: { [name: string]: Doc | string | number | boolean }, setterscript?: string) {
         const compiled = ScriptField.CompileScript(script, params, true, { value: '', ...capturedVariables });
         const compiledsetter = setterscript ? ScriptField.CompileScript(setterscript, { ...params, value: 'any' }, false, capturedVariables) : undefined;
@@ -225,7 +240,7 @@ export class ComputedField extends ScriptField {
 
     public static MakeInterpolatedNumber(fieldKey: string, interpolatorKey: string, doc: Doc, curTimecode: number, defaultVal: Opt<number>) {
         if (!doc[`${fieldKey}_indexed`]) {
-            const flist = new List<number>(numberRange(curTimecode + 1).map(i => undefined) as any as number[]);
+            const flist = new List<number>(numberRange(curTimecode + 1).map(() => undefined) as any as number[]);
             flist[curTimecode] = Cast(doc[fieldKey], 'number', null);
             doc[`${fieldKey}_indexed`] = flist;
         }
@@ -235,7 +250,7 @@ export class ComputedField extends ScriptField {
     }
     public static MakeInterpolatedString(fieldKey: string, interpolatorKey: string, doc: Doc, curTimecode: number) {
         if (!doc[`${fieldKey}_`]) {
-            const flist = new List<string>(numberRange(curTimecode + 1).map(i => undefined) as any as string[]);
+            const flist = new List<string>(numberRange(curTimecode + 1).map(() => undefined) as any as string[]);
             flist[curTimecode] = StrCast(doc[fieldKey]);
             doc[`${fieldKey}_indexed`] = flist;
         }
@@ -244,9 +259,9 @@ export class ComputedField extends ScriptField {
         return getField.compiled ? new ComputedField(getField, setField?.compiled ? setField : undefined) : undefined;
     }
     public static MakeInterpolatedDataField(fieldKey: string, interpolatorKey: string, doc: Doc, curTimecode: number) {
-        if (doc[`${fieldKey}`] instanceof List) return;
+        if (doc[`${fieldKey}`] instanceof List) return undefined;
         if (!doc[`${fieldKey}_indexed`]) {
-            const flist = new List<Field>(numberRange(curTimecode + 1).map(i => undefined) as any as Field[]);
+            const flist = new List<FieldType>(numberRange(curTimecode + 1).map(() => undefined) as any as FieldType[]);
             flist[curTimecode] = Field.Copy(doc[fieldKey]);
             doc[`${fieldKey}_indexed`] = flist;
         }
@@ -257,11 +272,13 @@ export class ComputedField extends ScriptField {
             false,
             {}
         );
-        return (doc[`${fieldKey}`] = getField.compiled ? new ComputedField(getField, setField?.compiled ? setField : undefined) : undefined);
+        doc[fieldKey] = getField.compiled ? new ComputedField(getField, setField?.compiled ? setField : undefined) : undefined;
+        return doc[fieldKey];
     }
 }
 
 ScriptingGlobals.add(
+    // eslint-disable-next-line prefer-arrow-callback
     function setIndexVal(list: any[], index: number, value: any) {
         while (list.length <= index) list.push(undefined);
         list[index] = value;
@@ -271,6 +288,7 @@ ScriptingGlobals.add(
 );
 
 ScriptingGlobals.add(
+    // eslint-disable-next-line prefer-arrow-callback
     function getIndexVal(list: any[], index: number, defaultVal: Opt<number> = undefined) {
         return list?.reduce((p, x, i) => ((i <= index && x !== undefined) || p === undefined ? x : p), defaultVal);
     },
@@ -279,12 +297,14 @@ ScriptingGlobals.add(
 );
 
 ScriptingGlobals.add(
+    // eslint-disable-next-line prefer-arrow-callback
     function makeScript(script: string) {
         return ScriptField.MakeScript(script);
     },
     'returns the value at a given index of a list',
     '(list: any[], index: number)'
 );
+// eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function dashCallChat(setVal: (val: FieldResult) => void, target: Doc, queryText: string) {
     ScriptField.CallGpt(queryText, setVal, target);
 }, 'calls chat gpt for the query string and then calls setVal with the result');
diff --git a/src/fields/Types.ts b/src/fields/Types.ts
index 337e8ca21..6ed94d341 100644
--- a/src/fields/Types.ts
+++ b/src/fields/Types.ts
@@ -1,5 +1,5 @@
 import { DateField } from './DateField';
-import { Doc, Field, FieldResult, Opt } from './Doc';
+import { Doc, FieldType, FieldResult, Opt } from './Doc';
 import { List } from './List';
 import { ProxyField } from './Proxy';
 import { RefField } from './RefField';
@@ -7,54 +7,55 @@ import { RichTextField } from './RichTextField';
 import { ScriptField } from './ScriptField';
 import { CsvField, ImageField, WebField } from './URLField';
 
+export type ToConstructor<T extends FieldType> = T extends string ? 'string' : T extends number ? 'number' : T extends boolean ? 'boolean' : T extends List<infer U> ? ListSpec<U> : new (...args: any[]) => T;
+
+export type DefaultFieldConstructor<T extends FieldType> = {
+    type: ToConstructor<T>;
+    defaultVal: T;
+};
+// type ListSpec<T extends Field[]> = { List: ToContructor<Head<T>> | ListSpec<Tail<T>> };
+export type ListSpec<T extends FieldType> = { List: ToConstructor<T> };
+
+export type InterfaceValue = ToConstructor<FieldType> | ListSpec<FieldType> | DefaultFieldConstructor<FieldType> | ((doc?: Doc) => any);
+
 export type ToType<T extends InterfaceValue> = T extends 'string'
     ? string
     : T extends 'number'
-    ? number
-    : T extends 'boolean'
-    ? boolean
-    : T extends ListSpec<infer U>
-    ? List<U>
-    : // T extends { new(...args: any[]): infer R } ? (R | Promise<R>) : never;
-    T extends DefaultFieldConstructor<infer _U>
-    ? never
-    : T extends { new (...args: any[]): List<Field> }
-    ? never
-    : T extends { new (...args: any[]): infer R }
-    ? R
-    : T extends (doc?: Doc) => infer R
-    ? R
-    : never;
-
-export type ToConstructor<T extends Field> = T extends string ? 'string' : T extends number ? 'number' : T extends boolean ? 'boolean' : T extends List<infer U> ? ListSpec<U> : new (...args: any[]) => T;
+      ? number
+      : T extends 'boolean'
+        ? boolean
+        : T extends ListSpec<infer U>
+          ? List<U>
+          : // T extends { new(...args: any[]): infer R } ? (R | Promise<R>) : never;
+            T extends DefaultFieldConstructor<infer _U>
+            ? never
+            : T extends { new (...args: any[]): List<FieldType> }
+              ? never
+              : T extends { new (...args: any[]): infer R }
+                ? R
+                : T extends (doc?: Doc) => infer R
+                  ? R
+                  : never;
 
 export type ToInterface<T extends Interface> = {
     [P in Exclude<keyof T, 'proto'>]: T[P] extends DefaultFieldConstructor<infer F> ? Exclude<FieldResult<F>, undefined> : FieldResult<ToType<T[P]>>;
 };
 
-// type ListSpec<T extends Field[]> = { List: ToContructor<Head<T>> | ListSpec<Tail<T>> };
-export type ListSpec<T extends Field> = { List: ToConstructor<T> };
-
-export type DefaultFieldConstructor<T extends Field> = {
-    type: ToConstructor<T>;
-    defaultVal: T;
-};
-
 // type ListType<U extends Field[]> = { 0: List<ListType<Tail<U>>>, 1: ToType<Head<U>> }[HasTail<U> extends true ? 0 : 1];
 
 export type Head<T extends any[]> = T extends [any, ...any[]] ? T[0] : never;
 export type Tail<T extends any[]> = ((...t: T) => any) extends (_: any, ...tail: infer TT) => any ? TT : [];
 export type HasTail<T extends any[]> = T extends [] | [any] ? false : true;
-
-export type InterfaceValue = ToConstructor<Field> | ListSpec<Field> | DefaultFieldConstructor<Field> | ((doc?: Doc) => any);
-//TODO Allow you to optionally specify default values for schemas, which should then make that field not be partial
+// TODO Allow you to optionally specify default values for schemas, which should then make that field not be partial
 export interface Interface {
     [key: string]: InterfaceValue;
     // [key: string]: ToConstructor<Field> | ListSpec<Field[]>;
 }
-export type WithoutRefField<T extends Field> = T extends RefField ? never : T;
+export type WithoutRefField<T extends FieldType> = T extends RefField ? never : T;
 
-export type CastCtor = ToConstructor<Field> | ListSpec<Field>;
+export type CastCtor = ToConstructor<FieldType> | ListSpec<FieldType>;
+
+type WithoutList<T extends FieldType> = T extends List<infer R> ? (R extends RefField ? (R | Promise<R>)[] : R[]) : T;
 
 export function Cast<T extends CastCtor>(field: FieldResult, ctor: T): FieldResult<ToType<T>>;
 export function Cast<T extends CastCtor>(field: FieldResult, ctor: T, defaultVal: WithoutList<WithoutRefField<ToType<T>>> | null): WithoutList<ToType<T>>;
@@ -116,18 +117,16 @@ export function ImageCast(field: FieldResult, defaultVal: ImageField | null = nu
     return Cast(field, ImageField, defaultVal);
 }
 
-type WithoutList<T extends Field> = T extends List<infer R> ? (R extends RefField ? (R | Promise<R>)[] : R[]) : T;
-
-export function FieldValue<T extends Field, U extends WithoutList<T>>(field: FieldResult<T>, defaultValue: U): WithoutList<T>;
-export function FieldValue<T extends Field>(field: FieldResult<T>): Opt<T>;
-export function FieldValue<T extends Field>(field: FieldResult<T>, defaultValue?: T): Opt<T> {
+export function FieldValue<T extends FieldType, U extends WithoutList<T>>(field: FieldResult<T>, defaultValue: U): WithoutList<T>;
+export function FieldValue<T extends FieldType>(field: FieldResult<T>): Opt<T>;
+export function FieldValue<T extends FieldType>(field: FieldResult<T>, defaultValue?: T): Opt<T> {
     return field instanceof Promise || field === undefined ? defaultValue : field;
 }
 
 export interface PromiseLike<T> {
     then(callback: (field: Opt<T>) => void): void;
 }
-export function PromiseValue<T extends Field>(field: FieldResult<T>): PromiseLike<Opt<T>> {
+export function PromiseValue<T extends FieldType>(field: FieldResult<T>): PromiseLike<Opt<T>> {
     if (field instanceof Promise) return field as Promise<Opt<T>>;
     return {
         then(cb: (field: Opt<T>) => void) {
diff --git a/src/fields/URLField.ts b/src/fields/URLField.ts
index 87334ad16..c6c51957d 100644
--- a/src/fields/URLField.ts
+++ b/src/fields/URLField.ts
@@ -1,18 +1,14 @@
+import { custom, serializable } from 'serializr';
+import { ClientUtils } from '../ClientUtils';
+import { scriptingGlobal } from '../client/util/ScriptingGlobals';
 import { Deserializable } from '../client/util/SerializationHelper';
-import { serializable, custom } from 'serializr';
+import { Copy, ToJavascriptString, ToScriptString, ToString } from './FieldSymbols';
 import { ObjectField } from './ObjectField';
-import { ToScriptString, ToString, Copy, ToJavascriptString } from './FieldSymbols';
-import { scriptingGlobal } from '../client/util/ScriptingGlobals';
-import { Utils } from '../Utils';
 
 function url() {
     return custom(
-        function (value: URL) {
-            return value?.origin === window.location.origin ? value.pathname : value?.href;
-        },
-        function (jsonValue: string) {
-            return new URL(jsonValue, window.location.origin);
-        }
+        (value: URL) => (value?.origin === window.location.origin ? value.pathname : value?.href),
+        (jsonValue: string) => new URL(jsonValue, window.location.origin)
     );
 }
 
@@ -24,26 +20,28 @@ export abstract class URLField extends ObjectField {
     constructor(url: URL);
     constructor(url: URL | string) {
         super();
-        if (typeof url === 'string') {
-            url = url.startsWith('http') ? new URL(url) : new URL(url, window.location.origin);
-        }
-        this.url = url;
+        this.url =
+            typeof url !== 'string'
+                ? url // it's an URL
+                : url.startsWith('http')
+                  ? new URL(url)
+                  : new URL(url, window.location.origin);
     }
 
     [ToScriptString]() {
-        if (Utils.prepend(this.url?.pathname) === this.url?.href) {
+        if (ClientUtils.prepend(this.url?.pathname) === this.url?.href) {
             return `new ${this.constructor.name}("${this.url.pathname}")`;
         }
         return `new ${this.constructor.name}("${this.url?.href}")`;
     }
     [ToJavascriptString]() {
-        if (Utils.prepend(this.url?.pathname) === this.url?.href) {
+        if (ClientUtils.prepend(this.url?.pathname) === this.url?.href) {
             return `new ${this.constructor.name}("${this.url.pathname}")`;
         }
         return `new ${this.constructor.name}("${this.url?.href}")`;
     }
     [ToString]() {
-        if (Utils.prepend(this.url?.pathname) === this.url?.href) {
+        if (ClientUtils.prepend(this.url?.pathname) === this.url?.href) {
             return this.url.pathname;
         }
         return this.url?.href;
diff --git a/src/fields/util.ts b/src/fields/util.ts
index ad592391e..f87c200c1 100644
--- a/src/fields/util.ts
+++ b/src/fields/util.ts
@@ -1,12 +1,11 @@
-import { $mobx, action, observable, runInAction, trace, values } from 'mobx';
+import { $mobx, action, observable, runInAction, trace } from 'mobx';
 import { computedFn } from 'mobx-utils';
-import { returnZero } from '../Utils';
+import { ClientUtils, returnZero } from '../ClientUtils';
 import { DocServer } from '../client/DocServer';
-import { LinkManager } from '../client/util/LinkManager';
 import { SerializationHelper } from '../client/util/SerializationHelper';
 import { UndoManager } from '../client/util/UndoManager';
-import { Doc, DocListCast, Field, FieldResult, HierarchyMapping, ReverseHierarchyMap, StrListCast, aclLevel, updateCachedAcls } from './Doc';
-import { AclAdmin, AclAugment, AclEdit, AclPrivate, DocAcl, DocData, DocLayout, FieldKeys, ForceServerWrite, Height, Initializing, SelfProxy, UpdatingFromServer, Width } from './DocSymbols';
+import { Doc, DocListCast, FieldType, FieldResult, HierarchyMapping, ReverseHierarchyMap, StrListCast, aclLevel, updateCachedAcls } from './Doc';
+import { AclAdmin, AclAugment, AclEdit, AclPrivate, DirectLinks, DocAcl, DocData, DocLayout, FieldKeys, ForceServerWrite, Height, Initializing, SelfProxy, UpdatingFromServer, Width } from './DocSymbols';
 import { FieldChanged, Id, Parent, ToValue } from './FieldSymbols';
 import { List } from './List';
 import { ObjectField } from './ObjectField';
@@ -16,24 +15,44 @@ import { RichTextField } from './RichTextField';
 import { SchemaHeaderField } from './SchemaHeaderField';
 import { ComputedField } from './ScriptField';
 import { DocCast, ScriptCast, StrCast } from './Types';
-import { BaseException } from 'pdfjs-dist/types/src/shared/util';
+
+/**
+ * These are the various levels of access a user can have to a document.
+ *
+ * Admin: a user with admin access to a document can remove/edit that document, add/remove/edit annotations (depending on permissions), as well as change others' access rights to that document.
+ * Edit: a user with edit access to a document can remove/edit that document, add/remove/edit annotations (depending on permissions), but not change any access rights to that document.
+ * Add: a user with add access to a document can augment documents/annotations to that document but cannot edit or delete anything.
+ * View: a user with view access to a document can only view it - they cannot add/remove/edit anything.
+ * None: the document is not shared with that user.
+ * Unset: Remove a sharing permission (eg., used )
+ */
+export enum SharingPermissions {
+    Admin = 'Admin',
+    Edit = 'Edit',
+    Augment = 'Augment',
+    View = 'View',
+    None = 'Not-Shared',
+}
 
 function _readOnlySetter(): never {
     throw new Error("Documents can't be modified in read-only mode");
 }
 
-var tracing = false;
+// eslint-disable-next-line prefer-const
+let tracing = false;
 export function TraceMobx() {
     tracing && trace();
 }
 
-const _setterImpl = action(function (target: any, prop: string | symbol | number, value: any, receiver: any): boolean {
+export const _propSetterCB = new Map<string, ((target: any, value: any) => void) | undefined>();
+
+const _setterImpl = action((target: any, prop: string | symbol | number, valueIn: any, receiver: any): boolean => {
     if (SerializationHelper.IsSerializing() || typeof prop === 'symbol') {
-        target[prop] = value;
+        target[prop] = valueIn;
         return true;
     }
 
-    value = value?.[SelfProxy] ?? value; // convert any Doc type values to Proxy's
+    let value = valueIn?.[SelfProxy] ?? valueIn; // convert any Doc type values to Proxy's
 
     const curValue = target.__fieldTuples[prop];
     if (curValue === value || (curValue instanceof ProxyField && value instanceof RefField && curValue.fieldId === value[Id])) {
@@ -50,6 +69,7 @@ const _setterImpl = action(function (target: any, prop: string | symbol | number
             throw new Error("Can't put the same object in multiple documents at the same time");
         }
         value[Parent] = receiver;
+        // eslint-disable-next-line no-use-before-define
         value[FieldChanged] = containedFieldChangedHandler(receiver, prop, value);
     }
     if (curValue instanceof ObjectField) {
@@ -59,11 +79,12 @@ const _setterImpl = action(function (target: any, prop: string | symbol | number
 
     if (typeof prop === 'string' && _propSetterCB.has(prop)) _propSetterCB.get(prop)!(target[SelfProxy], value);
 
+    // eslint-disable-next-line no-use-before-define
     const effectiveAcl = GetEffectiveAcl(target);
 
     const writeMode = DocServer.getFieldWriteMode(prop as string);
     const fromServer = target[UpdatingFromServer];
-    const sameAuthor = fromServer || receiver.author === Doc.CurrentUserEmail;
+    const sameAuthor = fromServer || receiver.author === ClientUtils.CurrentUserEmail;
     const writeToDoc =
         sameAuthor || effectiveAcl === AclEdit || effectiveAcl === AclAdmin || writeMode === DocServer.WriteMode.Playground || writeMode === DocServer.WriteMode.LivePlayground || (effectiveAcl === AclAugment && value instanceof RichTextField);
     const writeToServer =
@@ -95,7 +116,9 @@ const _setterImpl = action(function (target: any, prop: string | symbol | number
             (!receiver[UpdatingFromServer] || receiver[ForceServerWrite]) &&
             UndoManager.AddEvent(
                 {
-                    redo: () => (receiver[prop] = value),
+                    redo: () => {
+                        receiver[prop] = value;
+                    },
                     undo: () => {
                         const wasUpdate = receiver[UpdatingFromServer];
                         const wasForce = receiver[ForceServerWrite];
@@ -131,57 +154,16 @@ export function denormalizeEmail(email: string) {
     return email.replace(/__/g, '.');
 }
 
-/**
- * Copies parent's acl fields to the child
- */
-export function inheritParentAcls(parent: Doc, child: Doc, layoutOnly: boolean) {
-    [...Object.keys(parent), ...(Doc.CurrentUserEmail !== parent.author ? ['acl-Owner'] : [])]
-        .filter(key => key.startsWith('acl'))
-        .forEach(key => {
-            // if the default acl mode is private, then don't inherit the acl-guest permission, but set it to private.
-            // const permission: string = key === 'acl-guest' && Doc.defaultAclPrivate ? AclPrivate : parent[key];
-            const parAcl = ReverseHierarchyMap.get(StrCast(key === 'acl-Owner' ? (Doc.defaultAclPrivate ? SharingPermissions.None : SharingPermissions.Edit) : parent[key]))?.acl;
-            if (parAcl) {
-                const sharePermission = HierarchyMapping.get(parAcl)?.name;
-                sharePermission && distributeAcls(key === 'acl-Owner' ? `acl-${normalizeEmail(StrCast(parent.author))}` : key, sharePermission, child, undefined, false, layoutOnly);
-            }
-        });
-}
-
-/**
- * These are the various levels of access a user can have to a document.
- *
- * Admin: a user with admin access to a document can remove/edit that document, add/remove/edit annotations (depending on permissions), as well as change others' access rights to that document.
- *
- * Edit: a user with edit access to a document can remove/edit that document, add/remove/edit annotations (depending on permissions), but not change any access rights to that document.
- *
- * Add: a user with add access to a document can augment documents/annotations to that document but cannot edit or delete anything.
- *
- * View: a user with view access to a document can only view it - they cannot add/remove/edit anything.
- *
- * None: the document is not shared with that user.
- *
- * Unset: Remove a sharing permission (eg., used )
- */
-export enum SharingPermissions {
-    Admin = 'Admin',
-    Edit = 'Edit',
-    Augment = 'Augment',
-    View = 'View',
-    None = 'Not-Shared',
-}
-
 // return acl from cache or cache the acl and return.
-const getEffectiveAclCache = computedFn(function (target: any, user?: string) {
-    return getEffectiveAcl(target, user);
-}, true);
+// eslint-disable-next-line no-use-before-define
+const getEffectiveAclCache = computedFn((target: any, user?: string) => getEffectiveAcl(target, user), true);
 
 /**
  * Calculates the effective access right to a document for the current user.
  */
 export function GetEffectiveAcl(target: any, user?: string): symbol {
     if (!target) return AclPrivate;
-    if (target[UpdatingFromServer] || Doc.CurrentUserEmail === 'guest') return AclAdmin;
+    if (target[UpdatingFromServer] || ClientUtils.CurrentUserEmail === 'guest') return AclAdmin;
     return getEffectiveAclCache(target, user); // all changes received from the server must be processed as Admin.  return this directly so that the acls aren't cached (UpdatingFromServer is not observable)
 }
 
@@ -191,10 +173,9 @@ export function GetPropAcl(target: any, prop: string | symbol | number) {
     return GetEffectiveAcl(target);
 }
 
-let cachedGroups = observable([] as string[]);
-const getCachedGroupByNameCache = computedFn(function (name: string) {
-    return cachedGroups.includes(name);
-}, true);
+const cachedGroups = observable([] as string[]);
+const getCachedGroupByNameCache = computedFn((name: string) => cachedGroups.includes(name), true);
+
 export function GetCachedGroupByName(name: string) {
     return getCachedGroupByNameCache(name);
 }
@@ -205,10 +186,10 @@ function getEffectiveAcl(target: any, user?: string): symbol {
     const targetAcls = target[DocAcl];
     if (targetAcls?.['acl-Me'] === AclAdmin || GetCachedGroupByName('Admin')) return AclAdmin;
 
-    const userChecked = user || Doc.CurrentUserEmail; // if the current user is the author of the document / the current user is a member of the admin group
+    const userChecked = user || ClientUtils.CurrentUserEmail; // if the current user is the author of the document / the current user is a member of the admin group
     if (targetAcls && Object.keys(targetAcls).length) {
         let effectiveAcl = AclPrivate;
-        for (const [key, value] of Object.entries(targetAcls)) {
+        Object.entries(targetAcls).forEach(([key, value]) => {
             // there are issues with storing fields with . in the name, so they are replaced with _ during creation
             // as a result we need to restore them again during this comparison.
             const entity = denormalizeEmail(key.substring(4)); // an individual or a group
@@ -217,7 +198,7 @@ function getEffectiveAcl(target: any, user?: string): symbol {
                     effectiveAcl = value as symbol;
                 }
             }
-        }
+        });
 
         return DocServer?.Control?.isReadOnly?.() && HierarchyMapping.get(effectiveAcl)!.level < aclLevel.editable ? AclEdit : effectiveAcl;
     }
@@ -236,9 +217,9 @@ function getEffectiveAcl(target: any, user?: string): symbol {
  * @param allowUpgrade whether permissions can be made less restrictive
  * @param layoutOnly just sets the layout doc's ACL (unless the data doc has no entry for the ACL, in which case it will be set as well)
  */
-export function distributeAcls(key: string, acl: SharingPermissions, target: Doc, visited?: Doc[], allowUpgrade?: boolean, layoutOnly = false) {
-    const selfKey = `acl-${normalizeEmail(Doc.CurrentUserEmail)}`;
-    if (!visited) visited = [] as Doc[];
+// eslint-disable-next-line default-param-last
+export function distributeAcls(key: string, acl: SharingPermissions, target: Doc, visited: Doc[] = [], allowUpgrade?: boolean, layoutOnly = false) {
+    const selfKey = `acl-${normalizeEmail(ClientUtils.CurrentUserEmail)}`;
     if (!target || visited.includes(target) || key === selfKey) return;
     visited.push(target);
 
@@ -249,23 +230,21 @@ export function distributeAcls(key: string, acl: SharingPermissions, target: Doc
     if (!layoutOnly && dataDoc && (allowUpgrade !== false || !dataDoc[key] || curVal > aclVal)) {
         // propagate ACLs to links, children, and annotations
 
-        LinkManager.Links(dataDoc).forEach(link => distributeAcls(key, acl, link, visited, allowUpgrade ? true : false));
+        dataDoc[DirectLinks].forEach(link => distributeAcls(key, acl, link, visited, !!allowUpgrade));
 
         DocListCast(dataDoc[Doc.LayoutFieldKey(dataDoc)]).forEach(d => {
-            distributeAcls(key, acl, d, visited, allowUpgrade ? true : false);
-            d !== d[DocData] && distributeAcls(key, acl, d[DocData], visited, allowUpgrade ? true : false);
+            distributeAcls(key, acl, d, visited, !!allowUpgrade);
+            d !== d[DocData] && distributeAcls(key, acl, d[DocData], visited, !!allowUpgrade);
         });
 
         DocListCast(dataDoc[Doc.LayoutFieldKey(dataDoc) + '_annotations']).forEach(d => {
-            distributeAcls(key, acl, d, visited, allowUpgrade ? true : false);
-            d !== d[DocData] && distributeAcls(key, acl, d[DocData], visited, allowUpgrade ? true : false);
+            distributeAcls(key, acl, d, visited, !!allowUpgrade);
+            d !== d[DocData] && distributeAcls(key, acl, d[DocData], visited, !!allowUpgrade);
         });
 
         Object.keys(target) // share expanded layout templates (eg, for presElementBox'es )
             .filter(lkey => lkey.includes('layout[') && DocCast(target[lkey]))
-            .map(lkey => {
-                distributeAcls(key, acl, DocCast(target[lkey]), visited, allowUpgrade ? true : false);
-            });
+            .forEach(lkey => distributeAcls(key, acl, DocCast(target[lkey]), visited, !!allowUpgrade));
 
         if (GetEffectiveAcl(dataDoc) === AclAdmin) {
             dataDoc[key] = acl;
@@ -285,7 +264,23 @@ export function distributeAcls(key: string, acl: SharingPermissions, target: Doc
     dataDocChanged && updateCachedAcls(dataDoc);
 }
 
-export var _propSetterCB = new Map<string, ((target: any, value: any) => void) | undefined>();
+/**
+ * Copies parent's acl fields to the child
+ */
+export function inheritParentAcls(parent: Doc, child: Doc, layoutOnly: boolean) {
+    [...Object.keys(parent), ...(ClientUtils.CurrentUserEmail !== parent.author ? ['acl-Owner'] : [])]
+        .filter(key => key.startsWith('acl'))
+        .forEach(key => {
+            // if the default acl mode is private, then don't inherit the acl-guest permission, but set it to private.
+            // const permission: string = key === 'acl-guest' && Doc.defaultAclPrivate ? AclPrivate : parent[key];
+            const parAcl = ReverseHierarchyMap.get(StrCast(key === 'acl-Owner' ? (Doc.defaultAclPrivate ? SharingPermissions.None : SharingPermissions.Edit) : parent[key]))?.acl;
+            if (parAcl) {
+                const sharePermission = HierarchyMapping.get(parAcl)?.name;
+                sharePermission && distributeAcls(key === 'acl-Owner' ? `acl-${normalizeEmail(StrCast(parent.author))}` : key, sharePermission, child, undefined, false, layoutOnly);
+            }
+        });
+}
+
 /**
  * sets a callback function to be called whenever a value is assigned to the specified field key.
  * For example, this is used to "publish" documents with titles that start with '@'
@@ -299,13 +294,13 @@ export function SetPropSetterCb(prop: string, setter: ((target: any, value: any)
 //
 // target should be either a Doc or ListImpl.  receiver should be a Proxy<Doc> Or List.
 //
-export function setter(target: any, in_prop: string | symbol | number, value: any, receiver: any): boolean {
-    if (!in_prop) {
+export function setter(target: any, inProp: string | symbol | number, value: any, receiver: any): boolean {
+    if (!inProp) {
         console.log('WARNING: trying to set an empty property.  This should be fixed. ');
         return false;
     }
-    let prop = in_prop;
-    const effectiveAcl = in_prop === 'constructor' || typeof in_prop === 'symbol' ? AclAdmin : GetPropAcl(target, prop);
+    let prop = inProp;
+    const effectiveAcl = inProp === 'constructor' || typeof inProp === 'symbol' ? AclAdmin : GetPropAcl(target, prop);
     if (effectiveAcl !== AclEdit && effectiveAcl !== AclAugment && effectiveAcl !== AclAdmin) return true;
     // if you're trying to change an acl but don't have Admin access / you're trying to change it to something that isn't an acceptable acl, you can't
     if (typeof prop === 'string' && prop.startsWith('acl') && (effectiveAcl !== AclAdmin || ![...Object.values(SharingPermissions), undefined].includes(value))) return true;
@@ -319,12 +314,24 @@ export function setter(target: any, in_prop: string | symbol | number, value: an
     }
     if (target.__fieldTuples[prop] instanceof ComputedField) {
         if (target.__fieldTuples[prop].setterscript && value !== undefined && !(value instanceof ComputedField)) {
-            return ScriptCast(target.__fieldTuples[prop])?.setterscript?.run({ self: target[SelfProxy], this: target[SelfProxy], value }).success ? true : false;
+            return !!ScriptCast(target.__fieldTuples[prop])?.setterscript?.run({ self: target[SelfProxy], this: target[SelfProxy], value }).success;
         }
     }
     return _setter(target, prop, value, receiver);
 }
 
+function getFieldImpl(target: any, prop: string | number, proxy: any, ignoreProto: boolean = false): any {
+    const field = target.__fieldTuples[prop];
+    const value = field?.[ToValue]?.(proxy); // converts ComputedFields to values, or unpacks ProxyFields into Proxys
+    if (value) return value.value;
+    if (field === undefined && !ignoreProto && prop !== 'proto') {
+        const proto = getFieldImpl(target, 'proto', proxy, true); // TODO tfs: instead of proxy we could use target[SelfProxy]... I don't which semantics we want or if it really matters
+        if (proto instanceof Doc && GetEffectiveAcl(proto) !== AclPrivate) {
+            return getFieldImpl(proto, prop, proxy, ignoreProto);
+        }
+    }
+    return field;
+}
 export function getter(target: any, prop: string | symbol, proxy: any): any {
     // prettier-ignore
     switch (prop) {
@@ -336,6 +343,7 @@ export function getter(target: any, prop: string | symbol, proxy: any): any {
         case $mobx:     return target.__fieldTuples[prop];
         case DocLayout: return target.__LAYOUT__;
         case Height: case Width: if (GetEffectiveAcl(target) === AclPrivate) return returnZero;
+        // eslint-disable-next-line no-fallthrough
         default :
             if (typeof prop === 'symbol')  return target[prop];
             if (prop.startsWith('isMobX')) return target[prop];
@@ -343,23 +351,11 @@ export function getter(target: any, prop: string | symbol, proxy: any): any {
             if (GetEffectiveAcl(target) === AclPrivate && prop !== 'author') return undefined;
     }
 
-    const layout_prop = prop.startsWith('_') ? prop.substring(1) : undefined;
-    if (layout_prop && target.__LAYOUT__) return target.__LAYOUT__[layout_prop];
-    return getFieldImpl(target, layout_prop ?? prop, proxy);
+    const layoutProp = prop.startsWith('_') ? prop.substring(1) : undefined;
+    if (layoutProp && target.__LAYOUT__) return target.__LAYOUT__[layoutProp];
+    return getFieldImpl(target, layoutProp ?? prop, proxy);
 }
 
-function getFieldImpl(target: any, prop: string | number, proxy: any, ignoreProto: boolean = false): any {
-    const field = target.__fieldTuples[prop];
-    const value = field?.[ToValue]?.(proxy); // converts ComputedFields to values, or unpacks ProxyFields into Proxys
-    if (value) return value.value;
-    if (field === undefined && !ignoreProto && prop !== 'proto') {
-        const proto = getFieldImpl(target, 'proto', proxy, true); //TODO tfs: instead of proxy we could use target[SelfProxy]... I don't which semantics we want or if it really matters
-        if (proto instanceof Doc && GetEffectiveAcl(proto) !== AclPrivate) {
-            return getFieldImpl(proto, prop, proxy, ignoreProto);
-        }
-    }
-    return field;
-}
 export function getField(target: any, prop: string | number, ignoreProto: boolean = false): any {
     return getFieldImpl(target, prop, target[SelfProxy], ignoreProto);
 }
@@ -382,10 +378,10 @@ export function deleteProperty(target: any, prop: string | number | symbol) {
 // were replaced.  Based on this specification, an Undo event is setup that will save enough information about the ObjectField to be
 // able to undo and redo the partial change.
 //
-export function containedFieldChangedHandler(container: List<Field> | Doc, prop: string | number, liveContainedField: ObjectField) {
+export function containedFieldChangedHandler(container: List<FieldType> | Doc, prop: string | number, liveContainedField: ObjectField) {
     let lastValue: FieldResult = liveContainedField instanceof ObjectField ? ObjectField.MakeCopy(liveContainedField) : liveContainedField;
-    return (diff?: { op: '$addToSet' | '$remFromSet' | '$set'; items: Field[] | undefined; length: number | undefined; hint?: any }, dummyServerOp?: any) => {
-        const serializeItems = () => ({ __type: 'list', fields: diff?.items?.map((item: Field) => SerializationHelper.Serialize(item)) });
+    return (diff?: { op: '$addToSet' | '$remFromSet' | '$set'; items: FieldType[] | undefined; length: number | undefined; hint?: any }, dummyServerOp?: any) => {
+        const serializeItems = () => ({ __type: 'list', fields: diff?.items?.map((item: FieldType) => SerializationHelper.Serialize(item)) });
         // prettier-ignore
         const serverOp = diff?.op === '$addToSet'
             ? { $addToSet:   { ['fields.' + prop]: serializeItems() }, length: diff.length }
@@ -401,8 +397,8 @@ export function containedFieldChangedHandler(container: List<Field> | Doc, prop:
                 UndoManager.AddEvent(
                     {
                         redo: () => {
-                            //console.log('redo $add: ' + prop, diff.items); // bcz: uncomment to log undo
-                            (container as any)[prop as any]?.push(...(diff.items || [])?.map((item: any) => item.value ?? item));
+                            // console.log('redo $add: ' + prop, diff.items); // bcz: uncomment to log undo
+                            (container as any)[prop as any]?.push(...((diff.items || [])?.map((item: any) => item.value ?? item) ?? []));
                             lastValue = ObjectField.MakeCopy((container as any)[prop as any]);
                         },
                         undo: action(() => {
@@ -416,7 +412,7 @@ export function containedFieldChangedHandler(container: List<Field> | Doc, prop:
                             });
                             lastValue = ObjectField.MakeCopy((container as any)[prop as any]);
                         }),
-                        prop: 'add ' + diff.items?.length + ' items to list',
+                        prop: 'add ' + (diff.items?.length ?? 0) + ' items to list',
                     },
                     diff?.items
                 );
@@ -447,12 +443,14 @@ export function containedFieldChangedHandler(container: List<Field> | Doc, prop:
                             });
                             lastValue = ObjectField.MakeCopy((container as any)[prop as any]);
                         },
-                        prop: 'remove ' + diff.items?.length + ' items from list',
+                        prop: 'remove ' + (diff.items?.length ?? 0) + ' items from list',
                     },
                     diff?.items
                 );
             } else {
-                const setFieldVal = (val: Field | undefined) => (container instanceof Doc ? (container[prop as string] = val) : (container[prop as number] = val as Field));
+                const setFieldVal = (val: FieldType | undefined) => {
+                    container instanceof Doc ? (container[prop as string] = val) : (container[prop as number] = val as FieldType);
+                };
                 UndoManager.AddEvent(
                     {
                         redo: () => {
diff --git a/src/mobile/ImageUpload.tsx b/src/mobile/ImageUpload.tsx
index da01e099c..4e9d85b6d 100644
--- a/src/mobile/ImageUpload.tsx
+++ b/src/mobile/ImageUpload.tsx
@@ -1,9 +1,11 @@
+/* eslint-disable jsx-a11y/no-static-element-interactions */
+/* eslint-disable jsx-a11y/click-events-have-key-events */
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { action, observable } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
 import * as rp from 'request-promise';
-import { Utils } from '../Utils';
+import { ClientUtils } from '../ClientUtils';
 import { DocServer } from '../client/DocServer';
 import { Networking } from '../client/Network';
 import { Docs } from '../client/documents/Documents';
@@ -13,8 +15,9 @@ import { List } from '../fields/List';
 import { listSpec } from '../fields/Schema';
 import { Cast } from '../fields/Types';
 import './ImageUpload.scss';
-import { MobileInterface } from './MobileInterface';
+
 const { DFLT_IMAGE_NATIVE_DIM } = require('../client/views/global/globalCssVariables.module.scss'); // prettier-ignore
+
 export interface ImageUploadProps {
     Document: Doc; // Target document for upload (upload location)
 }
@@ -27,9 +30,12 @@ export class Uploader extends React.Component<ImageUploadProps> {
     @observable error: string = '';
     @observable nm: string = 'Choose files'; // Text of 'Choose Files' button
     @observable process: string = ''; // Current status of upload
+    @observable private dialogueBoxOpacity = 1;
+    @observable private overlayOpacity = 0.4;
 
     onClick = async () => {
         try {
+            // eslint-disable-next-line react/destructuring-assignment
             const col = this.props.Document;
             await Docs.Prototypes.initialize();
             const imgPrev = document.getElementById('img_preview');
@@ -41,11 +47,12 @@ export class Uploader extends React.Component<ImageUploadProps> {
                     this.process = 'Uploading Files';
                     for (let index = 0; index < files.length; ++index) {
                         const file = files[index];
+                        // eslint-disable-next-line no-await-in-loop
                         const res = await Networking.UploadFilesToServer({ file });
                         this.setOpacity(3, '1'); // Slab 3
                         // For each item that the user has selected
                         res.map(async ({ result }) => {
-                            const name = file.name;
+                            const { name } = file;
                             if (result instanceof Error) {
                                 return;
                             }
@@ -62,7 +69,7 @@ export class Uploader extends React.Component<ImageUploadProps> {
                                 doc = Docs.Create.ImageDocument(path, { _nativeWidth: defaultNativeImageDim, _width: 400, title: name });
                             }
                             this.setOpacity(4, '1'); // Slab 4
-                            const res = await rp.get(Utils.prepend('/getUserDocumentIds'));
+                            const res = await rp.get(ClientUtils.prepend('/getUserDocumentIds'));
                             if (!res) {
                                 throw new Error('No user id returned');
                             }
@@ -72,7 +79,7 @@ export class Uploader extends React.Component<ImageUploadProps> {
                                 pending = col;
                             }
                             if (pending) {
-                                const data = await Cast(pending.data, listSpec(Doc));
+                                const data = Cast(pending.data, listSpec(Doc));
                                 if (data) data.push(doc);
                                 else pending.data = new List([doc]);
                                 this.setOpacity(5, '1'); // Slab 5
@@ -99,8 +106,7 @@ export class Uploader extends React.Component<ImageUploadProps> {
 
     // Updates label after a files is selected (so user knows a file is uploaded)
     inputLabel = async () => {
-        const files: FileList | null = inputRef.current!.files;
-        await files;
+        const files: FileList | null = await inputRef.current!.files;
         if (files && files.length === 1) {
             this.nm = files[0].name;
         } else if (files && files.length > 1) {
@@ -125,7 +131,6 @@ export class Uploader extends React.Component<ImageUploadProps> {
     // Clears the upload and closes the upload menu
     closeUpload = () => {
         this.clearUpload();
-        MobileInterface.Instance.toggleUpload();
     };
 
     // Handles the setting of the loading bar
@@ -139,17 +144,17 @@ export class Uploader extends React.Component<ImageUploadProps> {
         return (
             <div className="imgupload_cont">
                 <div className="closeUpload" onClick={() => this.closeUpload()}>
-                    <FontAwesomeIcon icon="window-close" size={'lg'} />
+                    <FontAwesomeIcon icon="window-close" size="lg" />
                 </div>
                 <FontAwesomeIcon icon="upload" size="lg" style={{ fontSize: '130' }} />
-                <input type="file" accept="application/pdf, video/*,image/*" className={`inputFile ${this.nm !== 'Choose files' ? 'active' : ''}`} id="input_image_file" ref={inputRef} onChange={this.inputLabel} multiple></input>
+                <input type="file" accept="application/pdf, video/*,image/*" className={`inputFile ${this.nm !== 'Choose files' ? 'active' : ''}`} id="input_image_file" ref={inputRef} onChange={this.inputLabel} multiple />
                 <label className="file" id="label" htmlFor="input_image_file">
                     {this.nm}
                 </label>
                 <div className="upload_label" onClick={this.onClick}>
                     Upload
                 </div>
-                <img id="img_preview" src=""></img>
+                <img id="img_preview" src="" alt="" />
                 <div className="loadingImage">
                     <div className="loadingSlab" id="slab1" />
                     <div className="loadingSlab" id="slab2" />
@@ -164,10 +169,7 @@ export class Uploader extends React.Component<ImageUploadProps> {
         );
     }
 
-    @observable private dialogueBoxOpacity = 1;
-    @observable private overlayOpacity = 0.4;
-
     render() {
-        return <MainViewModal contents={this.uploadInterface} isDisplayed={true} interactive={true} dialogueBoxDisplayedOpacity={this.dialogueBoxOpacity} overlayDisplayedOpacity={this.overlayOpacity} closeOnExternalClick={this.closeUpload} />;
+        return <MainViewModal contents={this.uploadInterface} isDisplayed interactive dialogueBoxDisplayedOpacity={this.dialogueBoxOpacity} overlayDisplayedOpacity={this.overlayOpacity} closeOnExternalClick={this.closeUpload} />;
     }
 }
diff --git a/src/mobile/MobileInkOverlay.tsx b/src/mobile/MobileInkOverlay.tsx
index 23e19585a..6babd2f39 100644
--- a/src/mobile/MobileInkOverlay.tsx
+++ b/src/mobile/MobileInkOverlay.tsx
@@ -4,7 +4,7 @@ import * as React from 'react';
 import { DocServer } from '../client/DocServer';
 import { DragManager } from '../client/util/DragManager';
 import { Doc } from '../fields/Doc';
-import { GestureUtils } from '../pen-gestures/GestureUtils';
+import { Gestures } from '../pen-gestures/GestureTypes';
 import { GestureContent, MobileDocumentUploadContent, MobileInkOverlayContent, UpdateMobileInkOverlayPositionContent } from '../server/Message';
 import './MobileInkOverlay.scss';
 
@@ -76,11 +76,11 @@ export default class MobileInkOverlay extends React.Component {
 
         const target = document.elementFromPoint(this._x + 10, this._y + 10);
         target?.dispatchEvent(
-            new CustomEvent<GestureUtils.GestureEvent>('dashOnGesture', {
+            new CustomEvent<GestureEvent>('dashOnGesture', {
                 bubbles: true,
                 detail: {
                     points: points,
-                    gesture: GestureUtils.Gestures.Stroke,
+                    gesture: Gestures.Stroke,
                     bounds: B,
                 },
             })
diff --git a/src/mobile/MobileInterface.tsx b/src/mobile/MobileInterface.tsx
index dc4af8303..1d4c0adce 100644
--- a/src/mobile/MobileInterface.tsx
+++ b/src/mobile/MobileInterface.tsx
@@ -104,7 +104,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { action, computed, observable, runInAction } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
-import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue } from '../Utils';
+import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue } from '../ClientUtils';
 import { CollectionViewType, DocumentType } from '../client/documents/DocumentTypes';
 import { Docs, DocumentOptions } from '../client/documents/Documents';
 import { CurrentUserUtils } from '../client/util/CurrentUserUtils';
diff --git a/src/pen-gestures/GestureTypes.ts b/src/pen-gestures/GestureTypes.ts
new file mode 100644
index 000000000..d86562580
--- /dev/null
+++ b/src/pen-gestures/GestureTypes.ts
@@ -0,0 +1,16 @@
+export enum Gestures {
+    Line = 'line',
+    Stroke = 'stroke',
+    Scribble = 'scribble',
+    Text = 'text',
+    Triangle = 'triangle',
+    Circle = 'circle',
+    Rectangle = 'rectangle',
+    Arrow = 'arrow',
+}
+
+// Defines a point in an ink as a pair of x- and y-coordinates.
+export interface PointData {
+    X: number;
+    Y: number;
+}
diff --git a/src/pen-gestures/GestureUtils.ts b/src/pen-gestures/GestureUtils.ts
index 41917aac9..28323d62f 100644
--- a/src/pen-gestures/GestureUtils.ts
+++ b/src/pen-gestures/GestureUtils.ts
@@ -1,32 +1,32 @@
 import { Rect } from 'react-measure';
-import { PointData } from '../fields/InkField';
+import { Gestures, PointData } from './GestureTypes';
 import { NDollarRecognizer } from './ndollar';
 
 export namespace GestureUtils {
     export class GestureEvent {
-        constructor(readonly gesture: Gestures, readonly points: PointData[], readonly bounds: Rect, readonly text?: any) {}
+        readonly gesture: Gestures;
+        readonly points: PointData[];
+        readonly bounds: Rect;
+        readonly text?: any;
+
+        constructor(gesture: Gestures, points: PointData[], bounds: Rect, text?: any) {
+            this.gesture = gesture;
+            this.points = points;
+            this.bounds = bounds;
+            this.text = text;
+        }
     }
 
     export interface GestureEventDisposer {
         (): void;
     }
 
+    // eslint-disable-next-line no-undef
     export function MakeGestureTarget(element: HTMLElement, func: (e: Event, ge: GestureEvent) => void): GestureEventDisposer {
         const handler = (e: Event) => func(e, (e as CustomEvent<GestureEvent>).detail);
         element.addEventListener('dashOnGesture', handler);
         return () => element.removeEventListener('dashOnGesture', handler);
     }
 
-    export enum Gestures {
-        Line = 'line',
-        Stroke = 'stroke',
-        Scribble = 'scribble',
-        Text = 'text',
-        Triangle = 'triangle',
-        Circle = 'circle',
-        Rectangle = 'rectangle',
-        Arrow = 'arrow',
-    }
-
     export const GestureRecognizer = new NDollarRecognizer(false);
 }
diff --git a/src/pen-gestures/ndollar.ts b/src/pen-gestures/ndollar.ts
index 3ee9506cb..d28d303b8 100644
--- a/src/pen-gestures/ndollar.ts
+++ b/src/pen-gestures/ndollar.ts
@@ -1,4 +1,4 @@
-import { GestureUtils } from './GestureUtils';
+import { Gestures } from './GestureTypes';
 
 /**
  * The $N Multistroke Recognizer (JavaScript version)
@@ -172,7 +172,7 @@ export class NDollarRecognizer {
         //
         this.Multistrokes.push(
             new Multistroke(
-                GestureUtils.Gestures.Rectangle,
+                Gestures.Rectangle,
                 useBoundedRotationInvariance,
                 new Array(
                     new Array(
@@ -185,17 +185,17 @@ export class NDollarRecognizer {
                 )
             )
         );
-        this.Multistrokes.push(new Multistroke(GestureUtils.Gestures.Line, useBoundedRotationInvariance, new Array(new Array(new Point(12, 347), new Point(119, 347)))));
+        this.Multistrokes.push(new Multistroke(Gestures.Line, useBoundedRotationInvariance, new Array(new Array(new Point(12, 347), new Point(119, 347)))));
         this.Multistrokes.push(
             new Multistroke(
-                GestureUtils.Gestures.Triangle, // equilateral
+                Gestures.Triangle, // equilateral
                 useBoundedRotationInvariance,
                 new Array(new Array(new Point(40, 100), new Point(100, 200), new Point(140, 102), new Point(42, 100)))
             )
         );
         this.Multistrokes.push(
             new Multistroke(
-                GestureUtils.Gestures.Circle,
+                Gestures.Circle,
                 useBoundedRotationInvariance,
                 new Array(
                     new Array(
diff --git a/src/server/ActionUtilities.ts b/src/server/ActionUtilities.ts
index 55b50cc12..6f5b9272a 100644
--- a/src/server/ActionUtilities.ts
+++ b/src/server/ActionUtilities.ts
@@ -1,14 +1,14 @@
 import { exec } from 'child_process';
 import { Color, yellow } from 'colors';
 import { createWriteStream, exists, mkdir, readFile, unlink, writeFile } from 'fs';
-import * as nodemailer from "nodemailer";
-import { MailOptions } from "nodemailer/lib/json-transport";
+import * as nodemailer from 'nodemailer';
+import { MailOptions } from 'nodemailer/lib/json-transport';
 import * as path from 'path';
-import { rimraf } from "rimraf";
+import { rimraf } from 'rimraf';
 import { ExecOptions } from 'shelljs';
 import * as Mail from 'nodemailer/lib/mailer';
 
-const projectRoot = path.resolve(__dirname, "../../");
+const projectRoot = path.resolve(__dirname, '../../');
 export function pathFromRoot(relative?: string) {
     if (!relative) {
         return projectRoot;
@@ -18,7 +18,7 @@ export function pathFromRoot(relative?: string) {
 
 export async function fileDescriptorFromStream(path: string) {
     const logStream = createWriteStream(path);
-    return new Promise<number>(resolve => logStream.on("open", resolve));
+    return new Promise<number>(resolve => logStream.on('open', resolve));
 }
 
 export const command_line = (command: string, fromDirectory?: string) => {
@@ -27,25 +27,25 @@ export const command_line = (command: string, fromDirectory?: string) => {
         if (fromDirectory) {
             options.cwd = fromDirectory ? path.resolve(projectRoot, fromDirectory) : projectRoot;
         }
-        exec(command, options, (err, stdout) => err ? reject(err) : resolve(stdout));
+        exec(command, options, (err, stdout) => (err ? reject(err) : resolve(stdout)));
     });
 };
 
 export const read_text_file = (relativePath: string) => {
     const target = path.resolve(__dirname, relativePath);
     return new Promise<string>((resolve, reject) => {
-        readFile(target, (err, data) => err ? reject(err) : resolve(data.toString()));
+        readFile(target, (err, data) => (err ? reject(err) : resolve(data.toString())));
     });
 };
 
 export const write_text_file = (relativePath: string, contents: any) => {
     const target = path.resolve(__dirname, relativePath);
     return new Promise<void>((resolve, reject) => {
-        writeFile(target, contents, (err) => err ? reject(err) : resolve());
+        writeFile(target, contents, err => (err ? reject(err) : resolve()));
     });
 };
 
-export type Messager<T> = (outcome: { result: T | undefined, error: Error | null }) => string;
+export type Messager<T> = (outcome: { result: T | undefined; error: Error | null }) => string;
 
 export interface LogData<T> {
     startMessage: string;
@@ -56,22 +56,23 @@ export interface LogData<T> {
 }
 
 let current = Math.ceil(Math.random() * 20);
-export async function log_execution<T>({ startMessage, endMessage, action, color }: LogData<T>): Promise<T | undefined> {
-    let result: T | undefined = undefined, error: Error | null = null;
-    const resolvedColor = color || `\x1b[${31 + ++current % 6}m%s\x1b[0m`;
+export async function logExecution<T>({ startMessage, endMessage, action, color }: LogData<T>): Promise<T | undefined> {
+    let result: T | undefined = undefined,
+        error: Error | null = null;
+    const resolvedColor = color || `\x1b[${31 + (++current % 6)}m%s\x1b[0m`;
     log_helper(`${startMessage}...`, resolvedColor);
     try {
         result = await action();
     } catch (e: any) {
         error = e;
     } finally {
-        log_helper(typeof endMessage === "string" ? endMessage : endMessage({ result, error }), resolvedColor);
+        log_helper(typeof endMessage === 'string' ? endMessage : endMessage({ result, error }), resolvedColor);
     }
     return result;
 }
 
 function log_helper(content: string, color: Color | string) {
-    if (typeof color === "string") {
+    if (typeof color === 'string') {
         console.log(color, content);
     } else {
         console.log(color(content));
@@ -88,11 +89,11 @@ export function msToTime(duration: number) {
         minutes = Math.floor((duration / (1000 * 60)) % 60),
         hours = Math.floor((duration / (1000 * 60 * 60)) % 24);
 
-    const hoursS = (hours < 10) ? "0" + hours : hours;
-    const minutesS = (minutes < 10) ? "0" + minutes : minutes;
-    const secondsS = (seconds < 10) ? "0" + seconds : seconds;
+    const hoursS = hours < 10 ? '0' + hours : hours;
+    const minutesS = minutes < 10 ? '0' + minutes : minutes;
+    const secondsS = seconds < 10 ? '0' + seconds : seconds;
 
-    return hoursS + ":" + minutesS + ":" + secondsS + "." + milliseconds;
+    return hoursS + ':' + minutesS + ':' + secondsS + '.' + milliseconds;
 }
 
 export const createIfNotExists = async (path: string) => {
@@ -112,13 +113,12 @@ export async function Prune(rootDirectory: string): Promise<boolean> {
 export const Destroy = (mediaPath: string) => new Promise<boolean>(resolve => unlink(mediaPath, error => resolve(error === null)));
 
 export namespace Email {
-
     const smtpTransport = nodemailer.createTransport({
         service: 'Gmail',
         auth: {
             user: 'browndashptc@gmail.com',
-            pass: 'TsarNicholas#2'
-        }
+            pass: 'TsarNicholas#2',
+        },
     });
 
     export interface DispatchOptions<T extends string | string[]> {
@@ -135,16 +135,18 @@ export namespace Email {
 
     export async function dispatchAll({ to, subject, content, attachments }: DispatchOptions<string[]>) {
         const failures: DispatchFailure[] = [];
-        await Promise.all(to.map(async recipient => {
-            let error: Error | null;
-            const resolved = attachments ? "length" in attachments ? attachments : [attachments] : undefined;
-            if ((error = await Email.dispatch({ to: recipient, subject, content, attachments: resolved })) !== null) {
-                failures.push({
-                    recipient,
-                    error
-                });
-            }
-        }));
+        await Promise.all(
+            to.map(async recipient => {
+                let error: Error | null;
+                const resolved = attachments ? ('length' in attachments ? attachments : [attachments]) : undefined;
+                if ((error = await Email.dispatch({ to: recipient, subject, content, attachments: resolved })) !== null) {
+                    failures.push({
+                        recipient,
+                        error,
+                    });
+                }
+            })
+        );
         return failures.length ? failures : undefined;
     }
 
@@ -153,10 +155,9 @@ export namespace Email {
             to,
             from: 'browndashptc@gmail.com',
             subject,
-            text: `Hello ${to.split("@")[0]},\n\n${content}`,
-            attachments
+            text: `Hello ${to.split('@')[0]},\n\n${content}`,
+            attachments,
         } as MailOptions;
         return new Promise<Error | null>(resolve => smtpTransport.sendMail(mailOptions, resolve));
     }
-
-}
\ No newline at end of file
+}
diff --git a/src/server/ApiManagers/DataVizManager.ts b/src/server/ApiManagers/DataVizManager.ts
index 0d43130d1..88f22992d 100644
--- a/src/server/ApiManagers/DataVizManager.ts
+++ b/src/server/ApiManagers/DataVizManager.ts
@@ -1,14 +1,14 @@
-import { csvParser, csvToString } from "../DataVizUtils";
-import { Method, _success } from "../RouteManager";
-import ApiManager, { Registration } from "./ApiManager";
-import { Directory, serverPathToFile } from "./UploadManager";
 import * as path from 'path';
+import { csvParser, csvToString } from '../DataVizUtils';
+import { Method, _success } from '../RouteManager';
+import { Directory, serverPathToFile } from '../SocketData';
+import ApiManager, { Registration } from './ApiManager';
 
 export default class DataVizManager extends ApiManager {
     protected initialize(register: Registration): void {
         register({
             method: Method.GET,
-            subscription: "/csvData",
+            subscription: '/csvData',
             secureHandler: async ({ req, res }) => {
                 const uri = req.query.uri as string;
 
@@ -19,8 +19,7 @@ export default class DataVizManager extends ApiManager {
                     _success(res, parsedCsv);
                     resolve();
                 });
-            }
+            },
         });
     }
-    
-}
\ No newline at end of file
+}
diff --git a/src/server/ApiManagers/DeleteManager.ts b/src/server/ApiManagers/DeleteManager.ts
index c6c4ca464..9a9b807ae 100644
--- a/src/server/ApiManagers/DeleteManager.ts
+++ b/src/server/ApiManagers/DeleteManager.ts
@@ -1,5 +1,5 @@
 import ApiManager, { Registration } from './ApiManager';
-import { Method, _permission_denied } from '../RouteManager';
+import { Method, _permissionDenied } from '../RouteManager';
 import { WebSocket } from '../websocket';
 import { Database } from '../database';
 import { rimraf } from 'rimraf';
diff --git a/src/server/ApiManagers/DownloadManager.ts b/src/server/ApiManagers/DownloadManager.ts
index 2175b6db6..b105c825c 100644
--- a/src/server/ApiManagers/DownloadManager.ts
+++ b/src/server/ApiManagers/DownloadManager.ts
@@ -1,13 +1,13 @@
-import ApiManager, { Registration } from "./ApiManager";
-import { Method } from "../RouteManager";
-import RouteSubscriber from "../RouteSubscriber";
 import * as Archiver from 'archiver';
 import * as express from 'express';
-import { Database } from "../database";
-import * as path from "path";
-import { DashUploadUtils, SizeSuffix } from "../DashUploadUtils";
-import { publicDirectory } from "..";
-import { serverPathToFile, Directory } from "./UploadManager";
+import * as path from 'path';
+import { URL } from 'url';
+import { DashUploadUtils, SizeSuffix } from '../DashUploadUtils';
+import { Method } from '../RouteManager';
+import RouteSubscriber from '../RouteSubscriber';
+import { Directory, publicDirectory, serverPathToFile } from '../SocketData';
+import { Database } from '../database';
+import ApiManager, { Registration } from './ApiManager';
 
 export type Hierarchy = { [id: string]: string | Hierarchy };
 export type ZipMutator = (file: Archiver.Archiver) => void | Promise<void>;
@@ -16,147 +16,45 @@ export interface DocumentElements {
     title: string;
 }
 
-export default class DownloadManager extends ApiManager {
-
-    protected initialize(register: Registration): void {
-
-        /**
-         * Let's say someone's using Dash to organize images in collections.
-         * This lets them export the hierarchy they've built to their
-         * own file system in a useful format.
-         * 
-         * This handler starts with a single document id (interesting only
-         * if it's that of a collection). It traverses the database, captures
-         * the nesting of only nested images or collections, writes
-         * that to a zip file and returns it to the client for download.
-         */
-        register({
-            method: Method.GET,
-            subscription: new RouteSubscriber("imageHierarchyExport").add('docId'),
-            secureHandler: async ({ req, res }) => {
-                const id = req.params.docId;
-                const hierarchy: Hierarchy = {};
-                await buildHierarchyRecursive(id, hierarchy);
-                return BuildAndDispatchZip(res, zip => writeHierarchyRecursive(zip, hierarchy));
-            }
-        });
-
-        register({
-            method: Method.GET,
-            subscription: new RouteSubscriber("downloadId").add("docId"),
-            secureHandler: async ({ req, res }) => {
-                return BuildAndDispatchZip(res, async zip => {
-                    const { id, docs, files } = await getDocs(req.params.docId);
-                    const docString = JSON.stringify({ id, docs });
-                    zip.append(docString, { name: "doc.json" });
-                    files.forEach(val => {
-                        zip.file(publicDirectory + val, { name: val.substring(1) });
-                    });
-                });
-            }
-        });
-
-        register({
-            method: Method.GET,
-            subscription: new RouteSubscriber("serializeDoc").add("docId"),
-            secureHandler: async ({ req, res }) => {
-                const { docs, files } = await getDocs(req.params.docId);
-                res.send({ docs, files: Array.from(files) });
-            }
-        });
-
-    }
-
-}
-
-async function getDocs(id: string) {
-    const files = new Set<string>();
-    const docs: { [id: string]: any } = {};
-    const fn = (doc: any): string[] => {
-        const id = doc.id;
-        if (typeof id === "string" && id.endsWith("Proto")) {
-            //Skip protos
-            return [];
-        }
-        const ids: string[] = [];
-        for (const key in doc.fields) {
-            if (!doc.fields.hasOwnProperty(key)) { continue; }
-            const field = doc.fields[key];
-            if (field === undefined || field === null) { continue; }
-
-            if (field.__type === "proxy" || field.__type === "prefetch_proxy") {
-                ids.push(field.fieldId);
-            } else if (field.__type === "script" || field.__type === "computed") {
-                field.captures && ids.push(field.captures.fieldId);
-            } else if (field.__type === "list") {
-                ids.push(...fn(field));
-            } else if (typeof field === "string") {
-                const re = /"(?:dataD|d)ocumentId"\s*:\s*"([\w\-]*)"/g;
-                let match: string[] | null;
-                while ((match = re.exec(field)) !== null) {
-                    ids.push(match[1]);
-                }
-            } else if (field.__type === "RichTextField") {
-                const re = /"href"\s*:\s*"(.*?)"/g;
-                let match: string[] | null;
-                while ((match = re.exec(field.Data)) !== null) {
-                    const urlString = match[1];
-                    const split = new URL(urlString).pathname.split("doc/");
-                    if (split.length > 1) {
-                        ids.push(split[split.length - 1]);
-                    }
-                }
-                const re2 = /"src"\s*:\s*"(.*?)"/g;
-                while ((match = re2.exec(field.Data)) !== null) {
-                    const urlString = match[1];
-                    const pathname = new URL(urlString).pathname;
-                    files.add(pathname);
-                }
-            } else if (["audio", "image", "video", "pdf", "web", "map"].includes(field.__type)) {
-                const url = new URL(field.url);
-                const pathname = url.pathname;
-                files.add(pathname);
-            }
-        }
-
-        if (doc.id) {
-            docs[doc.id] = doc;
-        }
-        return ids;
-    };
-    await Database.Instance.visit([id], fn);
-    return { id, docs, files };
-}
-
 /**
- * This utility function factors out the process
- * of creating a zip file and sending it back to the client
- * by piping it into a response.
- * 
- * Learn more about piping and readable / writable streams here!
- * https://www.freecodecamp.org/news/node-js-streams-everything-you-need-to-know-c9141306be93/ 
- * 
- * @param res the writable stream response object that will transfer the generated zip file
- * @param mutator the callback function used to actually modify and insert information into the zip instance
+ * This is a very specific utility method to help traverse the database
+ * to parse data and titles out of images and collections alone.
+ *
+ * We don't know if the document id given to is corresponds to a view document or a data
+ * document. If it's a data document, the response from the database will have
+ * a data field. If not, call recursively on the proto, and resolve with *its* data
+ *
+ * @param targetId the id of the Dash document whose data is being requests
+ * @returns the data of the document, as well as its title
  */
-export async function BuildAndDispatchZip(res: express.Response, mutator: ZipMutator): Promise<void> {
-    res.set('Content-disposition', `attachment;`);
-    res.set('Content-Type', "application/zip");
-    const zip = Archiver('zip');
-    zip.pipe(res);
-    await mutator(zip);
-    return zip.finalize();
+async function getData(targetId: string): Promise<DocumentElements> {
+    return new Promise<DocumentElements>((resolve, reject) => {
+        Database.Instance.getDocument(targetId, async (result: any) => {
+            const { data, proto, title } = result.fields;
+            if (data) {
+                if (data.url) {
+                    resolve({ data: data.url, title });
+                } else if (data.fields) {
+                    resolve({ data: data.fields, title });
+                } else {
+                    reject();
+                }
+            } else if (proto) {
+                getData(proto.fieldId).then(resolve, reject);
+            } else {
+                reject();
+            }
+        });
+    });
 }
 
 /**
  * This function starts with a single document id as a seed,
  * typically that of a collection, and then descends the entire tree
- * of image or collection documents that are reachable from that seed. 
+ * of image or collection documents that are reachable from that seed.
  * @param seedId the id of the root of the subtree we're trying to capture, interesting only if it's a collection
  * @param hierarchy the data structure we're going to use to record the nesting of the collections and images as we descend
- */
-
-/*
+ 
 Below is an example of the JSON hierarchy built from two images contained inside a collection titled 'a nested collection',
 following the general recursive structure shown immediately below
 {
@@ -190,74 +88,175 @@ async function buildHierarchyRecursive(seedId: string, hierarchy: Hierarchy): Pr
 }
 
 /**
- * This is a very specific utility method to help traverse the database
- * to parse data and titles out of images and collections alone.
- * 
- * We don't know if the document id given to is corresponds to a view document or a data
- * document. If it's a data document, the response from the database will have
- * a data field. If not, call recursively on the proto, and resolve with *its* data
- * 
- * @param targetId the id of the Dash document whose data is being requests
- * @returns the data of the document, as well as its title
+ * This utility function factors out the process
+ * of creating a zip file and sending it back to the client
+ * by piping it into a response.
+ *
+ * Learn more about piping and readable / writable streams here!
+ * https://www.freecodecamp.org/news/node-js-streams-everything-you-need-to-know-c9141306be93/
+ *
+ * @param res the writable stream response object that will transfer the generated zip file
+ * @param mutator the callback function used to actually modify and insert information into the zip instance
  */
-async function getData(targetId: string): Promise<DocumentElements> {
-    return new Promise<DocumentElements>((resolve, reject) => {
-        Database.Instance.getDocument(targetId, async (result: any) => {
-            const { data, proto, title } = result.fields;
-            if (data) {
-                if (data.url) {
-                    resolve({ data: data.url, title });
-                } else if (data.fields) {
-                    resolve({ data: data.fields, title });
-                } else {
-                    reject();
-                }
-            } else if (proto) {
-                getData(proto.fieldId).then(resolve, reject);
-            } else {
-                reject();
-            }
-        });
-    });
+export async function BuildAndDispatchZip(res: express.Response, mutator: ZipMutator): Promise<void> {
+    res.set('Content-disposition', `attachment;`);
+    res.set('Content-Type', 'application/zip');
+    const zip = Archiver('zip');
+    zip.pipe(res);
+    await mutator(zip);
+    return zip.finalize();
 }
 
 /**
- * 
+ *
  * @param file the zip file to which we write the files
  * @param hierarchy the data structure from which we read, defining the nesting of the documents in the zip
  * @param prefix lets us create nested folders in the zip file by continually appending to the end
  * of the prefix with each layer of recursion.
- * 
+ *
  * Function Call #1 => "Dash Export"
  * Function Call #2 => "Dash Export/a nested collection"
  * Function Call #3 => "Dash Export/a nested collection/lowest level collection"
  * ...
  */
-async function writeHierarchyRecursive(file: Archiver.Archiver, hierarchy: Hierarchy, prefix = "Dash Export"): Promise<void> {
-    for (const documentTitle of Object.keys(hierarchy)) {
-        const result = hierarchy[documentTitle];
-        // base case or leaf node, we've hit a url (image)
-        if (typeof result === "string") {
-            let path: string;
-            let matches: RegExpExecArray | null;
-            if ((matches = /\:\d+\/files\/images\/(upload\_[\da-z]{32}.*)/g.exec(result)) !== null) {
-                // image already exists on our server
-                path = serverPathToFile(Directory.images, matches[1]);
+async function writeHierarchyRecursive(file: Archiver.Archiver, hierarchy: Hierarchy, prefix = 'Dash Export'): Promise<void> {
+    // eslint-disable-next-line no-restricted-syntax
+    for (const documentTitle in hierarchy) {
+        if (Object.prototype.hasOwnProperty.call(hierarchy, documentTitle)) {
+            const result = hierarchy[documentTitle];
+            // base case or leaf node, we've hit a url (image)
+            if (typeof result === 'string') {
+                let fPath: string;
+                const matches = /:\d+\/files\/images\/(upload_[\da-z]{32}.*)/g.exec(result);
+                if (matches !== null) {
+                    // image already exists on our server
+                    fPath = serverPathToFile(Directory.images, matches[1]);
+                } else {
+                    // the image doesn't already exist on our server (may have been dragged
+                    // and dropped in the browser and thus hosted remotely) so we upload it
+                    // to our server and point the zip file to it, so it can bundle up the bytes
+                    // eslint-disable-next-line no-await-in-loop
+                    const information = await DashUploadUtils.UploadImage(result);
+                    fPath = information instanceof Error ? '' : information.accessPaths[SizeSuffix.Original].server;
+                }
+                // write the file specified by the path to the directory in the
+                // zip file given by the prefix.
+                if (fPath) {
+                    file.file(fPath, { name: documentTitle, prefix });
+                }
             } else {
-                // the image doesn't already exist on our server (may have been dragged
-                // and dropped in the browser and thus hosted remotely) so we upload it
-                // to our server and point the zip file to it, so it can bundle up the bytes
-                const information = await DashUploadUtils.UploadImage(result);
-                path = information instanceof Error ? "" : information.accessPaths[SizeSuffix.Original].server;
+                // we've hit a collection, so we have to recurse
+                // eslint-disable-next-line no-await-in-loop
+                await writeHierarchyRecursive(file, result, `${prefix}/${documentTitle}`);
             }
-            // write the file specified by the path to the directory in the
-            // zip file given by the prefix.
-            if (path) {
-                file.file(path, { name: documentTitle, prefix });
+        }
+    }
+}
+
+async function getDocs(id: string) {
+    const files = new Set<string>();
+    const docs: { [id: string]: any } = {};
+    const fn = (doc: any): string[] => {
+        const { id } = doc;
+        if (typeof id === 'string' && id.endsWith('Proto')) {
+            // Skip protos
+            return [];
+        }
+        const ids: string[] = [];
+        // eslint-disable-next-line no-restricted-syntax
+        for (const key in doc.fields) {
+            // eslint-disable-next-line no-continue
+            if (!Object.prototype.hasOwnProperty.call(doc.fields, key)) continue;
+
+            const field = doc.fields[key];
+            // eslint-disable-next-line no-continue
+            if (field === undefined || field === null) continue;
+
+            if (field.__type === 'proxy' || field.__type === 'prefetch_proxy') {
+                ids.push(field.fieldId);
+            } else if (field.__type === 'script' || field.__type === 'computed') {
+                field.captures && ids.push(field.captures.fieldId);
+            } else if (field.__type === 'list') {
+                ids.push(...fn(field));
+            } else if (typeof field === 'string') {
+                const re = /"(?:dataD|d)ocumentId"\s*:\s*"([\w-]*)"/g;
+                for (let match = re.exec(field); match !== null; match = re.exec(field)) {
+                    ids.push(match[1]);
+                }
+            } else if (field.__type === 'RichTextField') {
+                const re = /"href"\s*:\s*"(.*?)"/g;
+                for (let match = re.exec(field.data); match !== null; match = re.exec(field.Data)) {
+                    const urlString = match[1];
+                    const split = new URL(urlString).pathname.split('doc/');
+                    if (split.length > 1) {
+                        ids.push(split[split.length - 1]);
+                    }
+                }
+                const re2 = /"src"\s*:\s*"(.*?)"/g;
+                for (let match = re2.exec(field.Data); match !== null; match = re2.exec(field.Data)) {
+                    const urlString = match[1];
+                    const { pathname } = new URL(urlString);
+                    files.add(pathname);
+                }
+            } else if (['audio', 'image', 'video', 'pdf', 'web', 'map'].includes(field.__type)) {
+                const { pathname } = new URL(field.url);
+                files.add(pathname);
             }
-        } else {
-            // we've hit a collection, so we have to recurse
-            await writeHierarchyRecursive(file, result, `${prefix}/${documentTitle}`);
         }
+
+        if (doc.id) {
+            docs[doc.id] = doc;
+        }
+        return ids;
+    };
+    await Database.Instance.visit([id], fn);
+    return { id, docs, files };
+}
+
+export default class DownloadManager extends ApiManager {
+    protected initialize(register: Registration): void {
+        /**
+         * Let's say someone's using Dash to organize images in collections.
+         * This lets them export the hierarchy they've built to their
+         * own file system in a useful format.
+         *
+         * This handler starts with a single document id (interesting only
+         * if it's that of a collection). It traverses the database, captures
+         * the nesting of only nested images or collections, writes
+         * that to a zip file and returns it to the client for download.
+         */
+        register({
+            method: Method.GET,
+            subscription: new RouteSubscriber('imageHierarchyExport').add('docId'),
+            secureHandler: async ({ req, res }) => {
+                const id = req.params.docId;
+                const hierarchy: Hierarchy = {};
+                await buildHierarchyRecursive(id, hierarchy);
+                return BuildAndDispatchZip(res, zip => writeHierarchyRecursive(zip, hierarchy));
+            },
+        });
+
+        register({
+            method: Method.GET,
+            subscription: new RouteSubscriber('downloadId').add('docId'),
+            secureHandler: async ({ req, res }) =>
+                BuildAndDispatchZip(res, async zip => {
+                    const { id, docs, files } = await getDocs(req.params.docId);
+                    const docString = JSON.stringify({ id, docs });
+                    zip.append(docString, { name: 'doc.json' });
+                    files.forEach(val => {
+                        zip.file(publicDirectory + val, { name: val.substring(1) });
+                    });
+                }),
+        });
+
+        register({
+            method: Method.GET,
+            subscription: new RouteSubscriber('serializeDoc').add('docId'),
+            secureHandler: async ({ req, res }) => {
+                const { docs, files } = await getDocs(req.params.docId);
+                res.send({ docs, files: Array.from(files) });
+            },
+        });
     }
-}
\ No newline at end of file
+}
diff --git a/src/server/ApiManagers/GeneralGoogleManager.ts b/src/server/ApiManagers/GeneralGoogleManager.ts
index f94b77cac..12913b1ef 100644
--- a/src/server/ApiManagers/GeneralGoogleManager.ts
+++ b/src/server/ApiManagers/GeneralGoogleManager.ts
@@ -1,51 +1,49 @@
-import ApiManager, { Registration } from "./ApiManager";
-import { Method, _permission_denied } from "../RouteManager";
-import { GoogleApiServerUtils } from "../apis/google/GoogleApiServerUtils";
-import RouteSubscriber from "../RouteSubscriber";
-import { Database } from "../database";
+import ApiManager, { Registration } from './ApiManager';
+import { Method } from '../RouteManager';
+import { GoogleApiServerUtils } from '../apis/google/GoogleApiServerUtils';
+import RouteSubscriber from '../RouteSubscriber';
+import { Database } from '../database';
 
 const EndpointHandlerMap = new Map<GoogleApiServerUtils.Action, GoogleApiServerUtils.ApiRouter>([
-    ["create", (api, params) => api.create(params)],
-    ["retrieve", (api, params) => api.get(params)],
-    ["update", (api, params) => api.batchUpdate(params)],
+    ['create', (api, params) => api.create(params)],
+    ['retrieve', (api, params) => api.get(params)],
+    ['update', (api, params) => api.batchUpdate(params)],
 ]);
 
 export default class GeneralGoogleManager extends ApiManager {
-
     protected initialize(register: Registration): void {
-
         register({
             method: Method.GET,
-            subscription: "/readGoogleAccessToken",
+            subscription: '/readGoogleAccessToken',
             secureHandler: async ({ user, res }) => {
-                const { credentials } = (await GoogleApiServerUtils.retrieveCredentials(user.id));
+                const { credentials } = await GoogleApiServerUtils.retrieveCredentials(user.id);
                 if (!credentials?.access_token) {
                     return res.send(GoogleApiServerUtils.generateAuthenticationUrl());
                 }
                 return res.send(credentials);
-            }
+            },
         });
 
         register({
             method: Method.POST,
-            subscription: "/writeGoogleAccessToken",
+            subscription: '/writeGoogleAccessToken',
             secureHandler: async ({ user, req, res }) => {
                 res.send(await GoogleApiServerUtils.processNewUser(user.id, req.body.authenticationCode));
-            }
+            },
         });
 
         register({
             method: Method.GET,
-            subscription: "/revokeGoogleAccessToken",
+            subscription: '/revokeGoogleAccessToken',
             secureHandler: async ({ user, res }) => {
                 await Database.Auxiliary.GoogleAccessToken.Revoke(user.id);
                 res.send();
-            }
+            },
         });
 
         register({
             method: Method.POST,
-            subscription: new RouteSubscriber("googleDocs").add("sector", "action"),
+            subscription: new RouteSubscriber('googleDocs').add('sector', 'action'),
             secureHandler: async ({ req, res, user }) => {
                 const sector: GoogleApiServerUtils.Service = req.params.sector as GoogleApiServerUtils.Service;
                 const action: GoogleApiServerUtils.Action = req.params.action as GoogleApiServerUtils.Action;
@@ -61,8 +59,7 @@ export default class GeneralGoogleManager extends ApiManager {
                     return;
                 }
                 res.send(undefined);
-            }
+            },
         });
-
     }
-}
\ No newline at end of file
+}
diff --git a/src/server/ApiManagers/MongoStore.js b/src/server/ApiManagers/MongoStore.js
new file mode 100644
index 000000000..28515fee4
--- /dev/null
+++ b/src/server/ApiManagers/MongoStore.js
@@ -0,0 +1,414 @@
+'use strict';
+var __createBinding =
+    (this && this.__createBinding) ||
+    (Object.create
+        ? function (o, m, k, k2) {
+              if (k2 === undefined) k2 = k;
+              var desc = Object.getOwnPropertyDescriptor(m, k);
+              if (!desc || ('get' in desc ? !m.__esModule : desc.writable || desc.configurable)) {
+                  desc = {
+                      enumerable: true,
+                      get: function () {
+                          return m[k];
+                      },
+                  };
+              }
+              Object.defineProperty(o, k2, desc);
+          }
+        : function (o, m, k, k2) {
+              if (k2 === undefined) k2 = k;
+              o[k2] = m[k];
+          });
+var __setModuleDefault =
+    (this && this.__setModuleDefault) ||
+    (Object.create
+        ? function (o, v) {
+              Object.defineProperty(o, 'default', { enumerable: true, value: v });
+          }
+        : function (o, v) {
+              o['default'] = v;
+          });
+var __importStar =
+    (this && this.__importStar) ||
+    function (mod) {
+        if (mod && mod.__esModule) return mod;
+        var result = {};
+        if (mod != null) for (var k in mod) if (k !== 'default' && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
+        __setModuleDefault(result, mod);
+        return result;
+    };
+var __importDefault =
+    (this && this.__importDefault) ||
+    function (mod) {
+        return mod && mod.__esModule ? mod : { default: mod };
+    };
+Object.defineProperty(exports, '__esModule', { value: true });
+const console_1 = require('console');
+const util_1 = __importDefault(require('util'));
+const session = __importStar(require('express-session'));
+const mongodb_1 = require('mongodb');
+const debug_1 = __importDefault(require('debug'));
+const debug = (0, debug_1.default)('connect-mongo');
+// eslint-disable-next-line @typescript-eslint/no-empty-function
+const noop = () => {};
+const unit = a => a;
+function defaultSerializeFunction(session) {
+    // Copy each property of the session to a new object
+    const obj = {};
+    let prop;
+    for (prop in session) {
+        if (prop === 'cookie') {
+            // Convert the cookie instance to an object, if possible
+            // This gets rid of the duplicate object under session.cookie.data property
+            // @ts-ignore FIXME:
+            obj.cookie = session.cookie.toJSON
+                ? // @ts-ignore FIXME:
+                  session.cookie.toJSON()
+                : session.cookie;
+        } else {
+            // @ts-ignore FIXME:
+            obj[prop] = session[prop];
+        }
+    }
+    return obj;
+}
+function computeTransformFunctions(options) {
+    if (options.serialize || options.unserialize) {
+        return {
+            serialize: options.serialize || defaultSerializeFunction,
+            unserialize: options.unserialize || unit,
+        };
+    }
+    if (options.stringify === false) {
+        return {
+            serialize: defaultSerializeFunction,
+            unserialize: unit,
+        };
+    }
+    // Default case
+    return {
+        serialize: JSON.stringify,
+        unserialize: JSON.parse,
+    };
+}
+class MongoStore extends session.Store {
+    constructor({ collectionName = 'sessions', ttl = 1209600, mongoOptions = {}, autoRemove = 'native', autoRemoveInterval = 10, touchAfter = 0, stringify = true, crypto, ...required }) {
+        super();
+        this.crypto = null;
+        debug('create MongoStore instance');
+        const options = {
+            collectionName,
+            ttl,
+            mongoOptions,
+            autoRemove,
+            autoRemoveInterval,
+            touchAfter,
+            stringify,
+            crypto: {
+                ...{
+                    secret: false,
+                    algorithm: 'aes-256-gcm',
+                    hashing: 'sha512',
+                    encodeas: 'base64',
+                    key_size: 32,
+                    iv_size: 16,
+                    at_size: 16,
+                },
+                ...crypto,
+            },
+            ...required,
+        };
+        // Check params
+        (0, console_1.assert)(options.mongoUrl || options.clientPromise || options.client, 'You must provide either mongoUrl|clientPromise|client in options');
+        (0, console_1.assert)(options.createAutoRemoveIdx === null || options.createAutoRemoveIdx === undefined, 'options.createAutoRemoveIdx has been reverted to autoRemove and autoRemoveInterval');
+        (0, console_1.assert)(!options.autoRemoveInterval || options.autoRemoveInterval <= 71582, /* (Math.pow(2, 32) - 1) / (1000 * 60) */ 'autoRemoveInterval is too large. options.autoRemoveInterval is in minutes but not seconds nor mills');
+        this.transformFunctions = computeTransformFunctions(options);
+        let _clientP;
+        if (options.mongoUrl) {
+            _clientP = mongodb_1.MongoClient.connect(options.mongoUrl, options.mongoOptions);
+        } else if (options.clientPromise) {
+            _clientP = options.clientPromise;
+        } else if (options.client) {
+            _clientP = Promise.resolve(options.client);
+        } else {
+            throw new Error('Cannot init client. Please provide correct options');
+        }
+        (0, console_1.assert)(!!_clientP, 'Client is null|undefined');
+        this.clientP = _clientP;
+        this.options = options;
+        this.collectionP = _clientP.then(async con => {
+            const collection = con.db(options.dbName).collection(options.collectionName);
+            await this.setAutoRemove(collection);
+            return collection;
+        });
+        if (options.crypto.secret) {
+            this.crypto = require('kruptein')(options.crypto);
+        }
+    }
+    static create(options) {
+        return new MongoStore(options);
+    }
+    setAutoRemove(collection) {
+        const removeQuery = () => ({
+            expires: {
+                $lt: new Date(),
+            },
+        });
+        switch (this.options.autoRemove) {
+            case 'native':
+                debug('Creating MongoDB TTL index');
+                return collection.createIndex(
+                    { expires: 1 },
+                    {
+                        background: true,
+                        expireAfterSeconds: 0,
+                    }
+                );
+            case 'interval':
+                debug('create Timer to remove expired sessions');
+                this.timer = setInterval(
+                    () =>
+                        collection.deleteMany(removeQuery(), {
+                            writeConcern: {
+                                w: 0,
+                                j: false,
+                            },
+                        }),
+                    this.options.autoRemoveInterval * 1000 * 60
+                );
+                this.timer.unref();
+                return Promise.resolve();
+            case 'disabled':
+            default:
+                return Promise.resolve();
+        }
+    }
+    computeStorageId(sessionId) {
+        if (this.options.transformId && typeof this.options.transformId === 'function') {
+            return this.options.transformId(sessionId);
+        }
+        return sessionId;
+    }
+    /**
+     * promisify and bind the `this.crypto.get` function.
+     * Please check !!this.crypto === true before using this getter!
+     */
+    get cryptoGet() {
+        if (!this.crypto) {
+            throw new Error('Check this.crypto before calling this.cryptoGet!');
+        }
+        return util_1.default.promisify(this.crypto.get).bind(this.crypto);
+    }
+    /**
+     * Decrypt given session data
+     * @param session session data to be decrypt. Mutate the input session.
+     */
+    async decryptSession(session) {
+        if (this.crypto && session) {
+            const plaintext = await this.cryptoGet(this.options.crypto.secret, session.session).catch(err => {
+                throw new Error(err);
+            });
+            // @ts-ignore
+            session.session = JSON.parse(plaintext);
+        }
+    }
+    /**
+     * Get a session from the store given a session ID (sid)
+     * @param sid session ID
+     */
+    get(sid, callback) {
+        (async () => {
+            try {
+                debug(`MongoStore#get=${sid}`);
+                const collection = await this.collectionP;
+                const session = await collection.findOne({
+                    _id: this.computeStorageId(sid),
+                    $or: [{ expires: { $exists: false } }, { expires: { $gt: new Date() } }],
+                });
+                if (this.crypto && session) {
+                    await this.decryptSession(session).catch(err => callback(err));
+                }
+                const s = session && this.transformFunctions.unserialize(session.session);
+                if (this.options.touchAfter > 0 && (session === null || session === void 0 ? void 0 : session.lastModified)) {
+                    s.lastModified = session.lastModified;
+                }
+                this.emit('get', sid);
+                callback(null, s === undefined ? null : s);
+            } catch (error) {
+                callback(error);
+            }
+        })();
+    }
+    /**
+     * Upsert a session into the store given a session ID (sid) and session (session) object.
+     * @param sid session ID
+     * @param session session object
+     */
+    set(sid, session, callback = noop) {
+        (async () => {
+            var _a;
+            try {
+                debug(`MongoStore#set=${sid}`);
+                // Removing the lastModified prop from the session object before update
+                // @ts-ignore
+                if (this.options.touchAfter > 0 && (session === null || session === void 0 ? void 0 : session.lastModified)) {
+                    // @ts-ignore
+                    delete session.lastModified;
+                }
+                const s = {
+                    _id: this.computeStorageId(sid),
+                    session: this.transformFunctions.serialize(session),
+                };
+                // Expire handling
+                if ((_a = session === null || session === void 0 ? void 0 : session.cookie) === null || _a === void 0 ? void 0 : _a.expires) {
+                    s.expires = new Date(session.cookie.expires);
+                } else {
+                    // If there's no expiration date specified, it is
+                    // browser-session cookie or there is no cookie at all,
+                    // as per the connect docs.
+                    //
+                    // So we set the expiration to two-weeks from now
+                    // - as is common practice in the industry (e.g Django) -
+                    // or the default specified in the options.
+                    s.expires = new Date(Date.now() + this.options.ttl * 1000);
+                }
+                // Last modify handling
+                if (this.options.touchAfter > 0) {
+                    s.lastModified = new Date();
+                }
+                if (this.crypto) {
+                    const cryptoSet = util_1.default.promisify(this.crypto.set).bind(this.crypto);
+                    const data = await cryptoSet(this.options.crypto.secret, s.session).catch(err => {
+                        throw new Error(err);
+                    });
+                    s.session = data;
+                }
+                const collection = await this.collectionP;
+                const rawResp = await collection.updateOne(
+                    { _id: s._id },
+                    { $set: s },
+                    {
+                        upsert: true,
+                        writeConcern: this.options.writeOperationOptions,
+                    }
+                );
+                if (rawResp.upsertedCount > 0) {
+                    this.emit('create', sid);
+                } else {
+                    this.emit('update', sid);
+                }
+                this.emit('set', sid);
+            } catch (error) {
+                return callback(error);
+            }
+            return callback(null);
+        })();
+    }
+    touch(sid, session, callback = noop) {
+        (async () => {
+            var _a;
+            try {
+                debug(`MongoStore#touch=${sid}`);
+                const updateFields = {};
+                const touchAfter = this.options.touchAfter * 1000;
+                const lastModified = session.lastModified ? session.lastModified.getTime() : 0;
+                const currentDate = new Date();
+                // If the given options has a touchAfter property, check if the
+                // current timestamp - lastModified timestamp is bigger than
+                // the specified, if it's not, don't touch the session
+                if (touchAfter > 0 && lastModified > 0) {
+                    const timeElapsed = currentDate.getTime() - lastModified;
+                    if (timeElapsed < touchAfter) {
+                        debug(`Skip touching session=${sid}`);
+                        return callback(null);
+                    }
+                    updateFields.lastModified = currentDate;
+                }
+                if ((_a = session === null || session === void 0 ? void 0 : session.cookie) === null || _a === void 0 ? void 0 : _a.expires) {
+                    updateFields.expires = new Date(session.cookie.expires);
+                } else {
+                    updateFields.expires = new Date(Date.now() + this.options.ttl * 1000);
+                }
+                const collection = await this.collectionP;
+                const rawResp = await collection.updateOne({ _id: this.computeStorageId(sid) }, { $set: updateFields }, { writeConcern: this.options.writeOperationOptions });
+                if (rawResp.matchedCount === 0) {
+                    return callback(new Error('Unable to find the session to touch'));
+                } else {
+                    this.emit('touch', sid, session);
+                    return callback(null);
+                }
+            } catch (error) {
+                return callback(error);
+            }
+        })();
+    }
+    /**
+     * Get all sessions in the store as an array
+     */
+    all(callback) {
+        (async () => {
+            try {
+                debug('MongoStore#all()');
+                const collection = await this.collectionP;
+                const sessions = collection.find({
+                    $or: [{ expires: { $exists: false } }, { expires: { $gt: new Date() } }],
+                });
+                const results = [];
+                for await (const session of sessions) {
+                    if (this.crypto && session) {
+                        await this.decryptSession(session);
+                    }
+                    results.push(this.transformFunctions.unserialize(session.session));
+                }
+                this.emit('all', results);
+                callback(null, results);
+            } catch (error) {
+                callback(error);
+            }
+        })();
+    }
+    /**
+     * Destroy/delete a session from the store given a session ID (sid)
+     * @param sid session ID
+     */
+    destroy(sid, callback = noop) {
+        debug(`MongoStore#destroy=${sid}`);
+        this.collectionP
+            .then(colleciton => colleciton.deleteOne({ _id: this.computeStorageId(sid) }, { writeConcern: this.options.writeOperationOptions }))
+            .then(() => {
+                this.emit('destroy', sid);
+                callback(null);
+            })
+            .catch(err => callback(err));
+    }
+    /**
+     * Get the count of all sessions in the store
+     */
+    length(callback) {
+        debug('MongoStore#length()');
+        this.collectionP
+            .then(collection => collection.countDocuments())
+            .then(c => callback(null, c))
+            // @ts-ignore
+            .catch(err => callback(err));
+    }
+    /**
+     * Delete all sessions from the store.
+     */
+    clear(callback = noop) {
+        debug('MongoStore#clear()');
+        this.collectionP
+            .then(collection => collection.drop())
+            .then(() => callback(null))
+            .catch(err => callback(err));
+    }
+    /**
+     * Close database connection
+     */
+    close() {
+        debug('MongoStore#close()');
+        return this.clientP.then(c => c.close());
+    }
+}
+exports.default = MongoStore;
+//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiTW9uZ29TdG9yZS5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uL3NyYy9saWIvTW9uZ29TdG9yZS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7O0FBQUEscUNBQWdDO0FBQ2hDLGdEQUF1QjtBQUN2Qix5REFBMEM7QUFDMUMscUNBS2dCO0FBQ2hCLGtEQUF5QjtBQUd6QixNQUFNLEtBQUssR0FBRyxJQUFBLGVBQUssRUFBQyxlQUFlLENBQUMsQ0FBQTtBQWdFcEMsZ0VBQWdFO0FBQ2hFLE1BQU0sSUFBSSxHQUFHLEdBQUcsRUFBRSxHQUFFLENBQUMsQ0FBQTtBQUNyQixNQUFNLElBQUksR0FBbUIsQ0FBQyxDQUFDLEVBQUUsRUFBRSxDQUFDLENBQUMsQ0FBQTtBQUVyQyxTQUFTLHdCQUF3QixDQUMvQixPQUE0QjtJQUU1QixvREFBb0Q7SUFDcEQsTUFBTSxHQUFHLEdBQUcsRUFBRSxDQUFBO0lBQ2QsSUFBSSxJQUFJLENBQUE7SUFDUixLQUFLLElBQUksSUFBSSxPQUFPLEVBQUU7UUFDcEIsSUFBSSxJQUFJLEtBQUssUUFBUSxFQUFFO1lBQ3JCLHdEQUF3RDtZQUN4RCwyRUFBMkU7WUFDM0Usb0JBQW9CO1lBQ3BCLEdBQUcsQ0FBQyxNQUFNLEdBQUcsT0FBTyxDQUFDLE1BQU0sQ0FBQyxNQUFNO2dCQUNoQyxDQUFDLENBQUMsb0JBQW9CO29CQUNwQixPQUFPLENBQUMsTUFBTSxDQUFDLE1BQU0sRUFBRTtnQkFDekIsQ0FBQyxDQUFDLE9BQU8sQ0FBQyxNQUFNLENBQUE7U0FDbkI7YUFBTTtZQUNMLG9CQUFvQjtZQUNwQixHQUFHLENBQUMsSUFBSSxDQUFDLEdBQUcsT0FBTyxDQUFDLElBQUksQ0FBQyxDQUFBO1NBQzFCO0tBQ0Y7SUFFRCxPQUFPLEdBQTBCLENBQUE7QUFDbkMsQ0FBQztBQUVELFNBQVMseUJBQXlCLENBQUMsT0FBbUM7SUFDcEUsSUFBSSxPQUFPLENBQUMsU0FBUyxJQUFJLE9BQU8sQ0FBQyxXQUFXLEVBQUU7UUFDNUMsT0FBTztZQUNMLFNBQVMsRUFBRSxPQUFPLENBQUMsU0FBUyxJQUFJLHdCQUF3QjtZQUN4RCxXQUFXLEVBQUUsT0FBTyxDQUFDLFdBQVcsSUFBSSxJQUFJO1NBQ3pDLENBQUE7S0FDRjtJQUVELElBQUksT0FBTyxDQUFDLFNBQVMsS0FBSyxLQUFLLEVBQUU7UUFDL0IsT0FBTztZQUNMLFNBQVMsRUFBRSx3QkFBd0I7WUFDbkMsV0FBVyxFQUFFLElBQUk7U0FDbEIsQ0FBQTtLQUNGO0lBQ0QsZUFBZTtJQUNmLE9BQU87UUFDTCxTQUFTLEVBQUUsSUFBSSxDQUFDLFNBQVM7UUFDekIsV0FBVyxFQUFFLElBQUksQ0FBQyxLQUFLO0tBQ3hCLENBQUE7QUFDSCxDQUFDO0FBRUQsTUFBcUIsVUFBVyxTQUFRLE9BQU8sQ0FBQyxLQUFLO0lBWW5ELFlBQVksRUFDVixjQUFjLEdBQUcsVUFBVSxFQUMzQixHQUFHLEdBQUcsT0FBTyxFQUNiLFlBQVksR0FBRyxFQUFFLEVBQ2pCLFVBQVUsR0FBRyxRQUFRLEVBQ3JCLGtCQUFrQixHQUFHLEVBQUUsRUFDdkIsVUFBVSxHQUFHLENBQUMsRUFDZCxTQUFTLEdBQUcsSUFBSSxFQUNoQixNQUFNLEVBQ04sR0FBRyxRQUFRLEVBQ1M7UUFDcEIsS0FBSyxFQUFFLENBQUE7UUFyQkQsV0FBTSxHQUFvQixJQUFJLENBQUE7UUFzQnBDLEtBQUssQ0FBQyw0QkFBNEIsQ0FBQyxDQUFBO1FBQ25DLE1BQU0sT0FBTyxHQUErQjtZQUMxQyxjQUFjO1lBQ2QsR0FBRztZQUNILFlBQVk7WUFDWixVQUFVO1lBQ1Ysa0JBQWtCO1lBQ2xCLFVBQVU7WUFDVixTQUFTO1lBQ1QsTUFBTSxFQUFFO2dCQUNOLEdBQUc7b0JBQ0QsTUFBTSxFQUFFLEtBQUs7b0JBQ2IsU0FBUyxFQUFFLGFBQWE7b0JBQ3hCLE9BQU8sRUFBRSxRQUFRO29CQUNqQixRQUFRLEVBQUUsUUFBUTtvQkFDbEIsUUFBUSxFQUFFLEVBQUU7b0JBQ1osT0FBTyxFQUFFLEVBQUU7b0JBQ1gsT0FBTyxFQUFFLEVBQUU7aUJBQ1o7Z0JBQ0QsR0FBRyxNQUFNO2FBQ1Y7WUFDRCxHQUFHLFFBQVE7U0FDWixDQUFBO1FBQ0QsZUFBZTtRQUNmLElBQUEsZ0JBQU0sRUFDSixPQUFPLENBQUMsUUFBUSxJQUFJLE9BQU8sQ0FBQyxhQUFhLElBQUksT0FBTyxDQUFDLE1BQU0sRUFDM0Qsa0VBQWtFLENBQ25FLENBQUE7UUFDRCxJQUFBLGdCQUFNLEVBQ0osT0FBTyxDQUFDLG1CQUFtQixLQUFLLElBQUk7WUFDbEMsT0FBTyxDQUFDLG1CQUFtQixLQUFLLFNBQVMsRUFDM0Msb0ZBQW9GLENBQ3JGLENBQUE7UUFDRCxJQUFBLGdCQUFNLEVBQ0osQ0FBQyxPQUFPLENBQUMsa0JBQWtCLElBQUksT0FBTyxDQUFDLGtCQUFrQixJQUFJLEtBQUs7UUFDbEUseUNBQXlDLENBQUMscUdBQXFHLENBQ2hKLENBQUE7UUFDRCxJQUFJLENBQUMsa0JBQWtCLEdBQUcseUJBQXlCLENBQUMsT0FBTyxDQUFDLENBQUE7UUFDNUQsSUFBSSxRQUE4QixDQUFBO1FBQ2xDLElBQUksT0FBTyxDQUFDLFFBQVEsRUFBRTtZQUNwQixRQUFRLEdBQUcscUJBQVcsQ0FBQyxPQUFPLENBQUMsT0FBTyxDQUFDLFFBQVEsRUFBRSxPQUFPLENBQUMsWUFBWSxDQUFDLENBQUE7U0FDdkU7YUFBTSxJQUFJLE9BQU8sQ0FBQyxhQUFhLEVBQUU7WUFDaEMsUUFBUSxHQUFHLE9BQU8sQ0FBQyxhQUFhLENBQUE7U0FDakM7YUFBTSxJQUFJLE9BQU8sQ0FBQyxNQUFNLEVBQUU7WUFDekIsUUFBUSxHQUFHLE9BQU8sQ0FBQyxPQUFPLENBQUMsT0FBTyxDQUFDLE1BQU0sQ0FBQyxDQUFBO1NBQzNDO2FBQU07WUFDTCxNQUFNLElBQUksS0FBSyxDQUFDLG9EQUFvRCxDQUFDLENBQUE7U0FDdEU7UUFDRCxJQUFBLGdCQUFNLEVBQUMsQ0FBQyxDQUFDLFFBQVEsRUFBRSwwQkFBMEIsQ0FBQyxDQUFBO1FBQzlDLElBQUksQ0FBQyxPQUFPLEdBQUcsUUFBUSxDQUFBO1FBQ3ZCLElBQUksQ0FBQyxPQUFPLEdBQUcsT0FBTyxDQUFBO1FBQ3RCLElBQUksQ0FBQyxXQUFXLEdBQUcsUUFBUSxDQUFDLElBQUksQ0FBQyxLQUFLLEVBQUUsR0FBRyxFQUFFLEVBQUU7WUFDN0MsTUFBTSxVQUFVLEdBQUcsR0FBRztpQkFDbkIsRUFBRSxDQUFDLE9BQU8sQ0FBQyxNQUFNLENBQUM7aUJBQ2xCLFVBQVUsQ0FBc0IsT0FBTyxDQUFDLGNBQWMsQ0FBQyxDQUFBO1lBQzFELE1BQU0sSUFBSSxDQUFDLGFBQWEsQ0FBQyxVQUFVLENBQUMsQ0FBQTtZQUNwQyxPQUFPLFVBQVUsQ0FBQTtRQUNuQixDQUFDLENBQUMsQ0FBQTtRQUNGLElBQUksT0FBTyxDQUFDLE1BQU0sQ0FBQyxNQUFNLEVBQUU7WUFDekIsSUFBSSxDQUFDLE1BQU0sR0FBRyxPQUFPLENBQUMsVUFBVSxDQUFDLENBQUMsT0FBTyxDQUFDLE1BQU0sQ0FBQyxDQUFBO1NBQ2xEO0lBQ0gsQ0FBQztJQUVELE1BQU0sQ0FBQyxNQUFNLENBQUMsT0FBNEI7UUFDeEMsT0FBTyxJQUFJLFVBQVUsQ0FBQyxPQUFPLENBQUMsQ0FBQTtJQUNoQyxDQUFDO0lBRU8sYUFBYSxDQUNuQixVQUEyQztRQUUzQyxNQUFNLFdBQVcsR0FBRyxHQUFHLEVBQUUsQ0FBQyxDQUFDO1lBQ3pCLE9BQU8sRUFBRTtnQkFDUCxHQUFHLEVBQUUsSUFBSSxJQUFJLEVBQUU7YUFDaEI7U0FDRixDQUFDLENBQUE7UUFDRixRQUFRLElBQUksQ0FBQyxPQUFPLENBQUMsVUFBVSxFQUFFO1lBQy9CLEtBQUssUUFBUTtnQkFDWCxLQUFLLENBQUMsNEJBQTRCLENBQUMsQ0FBQTtnQkFDbkMsT0FBTyxVQUFVLENBQUMsV0FBVyxDQUMzQixFQUFFLE9BQU8sRUFBRSxDQUFDLEVBQUUsRUFDZDtvQkFDRSxVQUFVLEVBQUUsSUFBSTtvQkFDaEIsa0JBQWtCLEVBQUUsQ0FBQztpQkFDdEIsQ0FDRixDQUFBO1lBQ0gsS0FBSyxVQUFVO2dCQUNiLEtBQUssQ0FBQyx5Q0FBeUMsQ0FBQyxDQUFBO2dCQUNoRCxJQUFJLENBQUMsS0FBSyxHQUFHLFdBQVcsQ0FDdEIsR0FBRyxFQUFFLENBQ0gsVUFBVSxDQUFDLFVBQVUsQ0FBQyxXQUFXLEVBQUUsRUFBRTtvQkFDbkMsWUFBWSxFQUFFO3dCQUNaLENBQUMsRUFBRSxDQUFDO3dCQUNKLENBQUMsRUFBRSxLQUFLO3FCQUNUO2lCQUNGLENBQUMsRUFDSixJQUFJLENBQUMsT0FBTyxDQUFDLGtCQUFrQixHQUFHLElBQUksR0FBRyxFQUFFLENBQzVDLENBQUE7Z0JBQ0QsSUFBSSxDQUFDLEtBQUssQ0FBQyxLQUFLLEVBQUUsQ0FBQTtnQkFDbEIsT0FBTyxPQUFPLENBQUMsT0FBTyxFQUFFLENBQUE7WUFDMUIsS0FBSyxVQUFVLENBQUM7WUFDaEI7Z0JBQ0UsT0FBTyxPQUFPLENBQUMsT0FBTyxFQUFFLENBQUE7U0FDM0I7SUFDSCxDQUFDO0lBRU8sZ0JBQWdCLENBQUMsU0FBaUI7UUFDeEMsSUFDRSxJQUFJLENBQUMsT0FBTyxDQUFDLFdBQVc7WUFDeEIsT0FBTyxJQUFJLENBQUMsT0FBTyxDQUFDLFdBQVcsS0FBSyxVQUFVLEVBQzlDO1lBQ0EsT0FBTyxJQUFJLENBQUMsT0FBTyxDQUFDLFdBQVcsQ0FBQyxTQUFTLENBQUMsQ0FBQTtTQUMzQztRQUNELE9BQU8sU0FBUyxDQUFBO0lBQ2xCLENBQUM7SUFFRDs7O09BR0c7SUFDSCxJQUFZLFNBQVM7UUFDbkIsSUFBSSxDQUFDLElBQUksQ0FBQyxNQUFNLEVBQUU7WUFDaEIsTUFBTSxJQUFJLEtBQUssQ0FBQyxrREFBa0QsQ0FBQyxDQUFBO1NBQ3BFO1FBQ0QsT0FBTyxjQUFJLENBQUMsU0FBUyxDQUFDLElBQUksQ0FBQyxNQUFNLENBQUMsR0FBRyxDQUFDLENBQUMsSUFBSSxDQUFDLElBQUksQ0FBQyxNQUFNLENBQUMsQ0FBQTtJQUMxRCxDQUFDO0lBRUQ7OztPQUdHO0lBQ0ssS0FBSyxDQUFDLGNBQWMsQ0FDMUIsT0FBK0M7UUFFL0MsSUFBSSxJQUFJLENBQUMsTUFBTSxJQUFJLE9BQU8sRUFBRTtZQUMxQixNQUFNLFNBQVMsR0FBRyxNQUFNLElBQUksQ0FBQyxTQUFTLENBQ3BDLElBQUksQ0FBQyxPQUFPLENBQUMsTUFBTSxDQUFDLE1BQWdCLEVBQ3BDLE9BQU8sQ0FBQyxPQUFPLENBQ2hCLENBQUMsS0FBSyxDQUFDLENBQUMsR0FBRyxFQUFFLEVBQUU7Z0JBQ2QsTUFBTSxJQUFJLEtBQUssQ0FBQyxHQUFHLENBQUMsQ0FBQTtZQUN0QixDQUFDLENBQUMsQ0FBQTtZQUNGLGFBQWE7WUFDYixPQUFPLENBQUMsT0FBTyxHQUFHLElBQUksQ0FBQyxLQUFLLENBQUMsU0FBUyxDQUFDLENBQUE7U0FDeEM7SUFDSCxDQUFDO0lBRUQ7OztPQUdHO0lBQ0gsR0FBRyxDQUNELEdBQVcsRUFDWCxRQUFrRTtRQUVsRSxDQUFDO1FBQUEsQ0FBQyxLQUFLLElBQUksRUFBRTtZQUNYLElBQUk7Z0JBQ0YsS0FBSyxDQUFDLGtCQUFrQixHQUFHLEVBQUUsQ0FBQyxDQUFBO2dCQUM5QixNQUFNLFVBQVUsR0FBRyxNQUFNLElBQUksQ0FBQyxXQUFXLENBQUE7Z0JBQ3pDLE1BQU0sT0FBTyxHQUFHLE1BQU0sVUFBVSxDQUFDLE9BQU8sQ0FBQztvQkFDdkMsR0FBRyxFQUFFLElBQUksQ0FBQyxnQkFBZ0IsQ0FBQyxHQUFHLENBQUM7b0JBQy9CLEdBQUcsRUFBRTt3QkFDSCxFQUFFLE9BQU8sRUFBRSxFQUFFLE9BQU8sRUFBRSxLQUFLLEVBQUUsRUFBRTt3QkFDL0IsRUFBRSxPQUFPLEVBQUUsRUFBRSxHQUFHLEVBQUUsSUFBSSxJQUFJLEVBQUUsRUFBRSxFQUFFO3FCQUNqQztpQkFDRixDQUFDLENBQUE7Z0JBQ0YsSUFBSSxJQUFJLENBQUMsTUFBTSxJQUFJLE9BQU8sRUFBRTtvQkFDMUIsTUFBTSxJQUFJLENBQUMsY0FBYyxDQUN2QixPQUF5QyxDQUMxQyxDQUFDLEtBQUssQ0FBQyxDQUFDLEdBQUcsRUFBRSxFQUFFLENBQUMsUUFBUSxDQUFDLEdBQUcsQ0FBQyxDQUFDLENBQUE7aUJBQ2hDO2dCQUNELE1BQU0sQ0FBQyxHQUNMLE9BQU8sSUFBSSxJQUFJLENBQUMsa0JBQWtCLENBQUMsV0FBVyxDQUFDLE9BQU8sQ0FBQyxPQUFPLENBQUMsQ0FBQTtnQkFDakUsSUFBSSxJQUFJLENBQUMsT0FBTyxDQUFDLFVBQVUsR0FBRyxDQUFDLEtBQUksT0FBTyxhQUFQLE9BQU8sdUJBQVAsT0FBTyxDQUFFLFlBQVksQ0FBQSxFQUFFO29CQUN4RCxDQUFDLENBQUMsWUFBWSxHQUFHLE9BQU8sQ0FBQyxZQUFZLENBQUE7aUJBQ3RDO2dCQUNELElBQUksQ0FBQyxJQUFJLENBQUMsS0FBSyxFQUFFLEdBQUcsQ0FBQyxDQUFBO2dCQUNyQixRQUFRLENBQUMsSUFBSSxFQUFFLENBQUMsS0FBSyxTQUFTLENBQUMsQ0FBQyxDQUFDLElBQUksQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUE7YUFDM0M7WUFBQyxPQUFPLEtBQUssRUFBRTtnQkFDZCxRQUFRLENBQUMsS0FBSyxDQUFDLENBQUE7YUFDaEI7UUFDSCxDQUFDLENBQUMsRUFBRSxDQUFBO0lBQ04sQ0FBQztJQUVEOzs7O09BSUc7SUFDSCxHQUFHLENBQ0QsR0FBVyxFQUNYLE9BQTRCLEVBQzVCLFdBQStCLElBQUk7UUFFbkMsQ0FBQztRQUFBLENBQUMsS0FBSyxJQUFJLEVBQUU7O1lBQ1gsSUFBSTtnQkFDRixLQUFLLENBQUMsa0JBQWtCLEdBQUcsRUFBRSxDQUFDLENBQUE7Z0JBQzlCLHVFQUF1RTtnQkFDdkUsYUFBYTtnQkFDYixJQUFJLElBQUksQ0FBQyxPQUFPLENBQUMsVUFBVSxHQUFHLENBQUMsS0FBSSxPQUFPLGFBQVAsT0FBTyx1QkFBUCxPQUFPLENBQUUsWUFBWSxDQUFBLEVBQUU7b0JBQ3hELGFBQWE7b0JBQ2IsT0FBTyxPQUFPLENBQUMsWUFBWSxDQUFBO2lCQUM1QjtnQkFDRCxNQUFNLENBQUMsR0FBd0I7b0JBQzdCLEdBQUcsRUFBRSxJQUFJLENBQUMsZ0JBQWdCLENBQUMsR0FBRyxDQUFDO29CQUMvQixPQUFPLEVBQUUsSUFBSSxDQUFDLGtCQUFrQixDQUFDLFNBQVMsQ0FBQyxPQUFPLENBQUM7aUJBQ3BELENBQUE7Z0JBQ0Qsa0JBQWtCO2dCQUNsQixJQUFJLE1BQUEsT0FBTyxhQUFQLE9BQU8sdUJBQVAsT0FBTyxDQUFFLE1BQU0sMENBQUUsT0FBTyxFQUFFO29CQUM1QixDQUFDLENBQUMsT0FBTyxHQUFHLElBQUksSUFBSSxDQUFDLE9BQU8sQ0FBQyxNQUFNLENBQUMsT0FBTyxDQUFDLENBQUE7aUJBQzdDO3FCQUFNO29CQUNMLGlEQUFpRDtvQkFDakQsdURBQXVEO29CQUN2RCwyQkFBMkI7b0JBQzNCLEVBQUU7b0JBQ0YsaURBQWlEO29CQUNqRCx5REFBeUQ7b0JBQ3pELDJDQUEyQztvQkFDM0MsQ0FBQyxDQUFDLE9BQU8sR0FBRyxJQUFJLElBQUksQ0FBQyxJQUFJLENBQUMsR0FBRyxFQUFFLEdBQUcsSUFBSSxDQUFDLE9BQU8sQ0FBQyxHQUFHLEdBQUcsSUFBSSxDQUFDLENBQUE7aUJBQzNEO2dCQUNELHVCQUF1QjtnQkFDdkIsSUFBSSxJQUFJLENBQUMsT0FBTyxDQUFDLFVBQVUsR0FBRyxDQUFDLEVBQUU7b0JBQy9CLENBQUMsQ0FBQyxZQUFZLEdBQUcsSUFBSSxJQUFJLEVBQUUsQ0FBQTtpQkFDNUI7Z0JBQ0QsSUFBSSxJQUFJLENBQUMsTUFBTSxFQUFFO29CQUNmLE1BQU0sU0FBUyxHQUFHLGNBQUksQ0FBQyxTQUFTLENBQUMsSUFBSSxDQUFDLE1BQU0sQ0FBQyxHQUFHLENBQUMsQ0FBQyxJQUFJLENBQUMsSUFBSSxDQUFDLE1BQU0sQ0FBQyxDQUFBO29CQUNuRSxNQUFNLElBQUksR0FBRyxNQUFNLFNBQVMsQ0FDMUIsSUFBSSxDQUFDLE9BQU8sQ0FBQyxNQUFNLENBQUMsTUFBZ0IsRUFDcEMsQ0FBQyxDQUFDLE9BQU8sQ0FDVixDQUFDLEtBQUssQ0FBQyxDQUFDLEdBQUcsRUFBRSxFQUFFO3dCQUNkLE1BQU0sSUFBSSxLQUFLLENBQUMsR0FBRyxDQUFDLENBQUE7b0JBQ3RCLENBQUMsQ0FBQyxDQUFBO29CQUNGLENBQUMsQ0FBQyxPQUFPLEdBQUcsSUFBc0MsQ0FBQTtpQkFDbkQ7Z0JBQ0QsTUFBTSxVQUFVLEdBQUcsTUFBTSxJQUFJLENBQUMsV0FBVyxDQUFBO2dCQUN6QyxNQUFNLE9BQU8sR0FBRyxNQUFNLFVBQVUsQ0FBQyxTQUFTLENBQ3hDLEVBQUUsR0FBRyxFQUFFLENBQUMsQ0FBQyxHQUFHLEVBQUUsRUFDZCxFQUFFLElBQUksRUFBRSxDQUFDLEVBQUUsRUFDWDtvQkFDRSxNQUFNLEVBQUUsSUFBSTtvQkFDWixZQUFZLEVBQUUsSUFBSSxDQUFDLE9BQU8sQ0FBQyxxQkFBcUI7aUJBQ2pELENBQ0YsQ0FBQTtnQkFDRCxJQUFJLE9BQU8sQ0FBQyxhQUFhLEdBQUcsQ0FBQyxFQUFFO29CQUM3QixJQUFJLENBQUMsSUFBSSxDQUFDLFFBQVEsRUFBRSxHQUFHLENBQUMsQ0FBQTtpQkFDekI7cUJBQU07b0JBQ0wsSUFBSSxDQUFDLElBQUksQ0FBQyxRQUFRLEVBQUUsR0FBRyxDQUFDLENBQUE7aUJBQ3pCO2dCQUNELElBQUksQ0FBQyxJQUFJLENBQUMsS0FBSyxFQUFFLEdBQUcsQ0FBQyxDQUFBO2FBQ3RCO1lBQUMsT0FBTyxLQUFLLEVBQUU7Z0JBQ2QsT0FBTyxRQUFRLENBQUMsS0FBSyxDQUFDLENBQUE7YUFDdkI7WUFDRCxPQUFPLFFBQVEsQ0FBQyxJQUFJLENBQUMsQ0FBQTtRQUN2QixDQUFDLENBQUMsRUFBRSxDQUFBO0lBQ04sQ0FBQztJQUVELEtBQUssQ0FDSCxHQUFXLEVBQ1gsT0FBc0QsRUFDdEQsV0FBK0IsSUFBSTtRQUVuQyxDQUFDO1FBQUEsQ0FBQyxLQUFLLElBQUksRUFBRTs7WUFDWCxJQUFJO2dCQUNGLEtBQUssQ0FBQyxvQkFBb0IsR0FBRyxFQUFFLENBQUMsQ0FBQTtnQkFDaEMsTUFBTSxZQUFZLEdBSWQsRUFBRSxDQUFBO2dCQUNOLE1BQU0sVUFBVSxHQUFHLElBQUksQ0FBQyxPQUFPLENBQUMsVUFBVSxHQUFHLElBQUksQ0FBQTtnQkFDakQsTUFBTSxZQUFZLEdBQUcsT0FBTyxDQUFDLFlBQVk7b0JBQ3ZDLENBQUMsQ0FBQyxPQUFPLENBQUMsWUFBWSxDQUFDLE9BQU8sRUFBRTtvQkFDaEMsQ0FBQyxDQUFDLENBQUMsQ0FBQTtnQkFDTCxNQUFNLFdBQVcsR0FBRyxJQUFJLElBQUksRUFBRSxDQUFBO2dCQUU5QiwrREFBK0Q7Z0JBQy9ELDREQUE0RDtnQkFDNUQsc0RBQXNEO2dCQUN0RCxJQUFJLFVBQVUsR0FBRyxDQUFDLElBQUksWUFBWSxHQUFHLENBQUMsRUFBRTtvQkFDdEMsTUFBTSxXQUFXLEdBQUcsV0FBVyxDQUFDLE9BQU8sRUFBRSxHQUFHLFlBQVksQ0FBQTtvQkFDeEQsSUFBSSxXQUFXLEdBQUcsVUFBVSxFQUFFO3dCQUM1QixLQUFLLENBQUMseUJBQXlCLEdBQUcsRUFBRSxDQUFDLENBQUE7d0JBQ3JDLE9BQU8sUUFBUSxDQUFDLElBQUksQ0FBQyxDQUFBO3FCQUN0QjtvQkFDRCxZQUFZLENBQUMsWUFBWSxHQUFHLFdBQVcsQ0FBQTtpQkFDeEM7Z0JBRUQsSUFBSSxNQUFBLE9BQU8sYUFBUCxPQUFPLHVCQUFQLE9BQU8sQ0FBRSxNQUFNLDBDQUFFLE9BQU8sRUFBRTtvQkFDNUIsWUFBWSxDQUFDLE9BQU8sR0FBRyxJQUFJLElBQUksQ0FBQyxPQUFPLENBQUMsTUFBTSxDQUFDLE9BQU8sQ0FBQyxDQUFBO2lCQUN4RDtxQkFBTTtvQkFDTCxZQUFZLENBQUMsT0FBTyxHQUFHLElBQUksSUFBSSxDQUFDLElBQUksQ0FBQyxHQUFHLEVBQUUsR0FBRyxJQUFJLENBQUMsT0FBTyxDQUFDLEdBQUcsR0FBRyxJQUFJLENBQUMsQ0FBQTtpQkFDdEU7Z0JBQ0QsTUFBTSxVQUFVLEdBQUcsTUFBTSxJQUFJLENBQUMsV0FBVyxDQUFBO2dCQUN6QyxNQUFNLE9BQU8sR0FBRyxNQUFNLFVBQVUsQ0FBQyxTQUFTLENBQ3hDLEVBQUUsR0FBRyxFQUFFLElBQUksQ0FBQyxnQkFBZ0IsQ0FBQyxHQUFHLENBQUMsRUFBRSxFQUNuQyxFQUFFLElBQUksRUFBRSxZQUFZLEVBQUUsRUFDdEIsRUFBRSxZQUFZLEVBQUUsSUFBSSxDQUFDLE9BQU8sQ0FBQyxxQkFBcUIsRUFBRSxDQUNyRCxDQUFBO2dCQUNELElBQUksT0FBTyxDQUFDLFlBQVksS0FBSyxDQUFDLEVBQUU7b0JBQzlCLE9BQU8sUUFBUSxDQUFDLElBQUksS0FBSyxDQUFDLHFDQUFxQyxDQUFDLENBQUMsQ0FBQTtpQkFDbEU7cUJBQU07b0JBQ0wsSUFBSSxDQUFDLElBQUksQ0FBQyxPQUFPLEVBQUUsR0FBRyxFQUFFLE9BQU8sQ0FBQyxDQUFBO29CQUNoQyxPQUFPLFFBQVEsQ0FBQyxJQUFJLENBQUMsQ0FBQTtpQkFDdEI7YUFDRjtZQUFDLE9BQU8sS0FBSyxFQUFFO2dCQUNkLE9BQU8sUUFBUSxDQUFDLEtBQUssQ0FBQyxDQUFBO2FBQ3ZCO1FBQ0gsQ0FBQyxDQUFDLEVBQUUsQ0FBQTtJQUNOLENBQUM7SUFFRDs7T0FFRztJQUNILEdBQUcsQ0FDRCxRQU1TO1FBRVQsQ0FBQztRQUFBLENBQUMsS0FBSyxJQUFJLEVBQUU7WUFDWCxJQUFJO2dCQUNGLEtBQUssQ0FBQyxrQkFBa0IsQ0FBQyxDQUFBO2dCQUN6QixNQUFNLFVBQVUsR0FBRyxNQUFNLElBQUksQ0FBQyxXQUFXLENBQUE7Z0JBQ3pDLE1BQU0sUUFBUSxHQUFHLFVBQVUsQ0FBQyxJQUFJLENBQUM7b0JBQy9CLEdBQUcsRUFBRTt3QkFDSCxFQUFFLE9BQU8sRUFBRSxFQUFFLE9BQU8sRUFBRSxLQUFLLEVBQUUsRUFBRTt3QkFDL0IsRUFBRSxPQUFPLEVBQUUsRUFBRSxHQUFHLEVBQUUsSUFBSSxJQUFJLEVBQUUsRUFBRSxFQUFFO3FCQUNqQztpQkFDRixDQUFDLENBQUE7Z0JBQ0YsTUFBTSxPQUFPLEdBQTBCLEVBQUUsQ0FBQTtnQkFDekMsSUFBSSxLQUFLLEVBQUUsTUFBTSxPQUFPLElBQUksUUFBUSxFQUFFO29CQUNwQyxJQUFJLElBQUksQ0FBQyxNQUFNLElBQUksT0FBTyxFQUFFO3dCQUMxQixNQUFNLElBQUksQ0FBQyxjQUFjLENBQUMsT0FBeUMsQ0FBQyxDQUFBO3FCQUNyRTtvQkFDRCxPQUFPLENBQUMsSUFBSSxDQUFDLElBQUksQ0FBQyxrQkFBa0IsQ0FBQyxXQUFXLENBQUMsT0FBTyxDQUFDLE9BQU8sQ0FBQyxDQUFDLENBQUE7aUJBQ25FO2dCQUNELElBQUksQ0FBQyxJQUFJLENBQUMsS0FBSyxFQUFFLE9BQU8sQ0FBQyxDQUFBO2dCQUN6QixRQUFRLENBQUMsSUFBSSxFQUFFLE9BQU8sQ0FBQyxDQUFBO2FBQ3hCO1lBQUMsT0FBTyxLQUFLLEVBQUU7Z0JBQ2QsUUFBUSxDQUFDLEtBQUssQ0FBQyxDQUFBO2FBQ2hCO1FBQ0gsQ0FBQyxDQUFDLEVBQUUsQ0FBQTtJQUNOLENBQUM7SUFFRDs7O09BR0c7SUFDSCxPQUFPLENBQUMsR0FBVyxFQUFFLFdBQStCLElBQUk7UUFDdEQsS0FBSyxDQUFDLHNCQUFzQixHQUFHLEVBQUUsQ0FBQyxDQUFBO1FBQ2xDLElBQUksQ0FBQyxXQUFXO2FBQ2IsSUFBSSxDQUFDLENBQUMsVUFBVSxFQUFFLEVBQUUsQ0FDbkIsVUFBVSxDQUFDLFNBQVMsQ0FDbEIsRUFBRSxHQUFHLEVBQUUsSUFBSSxDQUFDLGdCQUFnQixDQUFDLEdBQUcsQ0FBQyxFQUFFLEVBQ25DLEVBQUUsWUFBWSxFQUFFLElBQUksQ0FBQyxPQUFPLENBQUMscUJBQXFCLEVBQUUsQ0FDckQsQ0FDRjthQUNBLElBQUksQ0FBQyxHQUFHLEVBQUU7WUFDVCxJQUFJLENBQUMsSUFBSSxDQUFDLFNBQVMsRUFBRSxHQUFHLENBQUMsQ0FBQTtZQUN6QixRQUFRLENBQUMsSUFBSSxDQUFDLENBQUE7UUFDaEIsQ0FBQyxDQUFDO2FBQ0QsS0FBSyxDQUFDLENBQUMsR0FBRyxFQUFFLEVBQUUsQ0FBQyxRQUFRLENBQUMsR0FBRyxDQUFDLENBQUMsQ0FBQTtJQUNsQyxDQUFDO0lBRUQ7O09BRUc7SUFDSCxNQUFNLENBQUMsUUFBNEM7UUFDakQsS0FBSyxDQUFDLHFCQUFxQixDQUFDLENBQUE7UUFDNUIsSUFBSSxDQUFDLFdBQVc7YUFDYixJQUFJLENBQUMsQ0FBQyxVQUFVLEVBQUUsRUFBRSxDQUFDLFVBQVUsQ0FBQyxjQUFjLEVBQUUsQ0FBQzthQUNqRCxJQUFJLENBQUMsQ0FBQyxDQUFDLEVBQUUsRUFBRSxDQUFDLFFBQVEsQ0FBQyxJQUFJLEVBQUUsQ0FBQyxDQUFDLENBQUM7WUFDL0IsYUFBYTthQUNaLEtBQUssQ0FBQyxDQUFDLEdBQUcsRUFBRSxFQUFFLENBQUMsUUFBUSxDQUFDLEdBQUcsQ0FBQyxDQUFDLENBQUE7SUFDbEMsQ0FBQztJQUVEOztPQUVHO0lBQ0gsS0FBSyxDQUFDLFdBQStCLElBQUk7UUFDdkMsS0FBSyxDQUFDLG9CQUFvQixDQUFDLENBQUE7UUFDM0IsSUFBSSxDQUFDLFdBQVc7YUFDYixJQUFJLENBQUMsQ0FBQyxVQUFVLEVBQUUsRUFBRSxDQUFDLFVBQVUsQ0FBQyxJQUFJLEVBQUUsQ0FBQzthQUN2QyxJQUFJLENBQUMsR0FBRyxFQUFFLENBQUMsUUFBUSxDQUFDLElBQUksQ0FBQyxDQUFDO2FBQzFCLEtBQUssQ0FBQyxDQUFDLEdBQUcsRUFBRSxFQUFFLENBQUMsUUFBUSxDQUFDLEdBQUcsQ0FBQyxDQUFDLENBQUE7SUFDbEMsQ0FBQztJQUVEOztPQUVHO0lBQ0gsS0FBSztRQUNILEtBQUssQ0FBQyxvQkFBb0IsQ0FBQyxDQUFBO1FBQzNCLE9BQU8sSUFBSSxDQUFDLE9BQU8sQ0FBQyxJQUFJLENBQUMsQ0FBQyxDQUFDLEVBQUUsRUFBRSxDQUFDLENBQUMsQ0FBQyxLQUFLLEVBQUUsQ0FBQyxDQUFBO0lBQzVDLENBQUM7Q0FDRjtBQW5hRCw2QkFtYUMifQ==
diff --git a/src/server/ApiManagers/SearchManager.ts b/src/server/ApiManagers/SearchManager.ts
index 92c10975f..1b1db5809 100644
--- a/src/server/ApiManagers/SearchManager.ts
+++ b/src/server/ApiManagers/SearchManager.ts
@@ -1,6 +1,6 @@
 import { exec } from 'child_process';
 import { cyan, green, red, yellow } from 'colors';
-import { log_execution } from '../ActionUtilities';
+import { logExecution } from '../ActionUtilities';
 import { Method } from '../RouteManager';
 import RouteSubscriber from '../RouteSubscriber';
 import { Search } from '../Search';
@@ -66,13 +66,13 @@ export namespace SolrManager {
 
     export async function update() {
         console.log(green('Beginning update...'));
-        await log_execution<void>({
+        await logExecution<void>({
             startMessage: 'Clearing existing Solr information...',
             endMessage: 'Solr information successfully cleared',
             action: Search.clear,
             color: cyan,
         });
-        const cursor = await log_execution({
+        const cursor = await logExecution({
             startMessage: 'Connecting to and querying for all documents from database...',
             endMessage: ({ result, error }) => {
                 const success = error === null && result !== undefined;
@@ -118,7 +118,7 @@ export namespace SolrManager {
             }
         }
         await cursor?.forEach(updateDoc);
-        const result = await log_execution({
+        const result = await logExecution({
             startMessage: `Dispatching updates for ${updates.length} documents`,
             endMessage: 'Dispatched updates complete',
             action: () => Search.updateDocuments(updates),
diff --git a/src/server/ApiManagers/SessionManager.ts b/src/server/ApiManagers/SessionManager.ts
index e37f8c6db..c3139896f 100644
--- a/src/server/ApiManagers/SessionManager.ts
+++ b/src/server/ApiManagers/SessionManager.ts
@@ -1,67 +1,66 @@
-import ApiManager, { Registration } from "./ApiManager";
-import { Method, _permission_denied, AuthorizedCore, SecureHandler } from "../RouteManager";
-import RouteSubscriber from "../RouteSubscriber";
-import { sessionAgent } from "..";
-import { DashSessionAgent } from "../DashSession/DashSessionAgent";
+import ApiManager, { Registration } from './ApiManager';
+import { Method, _permissionDenied, AuthorizedCore, SecureHandler } from '../RouteManager';
+import RouteSubscriber from '../RouteSubscriber';
+import { sessionAgent } from '..';
+import { DashSessionAgent } from '../DashSession/DashSessionAgent';
 
-const permissionError = "You are not authorized!";
+const permissionError = 'You are not authorized!';
 
 export default class SessionManager extends ApiManager {
-
-    private secureSubscriber = (root: string, ...params: string[]) => new RouteSubscriber(root).add("session_key", ...params);
+    private secureSubscriber = (root: string, ...params: string[]) => new RouteSubscriber(root).add('session_key', ...params);
 
     private authorizedAction = (handler: SecureHandler) => {
         return (core: AuthorizedCore) => {
-            const { req: { params }, res } = core;
+            const {
+                req: { params },
+                res,
+            } = core;
             if (!process.env.MONITORED) {
-                return res.send("This command only makes sense in the context of a monitored session.");
+                return res.send('This command only makes sense in the context of a monitored session.');
             }
             if (params.session_key !== process.env.session_key) {
-                return _permission_denied(res, permissionError);
+                return _permissionDenied(res, permissionError);
             }
             return handler(core);
         };
-    }
+    };
 
     protected initialize(register: Registration): void {
-
         register({
             method: Method.GET,
-            subscription: this.secureSubscriber("debug", "to?"),
+            subscription: this.secureSubscriber('debug', 'to?'),
             secureHandler: this.authorizedAction(async ({ req: { params }, res }) => {
                 const to = params.to || DashSessionAgent.notificationRecipient;
-                const { error } = await sessionAgent.serverWorker.emit("debug", { to });
+                const { error } = await sessionAgent.serverWorker.emit('debug', { to });
                 res.send(error ? error.message : `Your request was successful: the server captured and compressed (but did not save) a new back up. It was sent to ${to}.`);
-            })
+            }),
         });
 
         register({
             method: Method.GET,
-            subscription: this.secureSubscriber("backup"),
+            subscription: this.secureSubscriber('backup'),
             secureHandler: this.authorizedAction(async ({ res }) => {
-                const { error } = await sessionAgent.serverWorker.emit("backup");
-                res.send(error ? error.message : "Your request was successful: the server successfully created a new back up.");
-            })
+                const { error } = await sessionAgent.serverWorker.emit('backup');
+                res.send(error ? error.message : 'Your request was successful: the server successfully created a new back up.');
+            }),
         });
 
         register({
             method: Method.GET,
-            subscription: this.secureSubscriber("kill"),
+            subscription: this.secureSubscriber('kill'),
             secureHandler: this.authorizedAction(({ res }) => {
-                res.send("Your request was successful: the server and its session have been killed.");
-                sessionAgent.killSession("an authorized user has manually ended the server session via the /kill route");
-            })
+                res.send('Your request was successful: the server and its session have been killed.');
+                sessionAgent.killSession('an authorized user has manually ended the server session via the /kill route');
+            }),
         });
 
         register({
             method: Method.GET,
-            subscription: this.secureSubscriber("deleteSession"),
+            subscription: this.secureSubscriber('deleteSession'),
             secureHandler: this.authorizedAction(async ({ res }) => {
-                const { error } = await sessionAgent.serverWorker.emit("delete");
-                res.send(error ? error.message : "Your request was successful: the server successfully deleted the database. Return to /home.");
-            })
+                const { error } = await sessionAgent.serverWorker.emit('delete');
+                res.send(error ? error.message : 'Your request was successful: the server successfully deleted the database. Return to /home.');
+            }),
         });
-
     }
-
-}
\ No newline at end of file
+}
diff --git a/src/server/ApiManagers/UploadManager.ts b/src/server/ApiManagers/UploadManager.ts
index 2306b6589..1a759f04d 100644
--- a/src/server/ApiManagers/UploadManager.ts
+++ b/src/server/ApiManagers/UploadManager.ts
@@ -1,50 +1,27 @@
+import * as AdmZip from 'adm-zip';
 import * as formidable from 'formidable';
-import { createReadStream, createWriteStream, unlink, writeFile } from 'fs';
-import * as path from 'path';
+import * as fs from 'fs';
+import { createReadStream, createWriteStream, unlink } from 'fs';
+import * as imageDataUri from 'image-data-uri';
 import Jimp from 'jimp';
-import { filesDirectory, publicDirectory } from '..';
+import * as path from 'path';
+import * as uuid from 'uuid';
 import { retrocycle } from '../../decycler/decycler';
+import { DashVersion } from '../../fields/DocSymbols';
 import { DashUploadUtils, InjectSize, SizeSuffix } from '../DashUploadUtils';
-import { Database } from '../database';
 import { Method, _success } from '../RouteManager';
-import RouteSubscriber from '../RouteSubscriber';
 import { AcceptableMedia, Upload } from '../SharedMediaTypes';
+import { clientPathToFile, Directory, pathToDirectory, publicDirectory, serverPathToFile } from '../SocketData';
+import { Database } from '../database';
 import ApiManager, { Registration } from './ApiManager';
 import { SolrManager } from './SearchManager';
-import * as uuid from 'uuid';
-import { DashVersion } from '../../fields/DocSymbols';
-import * as AdmZip from 'adm-zip';
-import * as imageDataUri from 'image-data-uri';
-import * as fs from 'fs';
-
-export enum Directory {
-    parsed_files = 'parsed_files',
-    images = 'images',
-    videos = 'videos',
-    pdfs = 'pdfs',
-    text = 'text',
-    audio = 'audio',
-    csv = 'csv',
-}
-
-export function serverPathToFile(directory: Directory, filename: string) {
-    return path.normalize(`${filesDirectory}/${directory}/${filename}`);
-}
-
-export function pathToDirectory(directory: Directory) {
-    return path.normalize(`${filesDirectory}/${directory}`);
-}
-
-export function clientPathToFile(directory: Directory, filename: string) {
-    return `/files/${directory}/${filename}`;
-}
 
 export default class UploadManager extends ApiManager {
     protected initialize(register: Registration): void {
         register({
             method: Method.POST,
             subscription: '/ping',
-            secureHandler: async ({ req, res }) => {
+            secureHandler: async ({ /* req, */ res }) => {
                 _success(res, { message: DashVersion, date: new Date() });
             },
         });
@@ -78,31 +55,33 @@ export default class UploadManager extends ApiManager {
                 form.on('progress', e => fileguids.split(';').map(guid => DashUploadUtils.uploadProgress.set(guid, `read:(${Math.round((100 * +e) / +filesize)}%) ${e} of ${filesize}`)));
                 return new Promise<void>(resolve => {
                     form.parse(req, async (_err, _fields, files) => {
-                        const results: Upload.FileResponse[] = [];
                         if (_err?.message) {
-                            results.push({
-                                source: {
-                                    filepath: '',
-                                    originalFilename: 'none',
-                                    newFilename: 'none',
-                                    mimetype: 'text',
-                                    size: 0,
-                                    hashAlgorithm: 'md5',
-                                    toJSON: () => ({ name: 'none', size: 0, length: 0, mtime: new Date(), filepath: '', originalFilename: 'none', newFilename: 'none', mimetype: 'text' }),
+                            _success(res, [
+                                {
+                                    source: {
+                                        filepath: '',
+                                        originalFilename: 'none',
+                                        newFilename: 'none',
+                                        mimetype: 'text',
+                                        size: 0,
+                                        hashAlgorithm: 'md5',
+                                        toJSON: () => ({ name: 'none', size: 0, length: 0, mtime: new Date(), filepath: '', originalFilename: 'none', newFilename: 'none', mimetype: 'text' }),
+                                    },
+                                    result: { name: 'failed upload', message: `${_err.message}` },
                                 },
-                                result: { name: 'failed upload', message: `${_err.message}` },
-                            });
-                        }
-                        fileguids.split(';').map(guid => DashUploadUtils.uploadProgress.set(guid, `resampling images`));
+                            ]);
+                        } else {
+                            fileguids.split(';').map(guid => DashUploadUtils.uploadProgress.set(guid, `resampling images`));
+                            const results = (
+                                await Promise.all(
+                                    Array.from(Object.keys(files)).map(
+                                        async key => (!files[key] ? undefined : DashUploadUtils.upload(files[key]![0] /* , key */)) // key is the guid used by the client to track upload progress.
+                                    )
+                                )
+                            ).filter(result => result && !(result.result instanceof Error));
 
-                        for (const key in files) {
-                            const f = files[key];
-                            if (f) {
-                                const result = await DashUploadUtils.upload(f[0], key); // key is the guid used by the client to track upload progress.
-                                result && !(result.result instanceof Error) && results.push(result);
-                            }
+                            _success(res, results);
                         }
-                        _success(res, results);
                         resolve();
                     });
                 });
@@ -113,17 +92,14 @@ export default class UploadManager extends ApiManager {
             method: Method.POST,
             subscription: '/uploadYoutubeVideo',
             secureHandler: async ({ req, res }) => {
-                //req.readableBuffer.head.data
-                return new Promise<void>(async resolve => {
-                    req.addListener('data', async args => {
-                        const payload = String.fromCharCode.apply(String, args);
-                        const { videoId, overwriteId } = JSON.parse(payload);
-                        const results: Upload.FileResponse[] = [];
-                        const result = await DashUploadUtils.uploadYoutube(videoId, overwriteId ?? videoId);
-                        result && results.push(result);
-                        _success(res, results);
-                        resolve();
-                    });
+                // req.readableBuffer.head.data
+                req.addListener('data', async args => {
+                    const payload = String.fromCharCode(...args); // .apply(String, args);
+                    const { videoId, overwriteId } = JSON.parse(payload);
+                    const results: Upload.FileResponse[] = [];
+                    const result = await DashUploadUtils.uploadYoutube(videoId, overwriteId ?? videoId);
+                    result && results.push(result);
+                    _success(res, results);
                 });
             },
         });
@@ -132,49 +108,10 @@ export default class UploadManager extends ApiManager {
             method: Method.POST,
             subscription: '/queryYoutubeProgress',
             secureHandler: async ({ req, res }) => {
-                return new Promise<void>(async resolve => {
-                    req.addListener('data', args => {
-                        const payload = String.fromCharCode.apply(String, args);
-                        const videoId = JSON.parse(payload).videoId;
-                        _success(res, { progress: DashUploadUtils.QueryYoutubeProgress(videoId, req.user) });
-                        resolve();
-                    });
-                });
-            },
-        });
-
-        register({
-            method: Method.POST,
-            subscription: new RouteSubscriber('youtubeScreenshot'),
-            secureHandler: async ({ req, res }) => {
-                const { id, timecode } = req.body;
-                const convert = (raw: string) => {
-                    const number = Math.floor(Number(raw));
-                    const seconds = number % 60;
-                    const minutes = (number - seconds) / 60;
-                    return `${minutes}m${seconds}s`;
-                };
-                const suffix = timecode ? `&t=${convert(timecode)}` : ``;
-                const targetUrl = `https://www.youtube.com/watch?v=${id}${suffix}`;
-                const buffer = await captureYoutubeScreenshot(targetUrl);
-                if (!buffer) {
-                    return res.send();
-                }
-                const resolvedName = `youtube_capture_${id}_${suffix}.png`;
-                const resolvedPath = serverPathToFile(Directory.images, resolvedName);
-                return new Promise<void>(resolve => {
-                    writeFile(resolvedPath, buffer, async error => {
-                        if (error) {
-                            return res.send();
-                        }
-                        await DashUploadUtils.outputResizedImages(resolvedPath, resolvedName, pathToDirectory(Directory.images));
-                        res.send({
-                            accessPaths: {
-                                agnostic: DashUploadUtils.getAccessPaths(Directory.images, resolvedName),
-                            },
-                        } as Upload.FileInformation);
-                        resolve();
-                    });
+                req.addListener('data', args => {
+                    const payload = String.fromCharCode(...args); // .apply(String, args);
+                    const { videoId } = JSON.parse(payload);
+                    _success(res, { progress: DashUploadUtils.QueryYoutubeProgress(videoId) });
                 });
             },
         });
@@ -186,7 +123,8 @@ export default class UploadManager extends ApiManager {
                 const { sources } = req.body;
                 if (Array.isArray(sources)) {
                     const results = await Promise.all(sources.map(source => DashUploadUtils.UploadImage(source)));
-                    return res.send(results);
+                    res.send(results);
+                    return;
                 }
                 res.send();
             },
@@ -203,20 +141,22 @@ export default class UploadManager extends ApiManager {
                 const getId = (id: string): string => {
                     if (!remap || id.endsWith('Proto')) return id;
                     if (id in ids) return ids[id];
-                    return (ids[id] = uuid.v4());
+                    ids[id] = uuid.v4();
+                    return ids[id];
                 };
-                const mapFn = (doc: any) => {
+                const mapFn = (docIn: any) => {
+                    const doc = docIn;
                     if (doc.id) {
                         doc.id = getId(doc.id);
                     }
+                    // eslint-disable-next-line no-restricted-syntax
                     for (const key in doc.fields) {
-                        if (!doc.fields.hasOwnProperty(key)) {
-                            continue;
-                        }
+                        // eslint-disable-next-line no-continue
+                        if (!Object.prototype.hasOwnProperty.call(doc.fields, key)) continue;
+
                         const field = doc.fields[key];
-                        if (field === undefined || field === null) {
-                            continue;
-                        }
+                        // eslint-disable-next-line no-continue
+                        if (field === undefined || field === null) continue;
 
                         if (field.__type === 'Doc') {
                             mapFn(field);
@@ -229,78 +169,80 @@ export default class UploadManager extends ApiManager {
                         } else if (field.__type === 'list') {
                             mapFn(field);
                         } else if (typeof field === 'string') {
-                            const re = /("(?:dataD|d)ocumentId"\s*:\s*")([\w\-]*)"/g;
-                            doc.fields[key] = (field as any).replace(re, (match: any, p1: string, p2: string) => {
-                                return `${p1}${getId(p2)}"`;
-                            });
+                            const re = /("(?:dataD|d)ocumentId"\s*:\s*")([\w-]*)"/g;
+                            doc.fields[key] = (field as any).replace(re, (match: any, p1: string, p2: string) => `${p1}${getId(p2)}"`);
                         } else if (field.__type === 'RichTextField') {
                             const re = /("href"\s*:\s*")(.*?)"/g;
-                            field.Data = field.Data.replace(re, (match: any, p1: string, p2: string) => {
-                                return `${p1}${getId(p2)}"`;
-                            });
+                            field.Data = field.Data.replace(re, (match: any, p1: string, p2: string) => `${p1}${getId(p2)}"`);
                         }
                     }
                 };
                 return new Promise<void>(resolve => {
                     form.parse(req, async (_err, fields, files) => {
-                        remap = Object.keys(fields).some(key => key === 'remap' && !fields.remap?.includes('false')); //.remap !== 'false'; // bcz: looking to see if the field 'remap' is set to 'false'
+                        remap = Object.keys(fields).some(key => key === 'remap' && !fields.remap?.includes('false')); // .remap !== 'false'; // bcz: looking to see if the field 'remap' is set to 'false'
                         let id: string = '';
                         let docids: string[] = [];
                         let linkids: string[] = [];
                         try {
-                            for (const name in files) {
-                                const f = files[name];
-                                if (!f) continue;
-                                const path_2 = f[0]; // what about the rest of the array? are we guaranteed only one value is set?
-                                const zip = new AdmZip(path_2.filepath);
-                                zip.getEntries().forEach((entry: any) => {
-                                    let entryName = entry.entryName.replace(/%%%/g, '/');
-                                    if (!entryName.startsWith('files/')) {
-                                        return;
-                                    }
-                                    const extension = path.extname(entryName);
-                                    const pathname = publicDirectory + '/' + entry.entryName;
-                                    const targetname = publicDirectory + '/' + entryName;
-                                    try {
-                                        zip.extractEntryTo(entry.entryName, publicDirectory, true, false);
-                                        createReadStream(pathname).pipe(createWriteStream(targetname));
-                                        Jimp.read(pathname).then(img => {
-                                            DashUploadUtils.imageResampleSizes(extension).forEach(({ width, suffix }) => {
-                                                const outputPath = InjectSize(targetname, suffix);
-                                                if (!width) createReadStream(pathname).pipe(createWriteStream(outputPath));
-                                                else img = img.resize(width, Jimp.AUTO).write(outputPath);
+                            // eslint-disable-next-line no-restricted-syntax
+                            for (const name in Object.keys(files)) {
+                                if (Object.prototype.hasOwnProperty.call(files, name)) {
+                                    const f = files[name];
+                                    // eslint-disable-next-line no-continue
+                                    if (!f) continue;
+                                    const path2 = f[0]; // what about the rest of the array? are we guaranteed only one value is set?
+                                    const zip = new AdmZip(path2.filepath);
+                                    zip.getEntries().forEach((entry: any) => {
+                                        const entryName = entry.entryName.replace(/%%%/g, '/');
+                                        if (!entryName.startsWith('files/')) {
+                                            return;
+                                        }
+                                        const extension = path.extname(entryName);
+                                        const pathname = publicDirectory + '/' + entry.entryName;
+                                        const targetname = publicDirectory + '/' + entryName;
+                                        try {
+                                            zip.extractEntryTo(entry.entryName, publicDirectory, true, false);
+                                            createReadStream(pathname).pipe(createWriteStream(targetname));
+                                            Jimp.read(pathname).then(imgIn => {
+                                                let img = imgIn;
+                                                DashUploadUtils.imageResampleSizes(extension).forEach(({ width, suffix }) => {
+                                                    const outputPath = InjectSize(targetname, suffix);
+                                                    if (!width) createReadStream(pathname).pipe(createWriteStream(outputPath));
+                                                    else img = img.resize(width, Jimp.AUTO).write(outputPath);
+                                                });
+                                                unlink(pathname, () => {});
                                             });
-                                            unlink(pathname, () => {});
-                                        });
-                                    } catch (e) {
-                                        console.log(e);
-                                    }
-                                });
-                                const json = zip.getEntry('docs.json');
-                                if (json) {
-                                    try {
-                                        const data = JSON.parse(json.getData().toString('utf8'), retrocycle());
-                                        const { docs, links } = data;
-                                        id = getId(data.id);
-                                        const rdocs = Object.keys(docs).map(key => docs[key]);
-                                        const ldocs = Object.keys(links).map(key => links[key]);
-                                        [...rdocs, ...ldocs].forEach(mapFn);
-                                        docids = rdocs.map(doc => doc.id);
-                                        linkids = ldocs.map(link => link.id);
-                                        await Promise.all(
-                                            [...rdocs, ...ldocs].map(
-                                                doc =>
-                                                    new Promise<void>(res => {
-                                                        // overwrite mongo doc with json doc contents
-                                                        Database.Instance.replace(doc.id, doc, (err, r) => res(err && console.log(err)), true);
-                                                    })
-                                            )
-                                        );
-                                    } catch (e) {
-                                        console.log(e);
+                                        } catch (e) {
+                                            console.log(e);
+                                        }
+                                    });
+                                    const json = zip.getEntry('docs.json');
+                                    if (json) {
+                                        try {
+                                            const data = JSON.parse(json.getData().toString('utf8'), retrocycle());
+                                            const { docs, links } = data;
+                                            id = getId(data.id);
+                                            const rdocs = Object.keys(docs).map(key => docs[key]);
+                                            const ldocs = Object.keys(links).map(key => links[key]);
+                                            [...rdocs, ...ldocs].forEach(mapFn);
+                                            docids = rdocs.map(doc => doc.id);
+                                            linkids = ldocs.map(link => link.id);
+                                            // eslint-disable-next-line no-await-in-loop
+                                            await Promise.all(
+                                                [...rdocs, ...ldocs].map(
+                                                    doc =>
+                                                        new Promise<void>(res => {
+                                                            // overwrite mongo doc with json doc contents
+                                                            Database.Instance.replace(doc.id, doc, err => res(err && console.log(err)), true);
+                                                        })
+                                                )
+                                            );
+                                        } catch (e) {
+                                            console.log(e);
+                                        }
                                     }
+                                    unlink(path2.filepath, () => {});
                                 }
-                                unlink(path_2.filepath, () => {});
                             }
                             SolrManager.update();
                             res.send(JSON.stringify({ id, docids, linkids } || 'error'));
@@ -319,9 +261,8 @@ export default class UploadManager extends ApiManager {
             secureHandler: async ({ req, res }) => {
                 const { source } = req.body;
                 if (typeof source === 'string') {
-                    return res.send(await DashUploadUtils.InspectImage(source));
-                }
-                res.send({});
+                    res.send(await DashUploadUtils.InspectImage(source));
+                } else res.send({});
             },
         });
 
@@ -329,7 +270,7 @@ export default class UploadManager extends ApiManager {
             method: Method.POST,
             subscription: '/uploadURI',
             secureHandler: ({ req, res }) => {
-                const uri: any = req.body.uri;
+                const { uri } = req.body;
                 const filename = req.body.name;
                 const origSuffix = req.body.nosuffix ? SizeSuffix.None : SizeSuffix.Original;
                 const deleteFiles = req.body.replaceRootFilename;
@@ -338,23 +279,24 @@ export default class UploadManager extends ApiManager {
                     return;
                 }
                 if (deleteFiles) {
-                    const path = serverPathToFile(Directory.images, '');
+                    const serverPath = serverPathToFile(Directory.images, '');
                     const regex = new RegExp(`${deleteFiles}.*`);
-                    fs.readdirSync(path)
+                    fs.readdirSync(serverPath)
                         .filter((f: any) => regex.test(f))
-                        .map((f: any) => fs.unlinkSync(path + f));
+                        .map((f: any) => fs.unlinkSync(serverPath + f));
                 }
-                return imageDataUri.outputFile(uri, serverPathToFile(Directory.images, InjectSize(filename, origSuffix))).then((savedName: string) => {
+                imageDataUri.outputFile(uri, serverPathToFile(Directory.images, InjectSize(filename, origSuffix))).then((savedName: string) => {
                     const ext = path.extname(savedName).toLowerCase();
                     if (AcceptableMedia.imageFormats.includes(ext)) {
-                        Jimp.read(savedName).then(img =>
+                        Jimp.read(savedName).then(imgIn => {
+                            let img = imgIn;
                             (!origSuffix ? [{ width: 400, suffix: SizeSuffix.Medium }] : Object.values(DashUploadUtils.Sizes)) //
                                 .forEach(({ width, suffix }) => {
                                     const outputPath = serverPathToFile(Directory.images, InjectSize(filename, suffix) + ext);
                                     if (!width) createReadStream(savedName).pipe(createWriteStream(outputPath));
                                     else img = img.resize(width, Jimp.AUTO).write(outputPath);
-                                })
-                        );
+                                });
+                        });
                     }
                     res.send(clientPathToFile(Directory.images, filename + ext));
                 });
@@ -362,35 +304,3 @@ export default class UploadManager extends ApiManager {
         });
     }
 }
-function delay(ms: number) {
-    return new Promise(resolve => setTimeout(resolve, ms));
-}
-/**
- * On success, returns a buffer containing the bytes of a screenshot
- * of the video (optionally, at a timecode) specified by @param targetUrl.
- *
- * On failure, returns undefined.
- */
-async function captureYoutubeScreenshot(targetUrl: string) {
-    // const browser = await launch({ args: ['--no-sandbox', '--disable-setuid-sandbox'] });
-    // const page = await browser.newPage();
-    // // await page.setViewport({ width: 1920, height: 1080 });
-
-    // // await page.goto(targetUrl, { waitUntil: 'domcontentloaded' as any });
-
-    // const videoPlayer = await page.$('.html5-video-player');
-    // videoPlayer && await page.focus("video");
-    // await delay(7000);
-    // const ad = await page.$('.ytp-ad-skip-button-text');
-    // await ad?.click();
-    // await videoPlayer?.click();
-    // await delay(1000);
-    // // hide youtube player controls.
-    // await page.evaluate(() => (document.querySelector('.ytp-chrome-bottom') as HTMLElement).style.display = 'none');
-
-    // const buffer = await videoPlayer?.screenshot({ encoding: "binary" });
-    // await browser.close();
-
-    // return buffer;
-    return null;
-}
diff --git a/src/server/ApiManagers/UserManager.ts b/src/server/ApiManagers/UserManager.ts
index 0431b9bcf..b587340e2 100644
--- a/src/server/ApiManagers/UserManager.ts
+++ b/src/server/ApiManagers/UserManager.ts
@@ -1,16 +1,14 @@
-import ApiManager, { Registration } from './ApiManager';
-import { Method } from '../RouteManager';
-import { Database } from '../database';
-import { msToTime } from '../ActionUtilities';
 import * as bcrypt from 'bcrypt-nodejs';
+import { check, validationResult } from 'express-validator';
+import { Utils } from '../../Utils';
 import { Opt } from '../../fields/Doc';
-import { WebSocket } from '../websocket';
-import { resolvedPorts } from '../server_Initialization';
 import { DashVersion } from '../../fields/DocSymbols';
-import { Utils } from '../../Utils';
-import { check, validationResult } from 'express-validator';
+import { msToTime } from '../ActionUtilities';
+import { Method } from '../RouteManager';
+import { resolvedPorts, socketMap, timeMap } from '../SocketData';
+import { Database } from '../database';
+import ApiManager, { Registration } from './ApiManager';
 
-export const timeMap: { [id: string]: number } = {};
 interface ActivityUnit {
     user: string;
     duration: number;
@@ -32,9 +30,10 @@ export default class UserManager extends ApiManager {
             method: Method.POST,
             subscription: '/setCacheDocumentIds',
             secureHandler: async ({ user, req, res }) => {
+                const userModel = user;
                 const result: any = {};
-                user.cacheDocumentIds = req.body.cacheDocumentIds;
-                user.save().then(undefined, err => {
+                userModel.cacheDocumentIds = req.body.cacheDocumentIds;
+                userModel.save().then(undefined, (err: any) => {
                     if (err) {
                         result.error = [{ msg: 'Error while caching documents' }];
                     }
@@ -90,17 +89,19 @@ export default class UserManager extends ApiManager {
             method: Method.POST,
             subscription: '/internalResetPassword',
             secureHandler: async ({ user, req, res }) => {
+                const userModel = user;
                 const result: any = {};
-                const { curr_pass, new_pass, new_confirm } = req.body;
+                // eslint-disable-next-line camelcase
+                const { curr_pass, new_pass } = req.body;
                 // perhaps should assert whether curr password is entered correctly
                 const validated = await new Promise<Opt<boolean>>(resolve => {
-                    bcrypt.compare(curr_pass, user.password, (err, passwords_match) => {
-                        if (err || !passwords_match) {
+                    bcrypt.compare(curr_pass, userModel.password, (err, passwordsMatch) => {
+                        if (err || !passwordsMatch) {
                             result.error = [{ msg: 'Incorrect current password' }];
                             res.send(result);
                             resolve(undefined);
                         } else {
-                            resolve(passwords_match);
+                            resolve(passwordsMatch);
                         }
                     });
                 });
@@ -111,10 +112,11 @@ export default class UserManager extends ApiManager {
 
                 check('new_pass', 'Password must be at least 4 characters long')
                     .run(req)
-                    .then(chcekcres => console.log(chcekcres)); //.len({ min: 4 });
+                    .then(chcekcres => console.log(chcekcres)); // .len({ min: 4 });
                 check('new_confirm', 'Passwords do not match')
                     .run(req)
-                    .then(theres => console.log(theres)); //.equals(new_pass);
+                    .then(theres => console.log(theres)); // .equals(new_pass);
+                // eslint-disable-next-line camelcase
                 if (curr_pass === new_pass) {
                     result.error = [{ msg: 'Current and new password are the same' }];
                 }
@@ -125,12 +127,13 @@ export default class UserManager extends ApiManager {
 
                 // will only change password if there are no errors.
                 if (!result.error) {
-                    user.password = new_pass;
-                    user.passwordResetToken = undefined;
-                    user.passwordResetExpires = undefined;
+                    // eslint-disable-next-line camelcase
+                    userModel.password = new_pass;
+                    userModel.passwordResetToken = undefined;
+                    userModel.passwordResetExpires = undefined;
                 }
 
-                user.save().then(undefined, err => {
+                userModel.save().then(undefined, err => {
                     if (err) {
                         result.error = [{ msg: 'Error while saving new password' }];
                     }
@@ -149,13 +152,16 @@ export default class UserManager extends ApiManager {
                 const activeTimes: ActivityUnit[] = [];
                 const inactiveTimes: ActivityUnit[] = [];
 
+                // eslint-disable-next-line no-restricted-syntax
                 for (const user in timeMap) {
-                    const time = timeMap[user];
-                    const socketPair = Array.from(WebSocket.socketMap).find(pair => pair[1] === user);
-                    if (socketPair && !socketPair[0].disconnected) {
-                        const duration = now - time;
-                        const target = duration / 1000 < 60 * 5 ? activeTimes : inactiveTimes;
-                        target.push({ user, duration });
+                    if (Object.prototype.hasOwnProperty.call(timeMap, user)) {
+                        const time = timeMap[user];
+                        const socketPair = Array.from(socketMap).find(pair => pair[1] === user);
+                        if (socketPair && !socketPair[0].disconnected) {
+                            const duration = now - time;
+                            const target = duration / 1000 < 60 * 5 ? activeTimes : inactiveTimes;
+                            target.push({ user, duration });
+                        }
                     }
                 }
 
diff --git a/src/server/Client.ts b/src/server/Client.ts
index e6f953712..f67999c5b 100644
--- a/src/server/Client.ts
+++ b/src/server/Client.ts
@@ -1,4 +1,4 @@
-import { computed } from "mobx";
+import { computed } from 'mobx';
 
 export class Client {
     private _guid: string;
@@ -7,5 +7,7 @@ export class Client {
         this._guid = guid;
     }
 
-    @computed public get GUID(): string { return this._guid; }
-}
\ No newline at end of file
+    @computed public get GUID(): string {
+        return this._guid;
+    }
+}
diff --git a/src/server/DashSession/DashSessionAgent.ts b/src/server/DashSession/DashSessionAgent.ts
index 1ef7a131d..f937c17ad 100644
--- a/src/server/DashSession/DashSessionAgent.ts
+++ b/src/server/DashSession/DashSessionAgent.ts
@@ -1,18 +1,19 @@
-import { Email, pathFromRoot } from '../ActionUtilities';
-import { red, yellow, green, cyan } from 'colors';
-import { get } from 'request-promise';
-import { Utils } from '../../Utils';
-import { WebSocket } from '../websocket';
-import { MessageStore } from '../Message';
-import { launchServer, onWindows } from '..';
-import { readdirSync, statSync, createWriteStream, readFileSync, unlinkSync } from 'fs';
 import * as Archiver from 'archiver';
+import { cyan, green, red, yellow } from 'colors';
+import { createWriteStream, readFileSync, readdirSync, statSync, unlinkSync } from 'fs';
 import { resolve } from 'path';
+import { get } from 'request-promise';
 import { rimraf } from 'rimraf';
+import { launchServer, onWindows } from '..';
+import { Utils } from '../../Utils';
+import { ServerUtils } from '../../ServerUtils';
+import { Email, pathFromRoot } from '../ActionUtilities';
+import { MessageStore } from '../Message';
+import { WebSocket } from '../websocket';
 import { AppliedSessionAgent, ExitHandler } from './Session/agents/applied_session_agent';
-import { ServerWorker } from './Session/agents/server_worker';
 import { Monitor } from './Session/agents/monitor';
-import { MessageHandler, ErrorLike } from './Session/agents/promisified_ipc_manager';
+import { ErrorLike, MessageHandler } from './Session/agents/promisified_ipc_manager';
+import { ServerWorker } from './Session/agents/server_worker';
 
 /**
  * If we're the monitor (master) thread, we should launch the monitor logic for the session.
@@ -22,6 +23,7 @@ import { MessageHandler, ErrorLike } from './Session/agents/promisified_ipc_mana
 export class DashSessionAgent extends AppliedSessionAgent {
     private readonly signature = '-Dash Server Session Manager';
     private readonly releaseDesktop = pathFromRoot('../../Desktop');
+    public static notificationRecipient = 'browndashptc@gmail.com';
 
     /**
      * The core method invoked when the single master thread is initialized.
@@ -149,7 +151,7 @@ export class DashSessionAgent extends AppliedSessionAgent {
         const { _socket } = WebSocket;
         if (_socket) {
             const message = typeof reason === 'boolean' ? (reason ? 'exit' : 'temporary') : 'crash';
-            Utils.Emit(_socket, MessageStore.ConnectionTerminated, message);
+            ServerUtils.Emit(_socket, MessageStore.ConnectionTerminated, message);
         }
     };
 
@@ -217,7 +219,3 @@ export class DashSessionAgent extends AppliedSessionAgent {
         }
     }
 }
-
-export namespace DashSessionAgent {
-    export const notificationRecipient = 'browndashptc@gmail.com';
-}
diff --git a/src/server/DashStats.ts b/src/server/DashStats.ts
index a9e6af67c..485ab9f99 100644
--- a/src/server/DashStats.ts
+++ b/src/server/DashStats.ts
@@ -1,9 +1,8 @@
 import { cyan, magenta } from 'colors';
 import { Response } from 'express';
-import SocketIO from 'socket.io';
-import { timeMap } from './ApiManagers/UserManager';
-import { WebSocket } from './websocket';
 import * as fs from 'fs';
+import SocketIO from 'socket.io';
+import { socketMap, timeMap, userOperations } from './SocketData';
 
 /**
  * DashStats focuses on tracking user data for each session.
@@ -17,7 +16,6 @@ export namespace DashStats {
 
     const statsCSVDirectory = './src/server/stats/';
     const statsCSVFilename = statsCSVDirectory + 'userLoginStats.csv';
-    const columns = ['USERNAME', 'ACTION', 'TIME'];
 
     /**
      * UserStats holds the stats associated with a particular user.
@@ -77,6 +75,111 @@ export namespace DashStats {
     // structure
     export const lastUserOperations = new Map<string, UserLastOperations>();
 
+    /**
+     * convertToCSV() is a helper method that stringifies a CSVStore object
+     * that can be written to the CSV file later.
+     * @param dataObject the object to stringify
+     * @returns the object as a string.
+     */
+    function convertToCSV(dataObject: CSVStore): string {
+        return `${dataObject.USERNAME},${dataObject.ACTION},${dataObject.TIME}\n`;
+    }
+    /**
+     * getLastOperationsOrDefault() is a helper method that will attempt
+     * to query the lastUserOperations map for a specified username. If the
+     * username is not in the map, an empty UserLastOperations object is returned.
+     * @param username
+     * @returns the user's UserLastOperations structure or an empty
+     * UserLastOperations object (All values set to 0) if the username is not found.
+     */
+    function getLastOperationsOrDefault(username: string): UserLastOperations {
+        if (lastUserOperations.get(username) === undefined) {
+            const initializeOperationsQueue = [];
+            for (let i = 0; i < RATE_INTERVAL; i++) {
+                initializeOperationsQueue.push(0);
+            }
+            return {
+                sampleOperations: 0,
+                lastSampleOperations: 0,
+                previousOperationsQueue: initializeOperationsQueue,
+            };
+        }
+        return lastUserOperations.get(username)!;
+    }
+
+    /**
+     * updateLastOperations updates a specific user's UserLastOperations information
+     * for the current sampling cycle. The method removes old/outdated counts for
+     * operations from the queue and adds new data for the current sampling
+     * cycle to the queue, updating the total count as it goes.
+     * @param lastOperationData the old UserLastOperations data that must be updated
+     * @param currentOperations the total number of operations measured for this sampling cycle.
+     * @returns the udpated UserLastOperations structure.
+     */
+    function updateLastOperations(lastOperationData: UserLastOperations, currentOperations: number): UserLastOperations {
+        // create a copy of the UserLastOperations to modify
+        const newLastOperationData: UserLastOperations = {
+            sampleOperations: lastOperationData.sampleOperations,
+            lastSampleOperations: lastOperationData.lastSampleOperations,
+            previousOperationsQueue: lastOperationData.previousOperationsQueue.slice(),
+        };
+
+        let newSampleOperations = newLastOperationData.sampleOperations;
+        newSampleOperations -= newLastOperationData.previousOperationsQueue.shift()!; // removes and returns the first element of the queue
+        const operationsThisCycle = currentOperations - lastOperationData.lastSampleOperations;
+        newSampleOperations += operationsThisCycle; // add the operations this cycle to find out what our count for the interval should be (e.g operations in the last 10 seconds)
+
+        // update values for the copy object
+        newLastOperationData.sampleOperations = newSampleOperations;
+
+        newLastOperationData.previousOperationsQueue.push(operationsThisCycle);
+        newLastOperationData.lastSampleOperations = currentOperations;
+
+        return newLastOperationData;
+    }
+
+    /**
+     * getUserOperationsOrDefault() is a helper method to get the user's total
+     * operations for the CURRENT sampling interval. The method will return 0
+     * if the username is not in the userOperations map.
+     * @param username the username to search the map for
+     * @returns the total number of operations recorded up to this sampling cycle.
+     */
+    function getUserOperationsOrDefault(username: string): number {
+        return userOperations.get(username) === undefined ? 0 : userOperations.get(username)!;
+    }
+
+    /**
+     * getCurrentStats() calculates the total stats for this cycle. In this case,
+     * getCurrentStats() returns an Array of UserStats[] objects describing
+     * the stats for each user
+     * @returns an array of UserStats storing data for each user at the current moment.
+     */
+    function getCurrentStats(): UserStats[] {
+        const socketPairs: UserStats[] = [];
+        Array.from(socketMap.entries()).forEach(([key, value]) => {
+            const username = value.split(' ')[0];
+            const connectionTime = new Date(timeMap[username]);
+
+            const connectionTimeString = connectionTime.toLocaleDateString() + ' ' + connectionTime.toLocaleTimeString();
+
+            if (!key.disconnected) {
+                const lastRecordedOperations = getLastOperationsOrDefault(username);
+                const currentUserOperationCount = getUserOperationsOrDefault(username);
+
+                socketPairs.push({
+                    socketId: key.id,
+                    username: username,
+                    time: connectionTimeString.includes('Invalid Date') ? '' : connectionTimeString,
+                    operations: userOperations.get(username) ? userOperations.get(username)! : 0,
+                    rate: lastRecordedOperations.sampleOperations,
+                });
+                lastUserOperations.set(username, updateLastOperations(lastRecordedOperations, currentUserOperationCount));
+            }
+        });
+        return socketPairs;
+    }
+
     /**
      * handleStats is called when the /stats route is called, providing a JSON
      * object with relevant stats. In this case, we return the number of
@@ -84,8 +187,7 @@ export namespace DashStats {
      * @param res Response object from Express
      */
     export function handleStats(res: Response) {
-        let current = getCurrentStats();
-        const results: CSVStore[] = [];
+        const current = getCurrentStats();
         res.json({
             currentConnections: current.length,
             socketMap: current,
@@ -99,7 +201,7 @@ export namespace DashStats {
      * @returns a StatsDataBundle that is sent to the frontend view on each websocket update
      */
     export function getUpdatedStatsBundle(): StatsDataBundle {
-        let current = getCurrentStats();
+        const current = getCurrentStats();
 
         return {
             connectedUsers: current,
@@ -113,11 +215,8 @@ export namespace DashStats {
      * @param res
      */
     export function handleStatsView(res: Response) {
-        let current = getCurrentStats();
-
-        let connectedUsers = current.map(socketPair => {
-            return socketPair.time + ' - ' + socketPair.username + ' Operations: ' + socketPair.operations;
-        });
+        const current = getCurrentStats();
+        const connectedUsers = current.map(({ time, username, operations }) => time + ' - ' + username + ' Operations: ' + operations);
 
         let serverTraffic = ServerTraffic.NOT_BUSY;
         if (current.length < BUSY_SERVER_BOUND) {
@@ -145,17 +244,17 @@ export namespace DashStats {
      */
     export function logUserLogin(username: string | undefined, socket: SocketIO.Socket) {
         if (!(username === undefined)) {
-            let currentDate = new Date();
+            const currentDate = new Date();
             console.log(magenta(`User ${username.split(' ')[0]} logged in at: ${currentDate.toISOString()}`));
 
-            let toWrite: CSVStore = {
+            const toWrite: CSVStore = {
                 USERNAME: username,
                 ACTION: 'loggedIn',
                 TIME: currentDate.toISOString(),
             };
 
             if (!fs.existsSync(statsCSVDirectory)) fs.mkdirSync(statsCSVDirectory);
-            let statsFile = fs.createWriteStream(statsCSVFilename, { flags: 'a' });
+            const statsFile = fs.createWriteStream(statsCSVFilename, { flags: 'a' });
             statsFile.write(convertToCSV(toWrite));
             statsFile.end();
             console.log(cyan(convertToCSV(toWrite)));
@@ -170,10 +269,10 @@ export namespace DashStats {
      */
     export function logUserLogout(username: string | undefined, socket: SocketIO.Socket) {
         if (!(username === undefined)) {
-            let currentDate = new Date();
+            const currentDate = new Date();
 
-            let statsFile = fs.createWriteStream(statsCSVFilename, { flags: 'a' });
-            let toWrite: CSVStore = {
+            const statsFile = fs.createWriteStream(statsCSVFilename, { flags: 'a' });
+            const toWrite: CSVStore = {
                 USERNAME: username,
                 ACTION: 'loggedOut',
                 TIME: currentDate.toISOString(),
@@ -182,110 +281,4 @@ export namespace DashStats {
             statsFile.end();
         }
     }
-
-    /**
-     * getLastOperationsOrDefault() is a helper method that will attempt
-     * to query the lastUserOperations map for a specified username. If the
-     * username is not in the map, an empty UserLastOperations object is returned.
-     * @param username
-     * @returns the user's UserLastOperations structure or an empty
-     * UserLastOperations object (All values set to 0) if the username is not found.
-     */
-    function getLastOperationsOrDefault(username: string): UserLastOperations {
-        if (lastUserOperations.get(username) === undefined) {
-            let initializeOperationsQueue = [];
-            for (let i = 0; i < RATE_INTERVAL; i++) {
-                initializeOperationsQueue.push(0);
-            }
-            return {
-                sampleOperations: 0,
-                lastSampleOperations: 0,
-                previousOperationsQueue: initializeOperationsQueue,
-            };
-        }
-        return lastUserOperations.get(username)!;
-    }
-
-    /**
-     * updateLastOperations updates a specific user's UserLastOperations information
-     * for the current sampling cycle. The method removes old/outdated counts for
-     * operations from the queue and adds new data for the current sampling
-     * cycle to the queue, updating the total count as it goes.
-     * @param lastOperationData the old UserLastOperations data that must be updated
-     * @param currentOperations the total number of operations measured for this sampling cycle.
-     * @returns the udpated UserLastOperations structure.
-     */
-    function updateLastOperations(lastOperationData: UserLastOperations, currentOperations: number): UserLastOperations {
-        // create a copy of the UserLastOperations to modify
-        let newLastOperationData: UserLastOperations = {
-            sampleOperations: lastOperationData.sampleOperations,
-            lastSampleOperations: lastOperationData.lastSampleOperations,
-            previousOperationsQueue: lastOperationData.previousOperationsQueue.slice(),
-        };
-
-        let newSampleOperations = newLastOperationData.sampleOperations;
-        newSampleOperations -= newLastOperationData.previousOperationsQueue.shift()!; // removes and returns the first element of the queue
-        let operationsThisCycle = currentOperations - lastOperationData.lastSampleOperations;
-        newSampleOperations += operationsThisCycle; // add the operations this cycle to find out what our count for the interval should be (e.g operations in the last 10 seconds)
-
-        // update values for the copy object
-        newLastOperationData.sampleOperations = newSampleOperations;
-
-        newLastOperationData.previousOperationsQueue.push(operationsThisCycle);
-        newLastOperationData.lastSampleOperations = currentOperations;
-
-        return newLastOperationData;
-    }
-
-    /**
-     * getUserOperationsOrDefault() is a helper method to get the user's total
-     * operations for the CURRENT sampling interval. The method will return 0
-     * if the username is not in the userOperations map.
-     * @param username the username to search the map for
-     * @returns the total number of operations recorded up to this sampling cycle.
-     */
-    function getUserOperationsOrDefault(username: string): number {
-        return WebSocket.userOperations.get(username) === undefined ? 0 : WebSocket.userOperations.get(username)!;
-    }
-
-    /**
-     * getCurrentStats() calculates the total stats for this cycle. In this case,
-     * getCurrentStats() returns an Array of UserStats[] objects describing
-     * the stats for each user
-     * @returns an array of UserStats storing data for each user at the current moment.
-     */
-    function getCurrentStats(): UserStats[] {
-        let socketPairs: UserStats[] = [];
-        for (let [key, value] of WebSocket.socketMap) {
-            let username = value.split(' ')[0];
-            let connectionTime = new Date(timeMap[username]);
-
-            let connectionTimeString = connectionTime.toLocaleDateString() + ' ' + connectionTime.toLocaleTimeString();
-
-            if (!key.disconnected) {
-                let lastRecordedOperations = getLastOperationsOrDefault(username);
-                let currentUserOperationCount = getUserOperationsOrDefault(username);
-
-                socketPairs.push({
-                    socketId: key.id,
-                    username: username,
-                    time: connectionTimeString.includes('Invalid Date') ? '' : connectionTimeString,
-                    operations: WebSocket.userOperations.get(username) ? WebSocket.userOperations.get(username)! : 0,
-                    rate: lastRecordedOperations.sampleOperations,
-                });
-                lastUserOperations.set(username, updateLastOperations(lastRecordedOperations, currentUserOperationCount));
-            }
-        }
-        return socketPairs;
-    }
-
-    /**
-     * convertToCSV() is a helper method that stringifies a CSVStore object
-     * that can be written to the CSV file later.
-     * @param dataObject the object to stringify
-     * @returns the object as a string.
-     */
-    function convertToCSV(dataObject: CSVStore): string {
-        return `${dataObject.USERNAME},${dataObject.ACTION},${dataObject.TIME}\n`;
-    }
 }
diff --git a/src/server/DashUploadUtils.ts b/src/server/DashUploadUtils.ts
index 307aec6fc..3d8325da9 100644
--- a/src/server/DashUploadUtils.ts
+++ b/src/server/DashUploadUtils.ts
@@ -15,16 +15,15 @@ import { basename } from 'path';
 import * as parse from 'pdf-parse';
 import * as request from 'request-promise';
 import { Duplex, Stream } from 'stream';
-import { filesDirectory, publicDirectory } from '.';
 import { Utils } from '../Utils';
-import { Opt } from '../fields/Doc';
-import { ParsedPDF } from '../server/PdfTypes';
 import { createIfNotExists } from './ActionUtilities';
 import { AzureManager } from './ApiManagers/AzureManager';
-import { Directory, clientPathToFile, pathToDirectory, serverPathToFile } from './ApiManagers/UploadManager';
+import { ParsedPDF } from './PdfTypes';
 import { AcceptableMedia, Upload } from './SharedMediaTypes';
+import { Directory, clientPathToFile, filesDirectory, pathToDirectory, publicDirectory, serverPathToFile } from './SocketData';
 import { resolvedServerUrl } from './server_Initialization';
-const spawn = require('child_process').spawn;
+
+const { spawn } = require('child_process');
 const { exec } = require('child_process');
 const requestImageSize = require('../client/util/request-image-size');
 
@@ -42,7 +41,7 @@ export function InjectSize(filename: string, size: SizeSuffix) {
 }
 
 function isLocal() {
-    return /Dash-Web[0-9]*[\\\/]src[\\\/]server[\\\/]public[\\\/](.*)/;
+    return /Dash-Web[0-9]*[\\/]src[\\/]server[\\/]public[\\/](.*)/;
 }
 
 function usingAzure() {
@@ -68,11 +67,21 @@ export namespace DashUploadUtils {
     const size = 'content-length';
     const type = 'content-type';
 
-    const BLOBSTORE_URL = process.env.BLOBSTORE_URL;
-    const RESIZE_FUNCTION_URL = process.env.RESIZE_FUNCTION_URL;
+    const { BLOBSTORE_URL, RESIZE_FUNCTION_URL } = process.env;
+
+    const { imageFormats, videoFormats, applicationFormats, audioFormats } = AcceptableMedia; // TODO:glr
 
-    const { imageFormats, videoFormats, applicationFormats, audioFormats } = AcceptableMedia; //TODO:glr
+    export function fExists(name: string, destination: Directory) {
+        const destinationPath = serverPathToFile(destination, name);
+        return existsSync(destinationPath);
+    }
 
+    export function getAccessPaths(directory: Directory, fileName: string) {
+        return {
+            client: clientPathToFile(directory, fileName),
+            server: serverPathToFile(directory, fileName),
+        };
+    }
     export async function concatVideos(filePaths: string[]): Promise<Upload.AccessPathInfo> {
         // make a list of paths to create the ordered text file for ffmpeg
         const inputListName = 'concat.txt';
@@ -80,14 +89,14 @@ export namespace DashUploadUtils {
         // make a list of paths to create the ordered text file for ffmpeg
         const filePathsText = filePaths.map(filePath => `file '${filePath}'`).join('\n');
         // write the text file to the file system
-        await new Promise<void>((res, reject) =>
+        await new Promise<void>((res, reject) => {
             writeFile(textFilePath, filePathsText, err => {
                 if (err) {
                     reject();
                     console.log(err);
                 } else res();
-            })
-        );
+            });
+        });
 
         // make output file name based on timestamp
         const outputFileName = `output-${Utils.GenerateGuid()}.mp4`;
@@ -95,19 +104,19 @@ export namespace DashUploadUtils {
         const outputFilePath = path.join(pathToDirectory(Directory.videos), outputFileName);
 
         // concatenate the videos
-        await new Promise((resolve, reject) =>
+        await new Promise((resolve, reject) => {
             ffmpeg()
                 .input(textFilePath)
                 .inputOptions(['-f concat', '-safe 0'])
                 // .outputOptions('-c copy')
-                //.videoCodec("copy")
+                // .videoCodec("copy")
                 .save(outputFilePath)
                 .on('error', (err: any) => {
                     console.log(err);
                     reject();
                 })
-                .on('end', resolve)
-        );
+                .on('end', resolve);
+        });
 
         // delete concat.txt from the file system
         unlinkSync(textFilePath);
@@ -135,270 +144,76 @@ export namespace DashUploadUtils {
         };
     }
 
-    export function QueryYoutubeProgress(videoId: string, user?: Express.User) {
+    export const uploadProgress = new Map<string, string>();
+
+    export function QueryYoutubeProgress(videoId: string) {
         // console.log(`PROGRESS:${videoId}`, (user as any)?.email);
         return uploadProgress.get(videoId) ?? 'pending data upload';
     }
 
-    export let uploadProgress = new Map<string, string>();
-
-    export function uploadYoutube(videoId: string, overwriteId: string): Promise<Upload.FileResponse> {
-        return new Promise<Upload.FileResponse<Upload.FileInformation>>((res, rej) => {
-            const name = videoId;
-            const filepath = name.replace(/^-/, '__') + '.mp4';
-            const finalPath = serverPathToFile(Directory.videos, filepath);
-            if (existsSync(finalPath)) {
-                uploadProgress.set(overwriteId, 'computing duration');
-                exec(`yt-dlp -o ${finalPath} "https://www.youtube.com/watch?v=${videoId}" --get-duration`, (error: any, stdout: any, stderr: any) => {
-                    const time = Array.from(stdout.trim().split(':')).reverse();
-                    const duration = (time.length > 2 ? Number(time[2]) * 1000 * 60 : 0) + (time.length > 1 ? Number(time[1]) * 60 : 0) + (time.length > 0 ? Number(time[0]) : 0);
-                    res(resolveExistingFile(name, filepath, Directory.videos, 'video/mp4', duration, undefined));
-                });
-            } else {
-                uploadProgress.set(overwriteId, 'starting download');
-                const ytdlp = spawn(`yt-dlp`, ['-o', filepath, `https://www.youtube.com/watch?v=${videoId}`, '--max-filesize', '100M', '-f', 'mp4']);
-
-                ytdlp.stdout.on('data', (data: any) => uploadProgress.set(overwriteId, data.toString()));
-
-                let errors = '';
-                ytdlp.stderr.on('data', (data: any) => {
-                    uploadProgress.set(overwriteId, 'error:' + data.toString());
-                    errors = data.toString();
-                });
-
-                ytdlp.on('exit', function (code: any) {
-                    if (code) {
-                        res({
-                            source: {
-                                size: 0,
-                                filepath: name,
-                                originalFilename: name,
-                                newFilename: name,
-                                mimetype: 'video',
-                                hashAlgorithm: 'md5',
-                                toJSON: () => ({ newFilename: name, filepath, mimetype: 'video', mtime: new Date(), size: 0, length: 0, originalFilename: name }),
-                            },
-                            result: { name: 'failed youtube query', message: `Could not archive video.  ${code ? errors : uploadProgress.get(videoId)}` },
-                        });
-                    } else {
-                        uploadProgress.set(overwriteId, 'computing duration');
-                        exec(`yt-dlp-o ${filepath} "https://www.youtube.com/watch?v=${videoId}" --get-duration`, (error: any, stdout: any, stderr: any) => {
-                            const time = Array.from(stdout.trim().split(':')).reverse();
-                            const duration = (time.length > 2 ? Number(time[2]) * 1000 * 60 : 0) + (time.length > 1 ? Number(time[1]) * 60 : 0) + (time.length > 0 ? Number(time[0]) : 0);
-                            const data = { size: 0, filepath, name, mimetype: 'video', originalFilename: name, newFilename: name, hashAlgorithm: 'md5' as 'md5', type: 'video/mp4' };
-                            const file = { ...data, toJSON: () => ({ ...data, length: 0, filename: data.filepath.replace(/.*\//, ''), mtime: new Date(), toJson: () => undefined as any }) };
-                            MoveParsedFile(file, Directory.videos).then(output => {
-                                console.log('OUTPUT = ' + output);
-                                res(output);
-                            });
-                        });
-                    }
+    /**
+     * Basically just a wrapper around rename, which 'deletes'
+     * the file at the old path and 'moves' it to the new one. For simplicity, the
+     * caller just has to pass in the name of the target directory, and this function
+     * will resolve the actual target path from that.
+     * @param file The file to move
+     * @param destination One of the specific media asset directories into which to move it
+     * @param suffix If the file doesn't have a suffix and you want to provide it one
+     * to appear in the new location
+     */
+    export async function MoveParsedFile(file: formidable.File, destination: Directory, suffix?: string, text?: string, duration?: number, targetName?: string): Promise<Upload.FileResponse> {
+        const { filepath } = file;
+        let name = targetName ?? path.basename(filepath);
+        suffix && (name += suffix);
+        return new Promise(resolve => {
+            const destinationPath = serverPathToFile(destination, name);
+            rename(filepath, destinationPath, error => {
+                resolve({
+                    source: file,
+                    result: error ?? {
+                        accessPaths: {
+                            agnostic: getAccessPaths(destination, name),
+                        },
+                        rawText: text,
+                        duration,
+                    },
                 });
-            }
+            });
         });
     }
 
-    export async function upload(file: File, overwriteGuid?: string): Promise<Upload.FileResponse> {
-        const isAzureOn = usingAzure();
-        const { mimetype: type, filepath, originalFilename } = file;
-        const types = type?.split('/') ?? [];
-        // uploadProgress.set(overwriteGuid ?? name, 'uploading'); // If the client sent a guid it uses to track upload progress, use that guid. Otherwise, use the file's name.
-
-        const category = types[0];
-        let format = `.${types[1]}`;
-        console.log(green(`Processing upload of file (${originalFilename}) and format (${format}) with upload type (${type}) in category (${category}).`));
-
-        switch (category) {
-            case 'image':
-                if (imageFormats.includes(format)) {
-                    const result = await UploadImage(filepath, basename(filepath));
-                    return { source: file, result };
-                }
-                fs.unlink(filepath, () => {});
-                return { source: file, result: { name: 'Unsupported image format', message: `Could not upload unsupported file (${originalFilename}). Please convert to an .jpg` } };
-            case 'video':
-                if (format.includes('x-matroska')) {
-                    console.log('case video');
-                    await new Promise(res =>
-                        ffmpeg(file.filepath)
-                            .videoCodec('copy') // this will copy the data instead of reencode it
-                            .save(file.filepath.replace('.mkv', '.mp4'))
-                            .on('end', res)
-                            .on('error', (e: any) => console.log(e))
-                    );
-                    file.filepath = file.filepath.replace('.mkv', '.mp4');
-                    format = '.mp4';
-                }
-                if (format.includes('quicktime')) {
-                    let abort = false;
-                    await new Promise<void>(res =>
-                        ffmpeg.ffprobe(file.filepath, (err: any, metadata: any) => {
-                            if (metadata.streams.some((stream: any) => stream.codec_name === 'hevc')) {
-                                abort = true;
-                            }
-                            res();
-                        })
-                    );
-                    if (abort) {
-                        // bcz: instead of aborting, we could convert the file using the code below to an mp4.  Problem is that this takes a long time and will clog up the server.
-                        // await new Promise(res =>
-                        //     ffmpeg(file.path)
-                        //         .videoCodec('libx264') // this will copy the data instead of reencode it
-                        //         .audioCodec('mp2')
-                        //         .save(file.path.replace('.MOV', '.mp4').replace('.mov', '.mp4'))
-                        //         .on('end', res)
-                        // );
-                        // file.path = file.path.replace('.mov', '.mp4').replace('.MOV', '.mp4');
-                        // format = '.mp4';
-                        fs.unlink(filepath, () => {});
-                        return { source: file, result: { name: 'Unsupported video format', message: `Could not upload unsupported file (${originalFilename}). Please convert to an .mp4` } };
-                    }
-                }
-                if (videoFormats.includes(format) || format.includes('.webm')) {
-                    return MoveParsedFile(file, Directory.videos);
-                }
-                fs.unlink(filepath, () => {});
-                return { source: file, result: { name: 'Unsupported video format', message: `Could not upload unsupported file (${originalFilename}). Please convert to an .mp4` } };
-            case 'application':
-                if (applicationFormats.includes(format)) {
-                    const val = UploadPdf(file);
-                    if (val) return val;
-                }
-            case 'audio':
-                const components = format.split(';');
-                if (components.length > 1) {
-                    format = components[0];
-                }
-                if (audioFormats.includes(format)) {
-                    return UploadAudio(file, format);
-                }
-                fs.unlink(filepath, () => {});
-                return { source: file, result: { name: 'Unsupported audio format', message: `Could not upload unsupported file (${originalFilename}). Please convert to an .mp3` } };
-            case 'text':
-                if (types[1] == 'csv') {
-                    return UploadCsv(file);
-                }
-        }
-
-        console.log(red(`Ignoring unsupported file (${originalFilename}) with upload type (${type}).`));
-        fs.unlink(filepath, () => {});
-        return { source: file, result: new Error(`Could not upload unsupported file (${originalFilename}) with upload type (${type}).`) };
-    }
-
-    async function UploadPdf(file: File) {
-        const fileKey = (await md5File(file.filepath)) + '.pdf';
-        const textFilename = `${fileKey.substring(0, fileKey.length - 4)}.txt`;
-        if (fExists(fileKey, Directory.pdfs) && fExists(textFilename, Directory.text)) {
-            fs.unlink(file.filepath, () => {});
-            return new Promise<Upload.FileResponse>(res => {
-                const textFilename = `${fileKey.substring(0, fileKey.length - 4)}.txt`;
-                const readStream = createReadStream(serverPathToFile(Directory.text, textFilename));
-                var rawText = '';
-                readStream
-                    .on('data', chunk => (rawText += chunk.toString())) //
-                    .on('end', () => res(resolveExistingFile(file.originalFilename ?? '', fileKey, Directory.pdfs, file.mimetype, undefined, rawText)));
-            });
-        }
-        const dataBuffer = readFileSync(file.filepath);
-        const result: ParsedPDF | any = await parse(dataBuffer).catch((e: any) => e);
-        if (!result.code) {
-            await new Promise<void>((resolve, reject) => {
-                const writeStream = createWriteStream(serverPathToFile(Directory.text, textFilename));
-                writeStream.write(result?.text, error => (error ? reject(error) : resolve()));
+    const parseExifData = async (source: string) => {
+        const image = await request.get(source, { encoding: null });
+        const { /* data, */ error } = await new Promise<{ data: any; error: any }>(resolve => {
+            // eslint-disable-next-line no-new
+            new ExifImage({ image }, (error, data) => {
+                const reason = (error as any)?.code;
+                resolve({ data, error: reason });
             });
-            return MoveParsedFile(file, Directory.pdfs, undefined, result?.text, undefined, fileKey);
-        }
-        return { source: file, result: { name: 'faile pdf pupload', message: `Could not upload (${file.originalFilename}).${result.message}` } };
-    }
-
-    async function UploadCsv(file: File) {
-        const { filepath: sourcePath } = file;
-        // read the file as a string
-        const data = readFileSync(sourcePath, 'utf8');
-        // split the string into an array of lines
-        return MoveParsedFile(file, Directory.csv, undefined, data);
-        // console.log(csvParser(data));
-    }
-
-    const manualSuffixes = ['.webm'];
-
-    async function UploadAudio(file: File, format: string) {
-        const suffix = manualSuffixes.includes(format) ? format : undefined;
-        return MoveParsedFile(file, Directory.audio, suffix);
-    }
-
-    /**
-     * Uploads an image specified by the @param source to Dash's /public/files/
-     * directory, and returns information generated during that upload
-     *
-     * @param {string} source is either the absolute path of an already uploaded image or
-     * the url of a remote image
-     * @param {string} filename dictates what to call the image. If not specified,
-     * the name {@param prefix}_upload_{GUID}
-     * @param {string} prefix is a string prepended to the generated image name in the
-     * event that @param filename is not specified
-     *
-     * @returns {ImageUploadInformation | Error} This method returns
-     * 1) the paths to the uploaded images (plural due to resizing)
-     * 2) the exif data embedded in the image, or the error explaining why exif couldn't be parsed
-     * 3) the size of the image, in bytes (4432130)
-     * 4) the content type of the image, i.e. image/(jpeg | png | ...)
-     */
-    export const UploadImage = async (source: string, filename?: string, prefix: string = ''): Promise<Upload.ImageInformation | Error> => {
-        const metadata = await InspectImage(source);
-        if (metadata instanceof Error) {
-            return { name: metadata.name, message: metadata.message };
-        }
-        const outputFile = filename || metadata.filename || '';
-
-        return UploadInspectedImage(metadata, outputFile, prefix);
+        });
+        return error ? { data: undefined, error } : { data: await exifr.parse(image), error };
     };
-
-    export async function buildFileDirectories() {
-        if (!existsSync(publicDirectory)) {
-            console.error('\nPlease ensure that the following directory exists...\n');
-            console.log(publicDirectory);
-            process.exit(0);
-        }
-        if (!existsSync(filesDirectory)) {
-            console.error('\nPlease ensure that the following directory exists...\n');
-            console.log(filesDirectory);
-            process.exit(0);
-        }
-        const pending = Object.keys(Directory).map(sub => createIfNotExists(`${filesDirectory}/${sub}`));
-        return Promise.all(pending);
-    }
-
-    export interface RequestedImageSize {
-        width: number;
-        height: number;
-        type: string;
-    }
-
-    export interface ImageResizer {
-        width: number;
-        suffix: SizeSuffix;
-    }
-
     /**
      * Based on the url's classification as local or remote, gleans
      * as much information as possible about the specified image
      *
      * @param source is the path or url to the image in question
      */
-    export const InspectImage = async (source: string): Promise<Upload.InspectionResults | Error> => {
-        let rawMatches: RegExpExecArray | null;
+    export const InspectImage = async (sourceIn: string): Promise<Upload.InspectionResults | Error> => {
+        let source = sourceIn;
+        const rawMatches = /^data:image\/([a-z]+);base64,(.*)/.exec(source);
         let filename: string | undefined;
         /**
          * Just more edge case handling: this if clause handles the case where an image onto the canvas that
          * is represented by a base64 encoded data uri, rather than a proper file. We manually write it out
          * to the server and then carry on as if it had been put there by the Formidable form / file parser.
          */
-        if ((rawMatches = /^data:image\/([a-z]+);base64,(.*)/.exec(source)) !== null) {
+        if (rawMatches !== null) {
             const [ext, data] = rawMatches.slice(1, 3);
-            const resolved = (filename = `upload_${Utils.GenerateGuid()}.${ext}`);
+            filename = `upload_${Utils.GenerateGuid()}.${ext}`;
+            const resolved = filename;
             if (usingAzure()) {
-                const response = await AzureManager.UploadBase64ImageBlob(resolved, data);
+                await AzureManager.UploadBase64ImageBlob(resolved, data);
                 source = `${AzureManager.BASE_STRING}/${resolved}`;
             } else {
                 source = `${resolvedServerUrl}${clientPathToFile(Directory.images, resolved)}`;
@@ -438,7 +253,7 @@ export namespace DashUploadUtils {
 
         // Use the request library to parse out file level image information in the headers
         const { headers } = await new Promise<any>((resolve, reject) => {
-            return request.head(resolvedUrl, (error, res) => (error ? reject(error) : resolve(res)));
+            request.head(resolvedUrl, (error, res) => (error ? reject(error) : resolve(res)));
         }).catch(e => {
             console.log('Error processing headers: ', e);
         });
@@ -449,6 +264,7 @@ export namespace DashUploadUtils {
             // Bundle up the information into an object
             return {
                 source,
+                // eslint-disable-next-line radix
                 contentSize: parseInt(headers[size]),
                 contentType: headers[type],
                 nativeWidth,
@@ -462,49 +278,71 @@ export namespace DashUploadUtils {
         }
     };
 
+    async function correctRotation(imgSourcePath: string) {
+        const buffer = fs.readFileSync(imgSourcePath);
+        try {
+            return (await autorotate.rotate(buffer, { quality: 30 })).buffer;
+        } catch (e) {
+            return buffer;
+        }
+    }
+
     /**
-     * Basically just a wrapper around rename, which 'deletes'
-     * the file at the old path and 'moves' it to the new one. For simplicity, the
-     * caller just has to pass in the name of the target directory, and this function
-     * will resolve the actual target path from that.
-     * @param file The file to move
-     * @param destination One of the specific media asset directories into which to move it
-     * @param suffix If the file doesn't have a suffix and you want to provide it one
-     * to appear in the new location
-     */
-    export async function MoveParsedFile(file: formidable.File, destination: Directory, suffix: string | undefined = undefined, text?: string, duration?: number, targetName?: string): Promise<Upload.FileResponse> {
-        const { filepath } = file;
-        let name = targetName ?? path.basename(filepath);
-        suffix && (name += suffix);
-        return new Promise(resolve => {
-            const destinationPath = serverPathToFile(destination, name);
-            rename(filepath, destinationPath, error => {
-                resolve({
-                    source: file,
-                    result: error
-                        ? error
-                        : {
-                              accessPaths: {
-                                  agnostic: getAccessPaths(destination, name),
-                              },
-                              rawText: text,
-                              duration,
-                          },
-                });
-            });
-        });
+     * define the resizers to use
+     * @param ext the extension
+     * @returns an array of resize descriptions
+     */
+    export function imageResampleSizes(ext: string): DashUploadUtils.ImageResizer[] {
+        return [
+            { suffix: SizeSuffix.Original, width: 0 },
+            ...[...(AcceptableMedia.imageFormats.includes(ext.toLowerCase()) ? Object.values(DashUploadUtils.Sizes) : [])].map(({ suffix, width }) => ({
+                width,
+                suffix,
+            })),
+        ];
     }
 
-    export function fExists(name: string, destination: Directory) {
-        const destinationPath = serverPathToFile(destination, name);
-        return existsSync(destinationPath);
-    }
+    /**
+     * outputResizedImages takes in a readable stream and resizes the images according to the sizes defined at the top of this file.
+     *
+     * The new images will be saved to the server with the corresponding prefixes.
+     * @param imgSourcePath file path for image being resized
+     * @param outputFileName the basename (No suffix) of the outputted file.
+     * @param outputDirectory the directory to output to, usually Directory.Images
+     * @returns a map with suffixes as keys and resized filenames as values.
+     */
+    export async function outputResizedImages(imgSourcePath: string, outputFileName: string, outputDirectory: string) {
+        const writtenFiles: { [suffix: string]: string } = {};
+        const sizes = imageResampleSizes(path.extname(outputFileName));
 
-    export function getAccessPaths(directory: Directory, fileName: string) {
-        return {
-            client: clientPathToFile(directory, fileName),
-            server: serverPathToFile(directory, fileName),
+        const imgBuffer = await correctRotation(imgSourcePath);
+        const imgReadStream = new Duplex();
+        imgReadStream.push(imgBuffer);
+        imgReadStream.push(null);
+        const outputPath = (suffix: SizeSuffix) => {
+            writtenFiles[suffix] = InjectSize(outputFileName, suffix);
+            return path.resolve(outputDirectory, writtenFiles[suffix]);
         };
+        await Promise.all(
+            sizes.filter(({ width }) => !width).map(({ suffix }) => 
+                new Promise<void>(res => {
+                    imgReadStream.pipe(createWriteStream(outputPath(suffix))).on('close', res);
+                })
+        )); // prettier-ignore
+
+        return Jimp.read(imgBuffer)
+            .then(async (imgIn: any) => {
+                let img = imgIn;
+                await Promise.all( sizes.filter(({ width }) => width).map(({ width, suffix }) => {
+                    img = img.resize(width, Jimp.AUTO).write(outputPath(suffix));
+                    return img;
+                } )); // prettier-ignore
+                return writtenFiles;
+            })
+            .catch((e: any) => {
+                console.log('ERROR' + e);
+                return writtenFiles;
+            });
     }
 
     /**
@@ -555,119 +393,265 @@ export namespace DashUploadUtils {
             } catch (e) {
                 // input is a blob or other, try reading it to create a metadata source file.
                 const reqSource = request(metadata.source);
-                let readStream: Stream = reqSource instanceof Promise ? await reqSource : reqSource;
+                const readStream: Stream = reqSource instanceof Promise ? await reqSource : reqSource;
                 const readSource = `${prefix}upload_${Utils.GenerateGuid()}.${metadata.contentType.split('/')[1].toLowerCase()}`;
-                await new Promise<void>((res, rej) =>
+                await new Promise<void>((res, rej) => {
                     readStream
                         .pipe(createWriteStream(readSource))
                         .on('close', () => res())
-                        .on('error', () => rej())
-                );
+                        .on('error', () => rej());
+                });
                 writtenFiles = await outputResizedImages(readSource, resolved, pathToDirectory(Directory.images));
-                fs.unlink(readSource, err => console.log("Couldn't unlink temporary image file:" + readSource));
+                fs.unlink(readSource, err => console.log("Couldn't unlink temporary image file:" + readSource, err));
             }
         }
-        for (const suffix of Object.keys(writtenFiles)) {
+        Array.from(Object.keys(writtenFiles)).forEach(suffix => {
             information.accessPaths[suffix] = getAccessPaths(images, writtenFiles[suffix]);
-        }
+        });
         if (isLocal().test(source) && cleanUp) {
             unlinkSync(source);
         }
         return information;
     };
 
-    const bufferConverterRec = (layer: any) => {
-        for (const key of Object.keys(layer)) {
-            const val: any = layer[key];
-            if (val instanceof Buffer) {
-                layer[key] = val.toString();
-            } else if (Array.isArray(val) && typeof val[0] === 'number') {
-                layer[key] = Buffer.from(val).toString();
-            } else if (typeof val === 'object') {
-                bufferConverterRec(val);
-            }
+    /**
+     * Uploads an image specified by the @param source to Dash's /public/files/
+     * directory, and returns information generated during that upload
+     *
+     * @param {string} source is either the absolute path of an already uploaded image or
+     * the url of a remote image
+     * @param {string} filename dictates what to call the image. If not specified,
+     * the name {@param prefix}_upload_{GUID}
+     * @param {string} prefix is a string prepended to the generated image name in the
+     * event that @param filename is not specified
+     *
+     * @returns {ImageUploadInformation | Error} This method returns
+     * 1) the paths to the uploaded images (plural due to resizing)
+     * 2) the exif data embedded in the image, or the error explaining why exif couldn't be parsed
+     * 3) the size of the image, in bytes (4432130)
+     * 4) the content type of the image, i.e. image/(jpeg | png | ...)
+     */
+    export const UploadImage = async (source: string, filename?: string, prefix: string = ''): Promise<Upload.ImageInformation | Error> => {
+        const metadata = await InspectImage(source);
+        if (metadata instanceof Error) {
+            return { name: metadata.name, message: metadata.message };
         }
+        const outputFile = filename || metadata.filename || '';
+
+        return UploadInspectedImage(metadata, outputFile, prefix);
     };
 
-    const parseExifData = async (source: string) => {
-        const image = await request.get(source, { encoding: null });
-        const { data, error } = await new Promise<{ data: any; error: any }>(resolve => {
-            new ExifImage({ image }, (error, data) => {
-                let reason: Opt<string> = undefined;
-                if (error) {
-                    reason = (error as any).code;
-                }
-                resolve({ data, error: reason });
-            });
+    export function uploadYoutube(videoId: string, overwriteId: string): Promise<Upload.FileResponse> {
+        return new Promise<Upload.FileResponse<Upload.FileInformation>>(res => {
+            const name = videoId;
+            const filepath = name.replace(/^-/, '__') + '.mp4';
+            const finalPath = serverPathToFile(Directory.videos, filepath);
+            if (existsSync(finalPath)) {
+                uploadProgress.set(overwriteId, 'computing duration');
+                exec(`yt-dlp -o ${finalPath} "https://www.youtube.com/watch?v=${videoId}" --get-duration`, (error: any, stdout: any /* , stderr: any */) => {
+                    const time = Array.from(stdout.trim().split(':')).reverse();
+                    const duration = (time.length > 2 ? Number(time[2]) * 1000 * 60 : 0) + (time.length > 1 ? Number(time[1]) * 60 : 0) + (time.length > 0 ? Number(time[0]) : 0);
+                    res(resolveExistingFile(name, filepath, Directory.videos, 'video/mp4', duration, undefined));
+                });
+            } else {
+                uploadProgress.set(overwriteId, 'starting download');
+                const ytdlp = spawn(`yt-dlp`, ['-o', filepath, `https://www.youtube.com/watch?v=${videoId}`, '--max-filesize', '100M', '-f', 'mp4']);
+
+                ytdlp.stdout.on('data', (data: any) => uploadProgress.set(overwriteId, data.toString()));
+
+                let errors = '';
+                ytdlp.stderr.on('data', (data: any) => {
+                    uploadProgress.set(overwriteId, 'error:' + data.toString());
+                    errors = data.toString();
+                });
+
+                ytdlp.on('exit', (code: any) => {
+                    if (code) {
+                        res({
+                            source: {
+                                size: 0,
+                                filepath: name,
+                                originalFilename: name,
+                                newFilename: name,
+                                mimetype: 'video',
+                                hashAlgorithm: 'md5',
+                                toJSON: () => ({ newFilename: name, filepath, mimetype: 'video', mtime: new Date(), size: 0, length: 0, originalFilename: name }),
+                            },
+                            result: { name: 'failed youtube query', message: `Could not archive video.  ${code ? errors : uploadProgress.get(videoId)}` },
+                        });
+                    } else {
+                        uploadProgress.set(overwriteId, 'computing duration');
+                        exec(`yt-dlp-o ${filepath} "https://www.youtube.com/watch?v=${videoId}" --get-duration`, (/* error: any, stdout: any, stderr: any */) => {
+                            // const time = Array.from(stdout.trim().split(':')).reverse();
+                            // const duration = (time.length > 2 ? Number(time[2]) * 1000 * 60 : 0) + (time.length > 1 ? Number(time[1]) * 60 : 0) + (time.length > 0 ? Number(time[0]) : 0);
+                            const data = { size: 0, filepath, name, mimetype: 'video', originalFilename: name, newFilename: name, hashAlgorithm: 'md5' as 'md5', type: 'video/mp4' };
+                            const file = { ...data, toJSON: () => ({ ...data, length: 0, filename: data.filepath.replace(/.*\//, ''), mtime: new Date(), toJson: () => undefined as any }) };
+                            MoveParsedFile(file, Directory.videos).then(output => res(output));
+                        });
+                    }
+                });
+            }
         });
-        //data && bufferConverterRec(data);
-        return error ? { data: undefined, error } : { data: await exifr.parse(image), error };
-    };
+    }
+    const manualSuffixes = ['.webm'];
 
-    const { pngs, jpgs, webps, tiffs } = AcceptableMedia;
-    const pngOptions = {
-        compressionLevel: 9,
-        adaptiveFiltering: true,
-        force: true,
-    };
+    async function UploadAudio(file: File, format: string) {
+        const suffix = manualSuffixes.includes(format) ? format : undefined;
+        return MoveParsedFile(file, Directory.audio, suffix);
+    }
 
-    async function correctRotation(imgSourcePath: string) {
-        const buffer = fs.readFileSync(imgSourcePath);
-        try {
-            return (await autorotate.rotate(buffer, { quality: 30 })).buffer;
-        } catch (e) {
-            return buffer;
+    async function UploadPdf(file: File) {
+        const fileKey = (await md5File(file.filepath)) + '.pdf';
+        const textFilename = `${fileKey.substring(0, fileKey.length - 4)}.txt`;
+        if (fExists(fileKey, Directory.pdfs) && fExists(textFilename, Directory.text)) {
+            fs.unlink(file.filepath, () => {});
+            return new Promise<Upload.FileResponse>(res => {
+                const textFilename = `${fileKey.substring(0, fileKey.length - 4)}.txt`;
+                const readStream = createReadStream(serverPathToFile(Directory.text, textFilename));
+                let rawText = '';
+                readStream
+                    .on('data', chunk => {
+                        rawText += chunk.toString();
+                    })
+                    .on('end', () => res(resolveExistingFile(file.originalFilename ?? '', fileKey, Directory.pdfs, file.mimetype, undefined, rawText)));
+            });
         }
+        const dataBuffer = readFileSync(file.filepath);
+        const result: ParsedPDF | any = await parse(dataBuffer).catch((e: any) => e);
+        if (!result.code) {
+            await new Promise<void>((resolve, reject) => {
+                const writeStream = createWriteStream(serverPathToFile(Directory.text, textFilename));
+                writeStream.write(result?.text, error => (error ? reject(error) : resolve()));
+            });
+            return MoveParsedFile(file, Directory.pdfs, undefined, result?.text, undefined, fileKey);
+        }
+        return { source: file, result: { name: 'faile pdf pupload', message: `Could not upload (${file.originalFilename}).${result.message}` } };
     }
 
-    /**
-     * outputResizedImages takes in a readable stream and resizes the images according to the sizes defined at the top of this file.
-     *
-     * The new images will be saved to the server with the corresponding prefixes.
-     * @param imgSourcePath file path for image being resized
-     * @param outputFileName the basename (No suffix) of the outputted file.
-     * @param outputDirectory the directory to output to, usually Directory.Images
-     * @returns a map with suffixes as keys and resized filenames as values.
-     */
-    export async function outputResizedImages(imgSourcePath: string, outputFileName: string, outputDirectory: string) {
-        const writtenFiles: { [suffix: string]: string } = {};
-        const sizes = imageResampleSizes(path.extname(outputFileName));
+    async function UploadCsv(file: File) {
+        const { filepath: sourcePath } = file;
+        // read the file as a string
+        const data = readFileSync(sourcePath, 'utf8');
+        // split the string into an array of lines
+        return MoveParsedFile(file, Directory.csv, undefined, data);
+        // console.log(csvParser(data));
+    }
 
-        const imgBuffer = await correctRotation(imgSourcePath);
-        const imgReadStream = new Duplex();
-        imgReadStream.push(imgBuffer);
-        imgReadStream.push(null);
-        const outputPath = (suffix: SizeSuffix) => path.resolve(outputDirectory, (writtenFiles[suffix] = InjectSize(outputFileName, suffix)));
-        await Promise.all(
-            sizes.filter(({ width }) => !width).map(({ suffix }) => 
-                new Promise<void>(res => imgReadStream.pipe(createWriteStream(outputPath(suffix))).on('close', res))
-        )); // prettier-ignore
+    export async function upload(file: File /* , overwriteGuid?: string */): Promise<Upload.FileResponse> {
+        // const isAzureOn = usingAzure();
+        const { mimetype, filepath, originalFilename } = file;
+        const types = mimetype?.split('/') ?? [];
+        // uploadProgress.set(overwriteGuid ?? name, 'uploading'); // If the client sent a guid it uses to track upload progress, use that guid. Otherwise, use the file's name.
 
-        return Jimp.read(imgBuffer)
-            .then(async (img: any) => {
-                await Promise.all( sizes.filter(({ width }) => width).map(({ width, suffix }) => 
-                    img = img.resize(width, Jimp.AUTO).write(outputPath(suffix))
-                )); // prettier-ignore
-                return writtenFiles;
-            })
-            .catch((e: any) => {
-                console.log('ERROR' + e);
-                return writtenFiles;
-            });
+        const category = types[0];
+        let format = `.${types[1]}`;
+        console.log(green(`Processing upload of file (${originalFilename}) and format (${format}) with upload type (${mimetype}) in category (${category}).`));
+
+        switch (category) {
+            case 'image':
+                if (imageFormats.includes(format)) {
+                    const result = await UploadImage(filepath, basename(filepath));
+                    return { source: file, result };
+                }
+                fs.unlink(filepath, () => {});
+                return { source: file, result: { name: 'Unsupported image format', message: `Could not upload unsupported file (${originalFilename}). Please convert to an .jpg` } };
+            case 'video': {
+                const vidFile = file;
+                if (format.includes('x-matroska')) {
+                    await new Promise(res => {
+                        ffmpeg(vidFile.filepath)
+                            .videoCodec('copy') // this will copy the data instead of reencode it
+                            .save(vidFile.filepath.replace('.mkv', '.mp4'))
+                            .on('end', res)
+                            .on('error', (e: any) => console.log(e));
+                    });
+                    vidFile.filepath = vidFile.filepath.replace('.mkv', '.mp4');
+                    format = '.mp4';
+                }
+                if (format.includes('quicktime')) {
+                    let abort = false;
+                    await new Promise<void>(res => {
+                        ffmpeg.ffprobe(vidFile.filepath, (err: any, metadata: any) => {
+                            if (metadata.streams.some((stream: any) => stream.codec_name === 'hevc')) {
+                                abort = true;
+                            }
+                            res();
+                        });
+                    });
+                    if (abort) {
+                        // bcz: instead of aborting, we could convert the file using the code below to an mp4.  Problem is that this takes a long time and will clog up the server.
+                        // await new Promise(res =>
+                        //     ffmpeg(file.path)
+                        //         .videoCodec('libx264') // this will copy the data instead of reencode it
+                        //         .audioCodec('mp2')
+                        //         .save(vidFile.path.replace('.MOV', '.mp4').replace('.mov', '.mp4'))
+                        //         .on('end', res)
+                        // );
+                        // vidFile.path = vidFile.path.replace('.mov', '.mp4').replace('.MOV', '.mp4');
+                        // format = '.mp4';
+                        fs.unlink(filepath, () => {});
+                        return { source: file, result: { name: 'Unsupported video format', message: `Could not upload unsupported file (${originalFilename}). Please convert to an .mp4` } };
+                    }
+                }
+                if (videoFormats.includes(format) || format.includes('.webm')) {
+                    return MoveParsedFile(vidFile, Directory.videos);
+                }
+                fs.unlink(filepath, () => {});
+                return { source: vidFile, result: { name: 'Unsupported video format', message: `Could not upload unsupported file (${originalFilename}). Please convert to an .mp4` } };
+            }
+            case 'application':
+                if (applicationFormats.includes(format)) {
+                    const val = UploadPdf(file);
+                    if (val) return val;
+                }
+                break;
+            case 'audio': {
+                const components = format.split(';');
+                if (components.length > 1) {
+                    [format] = components;
+                }
+                if (audioFormats.includes(format)) {
+                    return UploadAudio(file, format);
+                }
+                fs.unlink(filepath, () => {});
+                return { source: file, result: { name: 'Unsupported audio format', message: `Could not upload unsupported file (${originalFilename}). Please convert to an .mp3` } };
+            }
+            case 'text':
+                if (types[1] === 'csv') {
+                    return UploadCsv(file);
+                }
+                break;
+            default:
+        }
+
+        console.log(red(`Ignoring unsupported file (${originalFilename}) with upload type (${mimetype}).`));
+        fs.unlink(filepath, () => {});
+        return { source: file, result: new Error(`Could not upload unsupported file (${originalFilename}) with upload type (${mimetype}).`) };
     }
 
-    /**
-     * define the resizers to use
-     * @param ext the extension
-     * @returns an array of resize descriptions
-     */
-    export function imageResampleSizes(ext: string): DashUploadUtils.ImageResizer[] {
-        return [
-            { suffix: SizeSuffix.Original, width: 0 },
-            ...[...(AcceptableMedia.imageFormats.includes(ext.toLowerCase()) ? Object.values(DashUploadUtils.Sizes) : [])].map(({ suffix, width }) => ({
-                width,
-                suffix,
-            })),
-        ];
+    export async function buildFileDirectories() {
+        if (!existsSync(publicDirectory)) {
+            console.error('\nPlease ensure that the following directory exists...\n');
+            console.log(publicDirectory);
+            process.exit(0);
+        }
+        if (!existsSync(filesDirectory)) {
+            console.error('\nPlease ensure that the following directory exists...\n');
+            console.log(filesDirectory);
+            process.exit(0);
+        }
+        const pending = Object.keys(Directory).map(sub => createIfNotExists(`${filesDirectory}/${sub}`));
+        return Promise.all(pending);
+    }
+
+    export interface RequestedImageSize {
+        width: number;
+        height: number;
+        type: string;
+    }
+
+    export interface ImageResizer {
+        width: number;
+        suffix: SizeSuffix;
     }
 }
diff --git a/src/server/Message.ts b/src/server/Message.ts
index 8f0af08bc..03150c841 100644
--- a/src/server/Message.ts
+++ b/src/server/Message.ts
@@ -1,22 +1,47 @@
-import { Point } from "../pen-gestures/ndollar";
-import { Utils } from "../Utils";
+import * as uuid from 'uuid';
+import { Point } from '../pen-gestures/ndollar';
 
+function GenerateDeterministicGuid(seed: string): string {
+    return uuid.v5(seed, uuid.v5.URL);
+}
+// eslint-disable-next-line @typescript-eslint/no-unused-vars
 export class Message<T> {
     private _name: string;
     private _guid: string;
 
     constructor(name: string) {
         this._name = name;
-        this._guid = Utils.GenerateDeterministicGuid(name);
+        this._guid = GenerateDeterministicGuid(name);
     }
 
-    get Name(): string { return this._name; }
-    get Message(): string { return this._guid; }
+    get Name(): string {
+        return this._name;
+    }
+    get Message(): string {
+        return this._guid;
+    }
 }
 
 export enum Types {
-    Number, List, Key, Image, Web, Document, Text, Icon, RichText, DocumentReference,
-    Html, Video, Audio, Ink, PDF, Tuple, Boolean, Script, Templates
+    Number,
+    List,
+    Key,
+    Image,
+    Web,
+    Document,
+    Text,
+    Icon,
+    RichText,
+    DocumentReference,
+    Html,
+    Video,
+    Audio,
+    Ink,
+    PDF,
+    Tuple,
+    Boolean,
+    Script,
+    Templates,
 }
 
 export interface Transferable {
@@ -26,7 +51,9 @@ export interface Transferable {
 }
 
 export enum YoutubeQueryTypes {
-    Channels, SearchVideo, VideoDetails
+    Channels,
+    SearchVideo,
+    VideoDetails,
 }
 
 export interface YoutubeQueryInput {
@@ -45,7 +72,7 @@ export interface Diff extends Reference {
 
 export interface GestureContent {
     readonly points: Array<Point>;
-    readonly bounds: { right: number, left: number, bottom: number, top: number, width: number, height: number };
+    readonly bounds: { right: number; left: number; bottom: number; top: number; width: number; height: number };
     readonly width?: string;
     readonly color?: string;
 }
@@ -73,27 +100,27 @@ export interface RoomMessage {
 }
 
 export namespace MessageStore {
-    export const Foo = new Message<string>("Foo");
-    export const Bar = new Message<string>("Bar");
-    export const SetField = new Message<Transferable>("Set Field"); // send Transferable (no reply)
-    export const GetField = new Message<string>("Get Field"); // send string 'id' get Transferable back
-    export const GetFields = new Message<string[]>("Get Fields"); // send string[] of 'id' get Transferable[] back
-    export const GetDocument = new Message<string>("Get Document");
-    export const DeleteAll = new Message<any>("Delete All");
-    export const ConnectionTerminated = new Message<string>("Connection Terminated");
-
-    export const GesturePoints = new Message<GestureContent>("Gesture Points");
-    export const MobileInkOverlayTrigger = new Message<MobileInkOverlayContent>("Trigger Mobile Ink Overlay");
-    export const UpdateMobileInkOverlayPosition = new Message<UpdateMobileInkOverlayPositionContent>("Update Mobile Ink Overlay Position");
-    export const MobileDocumentUpload = new Message<MobileDocumentUploadContent>("Upload Document From Mobile");
-
-    export const GetRefField = new Message<string>("Get Ref Field");
-    export const GetRefFields = new Message<string[]>("Get Ref Fields");
-    export const UpdateField = new Message<Diff>("Update Ref Field");
-    export const CreateField = new Message<Reference>("Create Ref Field");
-    export const YoutubeApiQuery = new Message<YoutubeQueryInput>("Youtube Api Query");
-    export const DeleteField = new Message<string>("Delete field");
-    export const DeleteFields = new Message<string[]>("Delete fields");
-
-    export const UpdateStats = new Message<string>("updatestats");
+    export const Foo = new Message<string>('Foo');
+    export const Bar = new Message<string>('Bar');
+    export const SetField = new Message<Transferable>('Set Field'); // send Transferable (no reply)
+    export const GetField = new Message<string>('Get Field'); // send string 'id' get Transferable back
+    export const GetFields = new Message<string[]>('Get Fields'); // send string[] of 'id' get Transferable[] back
+    export const GetDocument = new Message<string>('Get Document');
+    export const DeleteAll = new Message<any>('Delete All');
+    export const ConnectionTerminated = new Message<string>('Connection Terminated');
+
+    export const GesturePoints = new Message<GestureContent>('Gesture Points');
+    export const MobileInkOverlayTrigger = new Message<MobileInkOverlayContent>('Trigger Mobile Ink Overlay');
+    export const UpdateMobileInkOverlayPosition = new Message<UpdateMobileInkOverlayPositionContent>('Update Mobile Ink Overlay Position');
+    export const MobileDocumentUpload = new Message<MobileDocumentUploadContent>('Upload Document From Mobile');
+
+    export const GetRefField = new Message<string>('Get Ref Field');
+    export const GetRefFields = new Message<string[]>('Get Ref Fields');
+    export const UpdateField = new Message<Diff>('Update Ref Field');
+    export const CreateField = new Message<Reference>('Create Ref Field');
+    export const YoutubeApiQuery = new Message<YoutubeQueryInput>('Youtube Api Query');
+    export const DeleteField = new Message<string>('Delete field');
+    export const DeleteFields = new Message<string[]>('Delete fields');
+
+    export const UpdateStats = new Message<string>('updatestats');
 }
diff --git a/src/server/RouteManager.ts b/src/server/RouteManager.ts
index 540bca776..d8e0455f6 100644
--- a/src/server/RouteManager.ts
+++ b/src/server/RouteManager.ts
@@ -1,9 +1,9 @@
 import { cyan, green, red } from 'colors';
 import { Express, Request, Response } from 'express';
-import { AdminPriviliges } from '.';
 import { Utils } from '../Utils';
-import { DashUserModel } from './authentication/DashUserModel';
 import RouteSubscriber from './RouteSubscriber';
+import { AdminPrivileges } from './SocketData';
+import { DashUserModel } from './authentication/DashUserModel';
 
 export enum Method {
     GET,
@@ -21,6 +21,34 @@ export type SecureHandler = (core: AuthorizedCore) => any | Promise<any>;
 export type PublicHandler = (core: CoreArguments) => any | Promise<any>;
 export type ErrorHandler = (core: CoreArguments & { error: any }) => any | Promise<any>;
 
+export const STATUS = {
+    OK: 200,
+    BAD_REQUEST: 400,
+    EXECUTION_ERROR: 500,
+    PERMISSION_DENIED: 403,
+};
+
+export function _error(res: Response, message: string, error?: any) {
+    console.error(message, error);
+    res.statusMessage = message;
+    res.status(STATUS.EXECUTION_ERROR).send(error);
+}
+
+export function _success(res: Response, body: any) {
+    res.status(STATUS.OK).send(body);
+}
+
+export function _invalid(res: Response, message: string) {
+    res.statusMessage = message;
+    res.status(STATUS.BAD_REQUEST).send();
+}
+
+export function _permissionDenied(res: Response, message?: string) {
+    if (message) {
+        res.statusMessage = message;
+    }
+    res.status(STATUS.PERMISSION_DENIED).send(`Permission Denied! ${message}`);
+}
 export interface RouteInitializer {
     method: Method;
     subscription: string | RouteSubscriber | (string | RouteSubscriber)[];
@@ -71,7 +99,7 @@ export default class RouteManager {
                 console.log('please remove all duplicate routes before continuing');
             }
             if (malformedCount) {
-                console.log(`please ensure all routes adhere to ^\/$|^\/[A-Za-z]+(\/\:[A-Za-z?_]+)*$`);
+                console.log(`please ensure all routes adhere to ^/$|^/[A-Za-z]+(/:[A-Za-z?_]+)*$`);
             }
             process.exit(1);
         } else {
@@ -94,7 +122,7 @@ export default class RouteManager {
         typeof initializer.subscription === 'string' && RouteManager.routes.push(initializer.subscription);
         initializer.subscription instanceof RouteSubscriber && RouteManager.routes.push(initializer.subscription.root);
         initializer.subscription instanceof Array &&
-            initializer.subscription.map(sub => {
+            initializer.subscription.forEach(sub => {
                 typeof sub === 'string' && RouteManager.routes.push(sub);
                 sub instanceof RouteSubscriber && RouteManager.routes.push(sub.root);
             });
@@ -120,23 +148,23 @@ export default class RouteManager {
             };
             if (user) {
                 if (requireAdmin && isRelease && process.env.PASSWORD) {
-                    if (AdminPriviliges.get(user.id)) {
-                        AdminPriviliges.delete(user.id);
+                    if (AdminPrivileges.get(user.id)) {
+                        AdminPrivileges.delete(user.id);
                     } else {
-                        return res.redirect(`/admin/${req.originalUrl.substring(1).replace('/', ':')}`);
+                        res.redirect(`/admin/${req.originalUrl.substring(1).replace('/', ':')}`);
+                        return;
                     }
                 }
                 await tryExecute(secureHandler, { ...core, user });
-            } else {
-                //req.session!.target = target;
-                if (publicHandler) {
-                    await tryExecute(publicHandler, core);
-                    if (!res.headersSent) {
-                        // res.redirect("/login");
-                    }
-                } else {
-                    res.redirect('/login');
+            }
+            // req.session!.target = target;
+            else if (publicHandler) {
+                await tryExecute(publicHandler, core);
+                if (!res.headersSent) {
+                    // res.redirect("/login");
                 }
+            } else {
+                res.redirect('/login');
             }
             setTimeout(() => {
                 if (!res.headersSent) {
@@ -153,7 +181,7 @@ export default class RouteManager {
             } else {
                 route = subscriber.build;
             }
-            if (!/^\/$|^\/[A-Za-z\*]+(\/\:[A-Za-z?_\*]+)*$/g.test(route)) {
+            if (!/^\/$|^\/[A-Za-z*]+(\/:[A-Za-z?_*]+)*$/g.test(route)) {
                 this.failedRegistrations.push({
                     reason: RegistrationError.Malformed,
                     route,
@@ -180,6 +208,7 @@ export default class RouteManager {
                     case Method.POST:
                         this.server.post(route, supervised);
                         break;
+                    default:
                 }
             }
         };
@@ -190,32 +219,3 @@ export default class RouteManager {
         }
     };
 }
-
-export const STATUS = {
-    OK: 200,
-    BAD_REQUEST: 400,
-    EXECUTION_ERROR: 500,
-    PERMISSION_DENIED: 403,
-};
-
-export function _error(res: Response, message: string, error?: any) {
-    console.error(message, error);
-    res.statusMessage = message;
-    res.status(STATUS.EXECUTION_ERROR).send(error);
-}
-
-export function _success(res: Response, body: any) {
-    res.status(STATUS.OK).send(body);
-}
-
-export function _invalid(res: Response, message: string) {
-    res.statusMessage = message;
-    res.status(STATUS.BAD_REQUEST).send();
-}
-
-export function _permission_denied(res: Response, message?: string) {
-    if (message) {
-        res.statusMessage = message;
-    }
-    res.status(STATUS.PERMISSION_DENIED).send(`Permission Denied! ${message}`);
-}
diff --git a/src/server/SocketData.ts b/src/server/SocketData.ts
new file mode 100644
index 000000000..e857996e5
--- /dev/null
+++ b/src/server/SocketData.ts
@@ -0,0 +1,35 @@
+import { Socket } from 'socket.io';
+import * as path from 'path';
+
+export const timeMap: { [id: string]: number } = {};
+export const userOperations = new Map<string, number>();
+export const socketMap = new Map<Socket, string>();
+
+export const publicDirectory = path.resolve(__dirname, 'public');
+export const filesDirectory = path.resolve(publicDirectory, 'files');
+
+export const AdminPrivileges: Map<string, boolean> = new Map();
+
+export const resolvedPorts: { server: number; socket: number } = { server: 1050, socket: 4321 };
+
+export enum Directory {
+    parsed_files = 'parsed_files',
+    images = 'images',
+    videos = 'videos',
+    pdfs = 'pdfs',
+    text = 'text',
+    audio = 'audio',
+    csv = 'csv',
+}
+
+export function serverPathToFile(directory: Directory, filename: string) {
+    return path.normalize(`${filesDirectory}/${directory}/${filename}`);
+}
+
+export function pathToDirectory(directory: Directory) {
+    return path.normalize(`${filesDirectory}/${directory}`);
+}
+
+export function clientPathToFile(directory: Directory, filename: string) {
+    return `/files/${directory}/${filename}`;
+}
diff --git a/src/server/apis/google/GoogleApiServerUtils.ts b/src/server/apis/google/GoogleApiServerUtils.ts
index 3940b7a3d..55e5fd7c0 100644
--- a/src/server/apis/google/GoogleApiServerUtils.ts
+++ b/src/server/apis/google/GoogleApiServerUtils.ts
@@ -1,9 +1,9 @@
-import { google } from 'googleapis';
-import { OAuth2Client, Credentials, OAuth2ClientOptions } from 'google-auth-library';
-import { Opt } from '../../../fields/Doc';
 import { GaxiosResponse } from 'gaxios';
-import * as request from 'request-promise';
+import { Credentials, OAuth2Client, OAuth2ClientOptions } from 'google-auth-library';
+import { google } from 'googleapis';
 import * as qs from 'query-string';
+import * as request from 'request-promise';
+import { Opt } from '../../../fields/Doc';
 import { Database } from '../../database';
 import { GoogleCredentialsLoader } from './CredentialsLoader';
 
diff --git a/src/server/authentication/AuthenticationManager.ts b/src/server/authentication/AuthenticationManager.ts
index 9c1525df0..b5d1dba28 100644
--- a/src/server/authentication/AuthenticationManager.ts
+++ b/src/server/authentication/AuthenticationManager.ts
@@ -6,7 +6,7 @@ import './Passport';
 import * as async from 'async';
 import * as nodemailer from 'nodemailer';
 import * as c from 'crypto';
-import { emptyFunction, Utils } from '../../Utils';
+import { emptyFunction, Utils } from '../../ClientUtils';
 import { MailOptions } from 'nodemailer/lib/stream-transport';
 import { check, validationResult } from 'express-validator';
 
diff --git a/src/server/authentication/DashUserModel.ts b/src/server/authentication/DashUserModel.ts
index dbb7a79ed..3bc21ecb6 100644
--- a/src/server/authentication/DashUserModel.ts
+++ b/src/server/authentication/DashUserModel.ts
@@ -1,9 +1,8 @@
-//@ts-ignore
 import * as bcrypt from 'bcrypt-nodejs';
-//@ts-ignore
 import * as mongoose from 'mongoose';
 import { Utils } from '../../Utils';
 
+type comparePasswordFunction = (candidatePassword: string, cb: (err: any, isMatch: any) => void) => void;
 export type DashUserModel = mongoose.Document & {
     email: String;
     password: string;
@@ -26,8 +25,6 @@ export type DashUserModel = mongoose.Document & {
     comparePassword: comparePasswordFunction;
 };
 
-type comparePasswordFunction = (candidatePassword: string, cb: (err: any, isMatch: any) => void) => void;
-
 export type AuthToken = {
     accessToken: string;
     kind: string;
@@ -75,16 +72,19 @@ userSchema.pre('save', function save(next) {
         bcrypt.hash(
             user.password,
             salt,
-            () => void {},
+            () => {},
             (err: mongoose.Error, hash: string) => {
                 if (err) {
                     return next(err);
                 }
                 user.password = hash;
                 next();
+                return undefined;
             }
         );
+        return undefined;
     });
+    return undefined;
 });
 
 const comparePassword: comparePasswordFunction = function (this: DashUserModel, candidatePassword, cb) {
diff --git a/src/server/database.ts b/src/server/database.ts
index 3a087ce38..3a28dc87e 100644
--- a/src/server/database.ts
+++ b/src/server/database.ts
@@ -9,6 +9,7 @@ import { Transferable } from './Message';
 import { Upload } from './SharedMediaTypes';
 
 export namespace Database {
+    // eslint-disable-next-line import/no-mutable-exports
     export let disconnect: Function;
 
     class DocSchema implements mongodb.BSON.Document {
@@ -30,7 +31,10 @@ export namespace Database {
     export async function tryInitializeConnection() {
         try {
             const { connection } = mongoose;
-            disconnect = async () => new Promise<any>(resolve => connection.close().then(resolve));
+            disconnect = async () =>
+                new Promise<any>(resolve => {
+                    connection.close().then(resolve);
+                });
             if (connection.readyState === ConnectionStates.disconnected) {
                 await new Promise<void>((resolve, reject) => {
                     connection.on('error', reject);
@@ -39,7 +43,7 @@ export namespace Database {
                         resolve();
                     });
                     mongoose.connect(url, {
-                        //useNewUrlParser: true,
+                        // useNewUrlParser: true,
                         dbName: schema,
                         // reconnectTries: Number.MAX_VALUE,
                         // reconnectInterval: 1000,
@@ -81,8 +85,8 @@ export namespace Database {
                 const collection = this.db.collection<DocSchema>(collectionName);
                 const prom = this.currentWrites[id];
                 let newProm: Promise<void>;
-                const run = (): Promise<void> => {
-                    return new Promise<void>(resolve => {
+                const run = (): Promise<void> =>
+                    new Promise<void>(resolve => {
                         collection
                             .updateOne({ _id: id }, value, { upsert })
                             .then(res => {
@@ -96,13 +100,12 @@ export namespace Database {
                                 console.log('MOngo UPDATE ONE ERROR:', error);
                             });
                     });
-                };
                 newProm = prom ? prom.then(run) : run();
                 this.currentWrites[id] = newProm;
                 return newProm;
-            } else {
-                this.onConnect.push(() => this.update(id, value, callback, upsert, collectionName));
             }
+            this.onConnect.push(() => this.update(id, value, callback, upsert, collectionName));
+            return undefined;
         }
 
         public replace(id: string, value: any, callback: (err: mongodb.MongoError, res: mongodb.UpdateResult<mongodb.Document>) => void, upsert = true, collectionName = DocumentsCollection) {
@@ -110,8 +113,8 @@ export namespace Database {
                 const collection = this.db.collection<DocSchema>(collectionName);
                 const prom = this.currentWrites[id];
                 let newProm: Promise<void>;
-                const run = (): Promise<void> => {
-                    return new Promise<void>(resolve => {
+                const run = (): Promise<void> =>
+                    new Promise<void>(resolve => {
                         collection.replaceOne({ _id: id }, value, { upsert }).then(res => {
                             if (this.currentWrites[id] === newProm) {
                                 delete this.currentWrites[id];
@@ -120,7 +123,6 @@ export namespace Database {
                             callback(undefined as any, res as any);
                         });
                     });
-                };
                 newProm = prom ? prom.then(run) : run();
                 this.currentWrites[id] = newProm;
             } else {
@@ -132,8 +134,10 @@ export namespace Database {
             const cursor = this.db?.listCollections();
             const collectionNames: string[] = [];
             if (cursor) {
+                // eslint-disable-next-line no-await-in-loop
                 while (await cursor.hasNext()) {
-                    const collection: any = await cursor.next();
+                    // eslint-disable-next-line no-await-in-loop
+                    const collection = await cursor.next();
                     collection && collectionNames.push(collection.name);
                 }
             }
@@ -141,26 +145,30 @@ export namespace Database {
         }
 
         public delete(query: any, collectionName?: string): Promise<mongodb.DeleteResult>;
+        // eslint-disable-next-line no-dupe-class-members
         public delete(id: string, collectionName?: string): Promise<mongodb.DeleteResult>;
-        public delete(id: any, collectionName = DocumentsCollection) {
+        // eslint-disable-next-line no-dupe-class-members
+        public delete(idIn: any, collectionName = DocumentsCollection) {
+            let id = idIn;
             if (typeof id === 'string') {
                 id = { _id: id };
             }
             if (this.db) {
-                const db = this.db;
-                return new Promise(res =>
-                    db
-                        .collection(collectionName)
+                const { db } = this;
+                return new Promise(res => {
+                    db.collection(collectionName)
                         .deleteMany(id)
-                        .then(result => res(result))
-                );
-            } else {
-                return new Promise(res => this.onConnect.push(() => res(this.delete(id, collectionName))));
+                        .then(result => res(result));
+                });
             }
+            return new Promise(res => {
+                this.onConnect.push(() => res(this.delete(id, collectionName)));
+            });
         }
 
         public async dropSchema(...targetSchemas: string[]): Promise<any> {
             const executor = async (database: mongodb.Db) => {
+                // eslint-disable-next-line no-use-before-define
                 const existing = await Instance.getCollectionNames();
                 let valid: string[];
                 if (targetSchemas.length) {
@@ -173,12 +181,13 @@ export namespace Database {
             };
             if (this.db) {
                 return executor(this.db);
-            } else {
-                this.onConnect.push(() => this.db && executor(this.db));
             }
+            this.onConnect.push(() => this.db && executor(this.db));
+            return undefined;
         }
 
-        public async insert(value: any, collectionName = DocumentsCollection) {
+        public async insert(valueIn: any, collectionName = DocumentsCollection) {
+            const value = valueIn;
             if (this.db && value !== null) {
                 if ('id' in value) {
                     value._id = value.id;
@@ -188,36 +197,36 @@ export namespace Database {
                 const collection = this.db.collection<DocSchema>(collectionName);
                 const prom = this.currentWrites[id];
                 let newProm: Promise<void>;
-                const run = (): Promise<void> => {
-                    return new Promise<void>(resolve => {
+                const run = (): Promise<void> =>
+                    new Promise<void>(resolve => {
                         collection
                             .insertOne(value)
-                            .then(res => {
+                            .then(() => {
                                 if (this.currentWrites[id] === newProm) {
                                     delete this.currentWrites[id];
                                 }
                                 resolve();
                             })
-                            .catch(err => {
-                                console.log('Mongo INSERT ERROR: ', err);
-                            });
+                            .catch(err => console.log('Mongo INSERT ERROR: ', err));
                     });
-                };
                 newProm = prom ? prom.then(run) : run();
                 this.currentWrites[id] = newProm;
                 return newProm;
-            } else if (value !== null) {
+            }
+            if (value !== null) {
                 this.onConnect.push(() => this.insert(value, collectionName));
             }
+            return undefined;
         }
 
         public getDocument(id: string, fn: (result?: Transferable) => void, collectionName = DocumentsCollection) {
             if (this.db) {
                 const collection = this.db.collection<DocSchema>(collectionName);
-                collection.findOne({ _id: id }).then(result => {
+                collection.findOne({ _id: id }).then(resultIn => {
+                    const result = resultIn;
                     if (result) {
                         result.id = result._id;
-                        //delete result._id;
+                        // delete result._id;
                         fn(result as any);
                     } else {
                         fn(undefined);
@@ -235,7 +244,8 @@ export namespace Database {
                     .find({ _id: { $in: ids } })
                     .toArray();
                 fn(
-                    found.map((doc: any) => {
+                    found.map((docIn: any) => {
+                        const doc = docIn;
                         doc.id = doc._id;
                         delete doc._id;
                         return doc;
@@ -253,24 +263,26 @@ export namespace Database {
                     const count = Math.min(ids.length, 1000);
                     const index = ids.length - count;
                     const fetchIds = ids.splice(index, count).filter(id => !visited.has(id));
-                    if (!fetchIds.length) {
-                        continue;
-                    }
-                    const docs = await new Promise<{ [key: string]: any }[]>(res => this.getDocuments(fetchIds, res, collectionName));
-                    for (const doc of docs) {
-                        const id = doc.id;
-                        visited.add(id);
-                        ids.push(...(await fn(doc)));
+                    if (fetchIds.length) {
+                        // eslint-disable-next-line no-await-in-loop
+                        const docs = await new Promise<{ [key: string]: any }[]>(res => {
+                            this.getDocuments(fetchIds, res, collectionName);
+                        });
+                        docs.forEach(async doc => {
+                            const { id } = doc;
+                            visited.add(id);
+                            ids.push(...(await fn(doc)));
+                        });
                     }
                 }
-            } else {
-                return new Promise(res => {
-                    this.onConnect.push(() => {
-                        this.visit(ids, fn, collectionName);
-                        res();
-                    });
-                });
+                return undefined;
             }
+            return new Promise(res => {
+                this.onConnect.push(() => {
+                    this.visit(ids, fn, collectionName);
+                    res();
+                });
+            });
         }
 
         public query(query: { [key: string]: any }, projection?: { [key: string]: 0 | 1 }, collectionName = DocumentsCollection): Promise<mongodb.FindCursor> {
@@ -280,36 +292,31 @@ export namespace Database {
                     cursor = cursor.project(projection);
                 }
                 return Promise.resolve<mongodb.FindCursor>(cursor);
-            } else {
-                return new Promise<mongodb.FindCursor>(res => {
-                    this.onConnect.push(() => res(this.query(query, projection, collectionName)));
-                });
             }
+            return new Promise<mongodb.FindCursor>(res => {
+                this.onConnect.push(() => {
+                    res(this.query(query, projection, collectionName));
+                });
+            });
         }
 
         public updateMany(query: any, update: any, collectionName = DocumentsCollection) {
             if (this.db) {
-                const db = this.db;
-                return new Promise<mongodb.UpdateResult>(res =>
-                    db
-                        .collection(collectionName)
+                const { db } = this;
+                return new Promise<mongodb.UpdateResult>(res => {
+                    db.collection(collectionName)
                         .updateMany(query, update)
                         .then(result => res(result))
-                        .catch(error => {
-                            console.log('Mongo INSERT MANY ERROR:', error);
-                        })
-                );
-            } else {
-                return new Promise<mongodb.UpdateResult>(res => {
-                    this.onConnect.push(() =>
-                        this.updateMany(query, update, collectionName)
-                            .then(res)
-                            .catch(error => {
-                                console.log('Mongo UPDATAE MANY ERROR: ', error);
-                            })
-                    );
+                        .catch(error => console.log('Mongo INSERT MANY ERROR:', error));
                 });
             }
+            return new Promise<mongodb.UpdateResult>(res => {
+                this.onConnect.push(() =>
+                    this.updateMany(query, update, collectionName)
+                        .then(res)
+                        .catch(error => console.log('Mongo UPDATAE MANY ERROR: ', error))
+                );
+            });
         }
 
         public print() {
@@ -375,9 +382,7 @@ export namespace Database {
          * Checks to see if an image with the given @param contentSize
          * already exists in the aux database, i.e. has already been downloaded from Google Photos.
          */
-        export const QueryUploadHistory = async (contentSize: number) => {
-            return SanitizedSingletonQuery<Upload.ImageInformation>({ contentSize }, AuxiliaryCollections.GooglePhotosUploadHistory);
-        };
+        export const QueryUploadHistory = async (contentSize: number) => SanitizedSingletonQuery<Upload.ImageInformation>({ contentSize }, AuxiliaryCollections.GooglePhotosUploadHistory);
 
         /**
          * Records the uploading of the image with the given @param information,
@@ -405,28 +410,25 @@ export namespace Database {
              * Retrieves the credentials associaed with @param userId
              * and optionally removes their database id according to @param removeId.
              */
-            export const Fetch = async (userId: string, removeId = true): Promise<Opt<StoredCredentials>> => {
-                return SanitizedSingletonQuery<StoredCredentials>({ userId }, AuxiliaryCollections.GoogleAccess, removeId);
-            };
+            export const Fetch = async (userId: string, removeId = true): Promise<Opt<StoredCredentials>> => SanitizedSingletonQuery<StoredCredentials>({ userId }, AuxiliaryCollections.GoogleAccess, removeId);
 
             /**
              * Writes the @param enrichedCredentials to the database, associated
              * with @param userId for later retrieval and updating.
              */
-            export const Write = async (userId: string, enrichedCredentials: GoogleApiServerUtils.EnrichedCredentials) => {
-                return Instance.insert({ userId, canAccess: [], ...enrichedCredentials }, AuxiliaryCollections.GoogleAccess);
-            };
+            export const Write = async (userId: string, enrichedCredentials: GoogleApiServerUtils.EnrichedCredentials) => Instance.insert({ userId, canAccess: [], ...enrichedCredentials }, AuxiliaryCollections.GoogleAccess);
 
             /**
-             * Updates the @param access_token and @param expiry_date fields
+             * Updates the @param accessToken and @param expiryDate fields
              * in the stored credentials associated with @param userId.
              */
-            export const Update = async (userId: string, access_token: string, expiry_date: number) => {
+            export const Update = async (userId: string, accessToken: string, expiryDate: number) => {
                 const entry = await Fetch(userId, false);
                 if (entry) {
-                    const parameters = { $set: { access_token, expiry_date } };
+                    const parameters = { $set: { access_token: accessToken, expiry_date: expiryDate } };
                     return Instance.update(entry._id, parameters, emptyFunction, true, AuxiliaryCollections.GoogleAccess);
                 }
+                return undefined;
             };
 
             /**
diff --git a/src/server/index.ts b/src/server/index.ts
index 47c37c9f0..5a86f36d9 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -1,9 +1,10 @@
-import * as dotenv from 'dotenv';
 import { yellow } from 'colors';
+import * as dotenv from 'dotenv';
 import * as mobileDetect from 'mobile-detect';
 import * as path from 'path';
 import * as qs from 'query-string';
-import { log_execution } from './ActionUtilities';
+import { logExecution } from './ActionUtilities';
+import { AdminPrivileges, resolvedPorts } from './SocketData';
 import DataVizManager from './ApiManagers/DataVizManager';
 import DeleteManager from './ApiManagers/DeleteManager';
 import DownloadManager from './ApiManagers/DownloadManager';
@@ -24,13 +25,10 @@ import { Database } from './database';
 import { Logger } from './ProcessFactory';
 import RouteManager, { Method, PublicHandler } from './RouteManager';
 import RouteSubscriber from './RouteSubscriber';
-import initializeServer, { resolvedPorts } from './server_Initialization';
+import initializeServer from './server_Initialization';
 dotenv.config();
-export const AdminPriviliges: Map<string, boolean> = new Map();
 export const onWindows = process.platform === 'win32';
 export let sessionAgent: AppliedSessionAgent;
-export const publicDirectory = path.resolve(__dirname, 'public');
-export const filesDirectory = path.resolve(publicDirectory, 'files');
 
 /**
  * These are the functions run before the server starts
@@ -45,7 +43,7 @@ async function preliminaryFunctions() {
     SSL.loadCredentials();
     GoogleApiServerUtils.processProjectCredentials();
     if (process.env.DB !== 'MEM') {
-        await log_execution({
+        await logExecution({
             startMessage: 'attempting to initialize mongodb connection',
             endMessage: 'connection outcome determined',
             action: Database.tryInitializeConnection,
@@ -136,7 +134,7 @@ function routeSetter({ isRelease, addSupervisedRoute, logRegistrationOutcome }:
             const { previous_target } = req.params;
             let redirect: string;
             if (password === PASSWORD) {
-                AdminPriviliges.set(id, true);
+                AdminPrivileges.set(id, true);
                 redirect = `/${previous_target.replace(':', '/')}`;
             } else {
                 redirect = `/admin/${previous_target}`;
@@ -174,7 +172,7 @@ function routeSetter({ isRelease, addSupervisedRoute, logRegistrationOutcome }:
  * the main monitor (master) thread.
  */
 export async function launchServer() {
-    await log_execution({
+    await logExecution({
         startMessage: '\nstarting execution of preliminary functions',
         endMessage: 'completed preliminary functions\n',
         action: preliminaryFunctions,
diff --git a/src/server/remapUrl.ts b/src/server/remapUrl.ts
index b8e17ec66..ca7ca241f 100644
--- a/src/server/remapUrl.ts
+++ b/src/server/remapUrl.ts
@@ -1,58 +1,69 @@
-import { Database } from "./database";
-import { resolvedPorts } from "./server_Initialization";
+import { URL } from 'url';
+import { Database } from './database';
+import { resolvedPorts } from './SocketData';
 
-//npx ts-node src/server/remapUrl.ts
+// npx ts-node src/server/remapUrl.ts
 
 const suffixMap: { [type: string]: true } = {
-    "video": true,
-    "pdf": true,
-    "audio": true,
-    "web": true,
-    "image": true,
-    "map": true,
+    video: true,
+    pdf: true,
+    audio: true,
+    web: true,
+    image: true,
+    map: true,
 };
 
 async function update() {
-    await new Promise(res => setTimeout(res, 10));
-    console.log("update");
+    await new Promise(res => {
+        setTimeout(res, 10);
+    });
+    console.log('update');
     const cursor = await Database.Instance.query({});
-    console.log("Cleared");
+    console.log('Cleared');
     const updates: [string, any][] = [];
     function updateDoc(doc: any) {
-        if (doc.__type !== "Doc") {
+        if (doc.__type !== 'Doc') {
             return;
         }
-        const fields = doc.fields;
+        const { fields } = doc;
         if (!fields) {
             return;
         }
-        const update: any = {
-        };
+        const updated: any = {};
         let dynfield = false;
-        for (const key in fields) {
+        Array.from(Object.keys(fields)).forEach(key => {
             const value = fields[key];
             if (value && value.__type && suffixMap[value.__type]) {
                 const url = new URL(value.url);
-                if (url.href.includes("localhost") && url.href.includes("Bill")) {
+                if (url.href.includes('localhost') && url.href.includes('Bill')) {
                     dynfield = true;
 
-                    update.$set = { ["fields." + key + ".url"]: `${url.protocol}//dash-web.eastus2.cloudapp.azure.com:${resolvedPorts.server}${url.pathname}` };
+                    updated.$set = { ['fields.' + key + '.url']: `${url.protocol}//dash-web.eastus2.cloudapp.azure.com:${resolvedPorts.server}${url.pathname}` };
                 }
             }
-        }
+        });
         if (dynfield) {
-            updates.push([doc._id, update]);
+            updates.push([doc._id, updated]);
         }
     }
     await cursor.forEach(updateDoc);
-    await Promise.all(updates.map(doc => {
-        console.log(doc[0], doc[1]);
-        return new Promise<void>(res => Database.Instance.update(doc[0], doc[1], () => {
-            console.log("wrote " + JSON.stringify(doc[1]));
-            res();
-        }, false));
-    }));
-    console.log("Done");
+    await Promise.all(
+        updates.map(doc => {
+            console.log(doc[0], doc[1]);
+            return new Promise<void>(res => {
+                Database.Instance.update(
+                    doc[0],
+                    doc[1],
+                    () => {
+                        console.log('wrote ' + JSON.stringify(doc[1]));
+                        res();
+                    },
+                    false
+                );
+            });
+        })
+    );
+    console.log('Done');
     // await Promise.all(updates.map(update => {
     //     return limit(() => Search.updateDocument(update));
     // }));
diff --git a/src/server/server_Initialization.ts b/src/server/server_Initialization.ts
index 2d52ea906..8db7e9933 100644
--- a/src/server/server_Initialization.ts
+++ b/src/server/server_Initialization.ts
@@ -1,73 +1,59 @@
 import * as bodyParser from 'body-parser';
+import * as brotli from 'brotli';
 import { blue, yellow } from 'colors';
+import * as flash from 'connect-flash';
+import * as MongoStoreConnect from 'connect-mongo';
 import * as cors from 'cors';
 import * as express from 'express';
+import * as expressFlash from 'express-flash';
 import * as session from 'express-session';
 import { createServer } from 'https';
 import * as passport from 'passport';
 import * as request from 'request';
 import * as webpack from 'webpack';
 import * as wdm from 'webpack-dev-middleware';
+// eslint-disable-next-line import/no-extraneous-dependencies
 import * as whm from 'webpack-hot-middleware';
 import * as zlib from 'zlib';
-import { publicDirectory } from '.';
+import * as config from '../../webpack.config.js';
 import { logPort } from './ActionUtilities';
+import RouteManager from './RouteManager';
+import RouteSubscriber from './RouteSubscriber';
+import { publicDirectory, resolvedPorts } from './SocketData';
 import { SSL } from './apis/google/CredentialsLoader';
 import { getForgot, getLogin, getLogout, getReset, getSignup, postForgot, postLogin, postReset, postSignup } from './authentication/AuthenticationManager';
 import { Database } from './database';
-import RouteManager from './RouteManager';
-import RouteSubscriber from './RouteSubscriber';
 import { WebSocket } from './websocket';
-import * as expressFlash from 'express-flash';
-import * as flash from 'connect-flash';
-import * as brotli from 'brotli';
-import * as MongoStoreConnect from 'connect-mongo';
-import * as config from '../../webpack.config';
 
 /* RouteSetter is a wrapper around the server that prevents the server
    from being exposed. */
 export type RouteSetter = (server: RouteManager) => void;
-//export let disconnect: Function;
+// export let disconnect: Function;
 
-export let resolvedPorts: { server: number; socket: number } = { server: 1050, socket: 4321 };
+// eslint-disable-next-line import/no-mutable-exports
 export let resolvedServerUrl: string;
 
-export default async function InitializeServer(routeSetter: RouteSetter) {
-    const isRelease = determineEnvironment();
-    const app = buildWithMiddleware(express());
-    const compiler = webpack(config as any);
-
-    // route table managed by express.  routes are tested sequentially against each of these map rules.  when a match is found, the handler is called to process the request
-    app.use(wdm(compiler, { publicPath: config.output.publicPath }));
-    app.use(whm(compiler));
-    app.get(new RegExp(/^\/+$/), (req, res) => res.redirect(req.user ? '/home' : '/login')); // target urls that consist of one or more '/'s with nothing in between
-    app.use(express.static(publicDirectory, { setHeaders: res => res.setHeader('Access-Control-Allow-Origin', '*') })); //all urls that start with dash's public directory: /files/  (e.g., /files/images, /files/audio, etc)
-    app.use(cors({ origin: (_origin: any, callback: any) => callback(null, true) }));
-    registerAuthenticationRoutes(app); // this adds routes to authenticate a user (login, etc)
-    registerCorsProxy(app); // this adds a /corsProxy/ route to allow clients to get to urls that would otherwise be blocked by cors policies
-    isRelease && !SSL.Loaded && SSL.exit();
-    routeSetter(new RouteManager(app, isRelease)); // this sets up all the regular supervised routes (things like /home, download/upload api's, pdf, search, session, etc)
-    registerEmbeddedBrowseRelativePathHandler(app); // this allows renered web pages which internally have relative paths to find their content
+const week = 7 * 24 * 60 * 60 * 1000;
+const secret = '64d6866242d3b5a5503c675b32c9605e4e90478e9b77bcf2bc';
+const store = process.env.DB === 'MEM' ? new session.MemoryStore() : MongoStoreConnect.create({ mongoUrl: Database.url });
 
-    isRelease && process.env.serverPort && (resolvedPorts.server = Number(process.env.serverPort));
-    const server = isRelease ? createServer(SSL.Credentials, app) : app;
-    await new Promise<void>(resolve => server.listen(resolvedPorts.server, resolve));
-    logPort('server', resolvedPorts.server);
+/* Determine if the enviroment is dev mode or release mode. */
+function determineEnvironment() {
+    const isRelease = process.env.RELEASE === 'true';
 
-    resolvedServerUrl = `${isRelease && process.env.serverName ? `https://${process.env.serverName}.com` : 'http://localhost'}:${resolvedPorts.server}`;
+    const color = isRelease ? blue : yellow;
+    const label = isRelease ? 'release' : 'development';
+    console.log(`\nrunning server in ${color(label)} mode`);
 
-    // initialize the web socket (bidirectional communication: if a user changes
-    // a field on one client, that change must be broadcast to all other clients)
-    await WebSocket.initialize(isRelease, SSL.Credentials);
+    // // swilkins: I don't think we need to read from ClientUtils.RELEASE anymore. Should be able to invoke process.env.RELEASE
+    // // on the client side, thanks to dotenv in webpack.config.js
+    // let clientUtils = fs.readFileSync('./src/client/util/ClientUtils.ts.temp', 'utf8');
+    // clientUtils = `//AUTO-GENERATED FILE: DO NOT EDIT\n${clientUtils.replace('"mode"', String(isRelease))}`;
+    // fs.writeFileSync('./src/client/util/ClientUtils.ts', clientUtils, 'utf8');
 
-    //disconnect = async () => new Promise<Error>(resolve => server.close(resolve));
     return isRelease;
 }
 
-const week = 7 * 24 * 60 * 60 * 1000;
-const secret = '64d6866242d3b5a5503c675b32c9605e4e90478e9b77bcf2bc';
-const store = process.env.DB === 'MEM' ? new session.MemoryStore() : MongoStoreConnect.create({ mongoUrl: Database.url });
-
 function buildWithMiddleware(server: express.Express) {
     [
         session({
@@ -100,72 +86,43 @@ function buildWithMiddleware(server: express.Express) {
     return server;
 }
 
-/* Determine if the enviroment is dev mode or release mode. */
-function determineEnvironment() {
-    const isRelease = process.env.RELEASE === 'true';
-
-    const color = isRelease ? blue : yellow;
-    const label = isRelease ? 'release' : 'development';
-    console.log(`\nrunning server in ${color(label)} mode`);
-
-    // // swilkins: I don't think we need to read from ClientUtils.RELEASE anymore. Should be able to invoke process.env.RELEASE
-    // // on the client side, thanks to dotenv in webpack.config.js
-    // let clientUtils = fs.readFileSync('./src/client/util/ClientUtils.ts.temp', 'utf8');
-    // clientUtils = `//AUTO-GENERATED FILE: DO NOT EDIT\n${clientUtils.replace('"mode"', String(isRelease))}`;
-    // fs.writeFileSync('./src/client/util/ClientUtils.ts', clientUtils, 'utf8');
-
-    return isRelease;
-}
-
-function registerAuthenticationRoutes(server: express.Express) {
-    server.get('/signup', getSignup);
-    server.post('/signup', postSignup);
-
-    server.get('/login', getLogin);
-    server.post('/login', postLogin);
-
-    server.get('/logout', getLogout);
-
-    server.get('/forgotPassword', getForgot);
-    server.post('/forgotPassword', postForgot);
-
-    const reset = new RouteSubscriber('resetPassword').add('token').build;
-    server.get(reset, getReset);
-    server.post(reset, postReset);
-}
-
-function registerCorsProxy(server: express.Express) {
-    server.use('/corsProxy', async (req, res) => {
-        res.setHeader('Access-Control-Allow-Origin', '*');
-        res.header('Access-Control-Allow-Methods', 'GET, PUT, PATCH, POST, DELETE');
-        res.header('Access-Control-Allow-Headers', req.header('access-control-request-headers'));
-        const referer = req.headers.referer ? decodeURIComponent(req.headers.referer) : '';
-        let requrlraw = decodeURIComponent(req.url.substring(1));
-        const qsplit = requrlraw.split('?q=');
-        const newqsplit = requrlraw.split('&q=');
-        if (qsplit.length > 1 && newqsplit.length > 1) {
-            const lastq = newqsplit[newqsplit.length - 1];
-            requrlraw = qsplit[0] + '?q=' + lastq.split('&')[0] + '&' + qsplit[1].split('&')[1];
-        }
-        const requrl = requrlraw.startsWith('/') ? referer + requrlraw : requrlraw;
-        // cors weirdness here...
-        // if the referer is a cors page and the cors() route (I think) redirected to /corsProxy/<path> and the requested url path was relative,
-        // then we redirect again to the cors referer and just add the relative path.
-        if (!requrl.startsWith('http') && req.originalUrl.startsWith('/corsProxy') && referer?.includes('corsProxy')) {
-            res.redirect(referer + (referer.endsWith('/') ? '' : '/') + requrl);
+function registerEmbeddedBrowseRelativePathHandler(server: express.Express) {
+    server.use('*', (req, res) => {
+        // res.setHeader('Access-Control-Allow-Origin', '*');
+        // res.header('Access-Control-Allow-Methods', 'GET, PUT, PATCH, POST, DELETE');
+        // res.header('Access-Control-Allow-Headers', req.header('access-control-request-headers'));
+        const relativeUrl = req.originalUrl;
+        if (!res.headersSent && req.headers.referer?.includes('corsProxy')) {
+            if (!req.user) res.redirect('/home'); // When no user is logged in, we interpret a relative URL as being a reference to something they don't have access to and redirect to /home
+            // a request for something by a proxied referrer means it must be a relative reference.  So construct a proxied absolute reference here.
+            try {
+                const proxiedRefererUrl = decodeURIComponent(req.headers.referer); // (e.g., http://localhost:<port>/corsProxy/https://en.wikipedia.org/wiki/Engelbart)
+                const dashServerUrl = proxiedRefererUrl.match(/.*corsProxy\//)![0]; // the dash server url (e.g.: http://localhost:<port>/corsProxy/ )
+                const actualReferUrl = proxiedRefererUrl.replace(dashServerUrl, ''); // the url of the referer without the proxy (e.g., : https://en.wikipedia.org/wiki/Engelbart)
+                const absoluteTargetBaseUrl = actualReferUrl.match(/https?:\/\/[^/]*/)![0]; // the base of the original url (e.g.,  https://en.wikipedia.org)
+                const redirectedProxiedUrl = dashServerUrl + encodeURIComponent(absoluteTargetBaseUrl + relativeUrl); // the new proxied full url (e.g., http://localhost:<port>/corsProxy/https://en.wikipedia.org/<somethingelse>)
+                const redirectUrl = relativeUrl.startsWith('//') ? 'http:' + relativeUrl : redirectedProxiedUrl;
+                res.redirect(redirectUrl);
+            } catch (e) {
+                console.log('Error embed: ', e);
+            }
+        } else if (relativeUrl.startsWith('/search') && !req.headers.referer?.includes('corsProxy')) {
+            // detect search query and use default search engine
+            res.redirect(req.headers.referer + 'corsProxy/' + encodeURIComponent('http://www.google.com' + relativeUrl));
         } else {
-            proxyServe(req, requrl, res);
+            res.end();
         }
     });
 }
 
 function proxyServe(req: any, requrl: string, response: any) {
+    // eslint-disable-next-line global-require
     const htmlBodyMemoryStream = new (require('memorystream'))();
-    var wasinBrFormat = false;
+    let wasinBrFormat = false;
     const sendModifiedBody = () => {
         const header = response.headers['content-encoding'];
-        const refToCors = (match: any, tag: string, sym: string, href: string, offset: any, string: any) => `${tag}=${sym + resolvedServerUrl}/corsProxy/${href + sym}`;
-        const relpathToCors = (match: any, href: string, offset: any, string: any) => `="${resolvedServerUrl + '/corsProxy/' + decodeURIComponent(req.originalUrl.split('/corsProxy/')[1].match(/https?:\/\/[^\/]*/)?.[0] ?? '') + '/' + href}"`;
+        const refToCors = (match: any, tag: string, sym: string, href: string) => `${tag}=${sym + resolvedServerUrl}/corsProxy/${href + sym}`;
+        // const relpathToCors = (match: any, href: string, offset: any, string: any) => `="${resolvedServerUrl + '/corsProxy/' + decodeURIComponent(req.originalUrl.split('/corsProxy/')[1].match(/https?:\/\/[^\/]*/)?.[0] ?? '') + '/' + href}"`;
         if (header) {
             try {
                 const bodyStream = htmlBodyMemoryStream.read();
@@ -174,8 +131,8 @@ function proxyServe(req: any, requrl: string, response: any) {
                     const htmlText = htmlInputText
                         .toString('utf8')
                         .replace('<head>', '<head> <style>[id ^= "google"] { display: none; } </style>')
-                        .replace(/(src|href)=([\'\"])(https?[^\2\n]*)\1/g, refToCors) // replace src or href='http(s)://...' or href="http(s)://.."
-                        //.replace(/= *"\/([^"]*)"/g, relpathToCors)
+                        .replace(/(src|href)=(['"])(https?[^\2\n]*)\1/g, refToCors) // replace src or href='http(s)://...' or href="http(s)://.."
+                        // .replace(/= *"\/([^"]*)"/g, relpathToCors)
                         .replace(/data-srcset="[^"]*"/g, '')
                         .replace(/srcset="[^"]*"/g, '')
                         .replace(/target="_blank"/g, '');
@@ -198,7 +155,7 @@ function proxyServe(req: any, requrl: string, response: any) {
         }
     };
     const retrieveHTTPBody = () => {
-        //req.headers.cookie = '';
+        // req.headers.cookie = '';
         req.pipe(request(requrl))
             .on('error', (e: any) => {
                 console.log(`CORS url error: ${requrl}`, e);
@@ -227,6 +184,7 @@ function proxyServe(req: any, requrl: string, response: any) {
                 res.headers['x-permitted-cross-domain-policies'] = 'all';
                 res.headers['x-frame-options'] = '';
                 res.headers['content-security-policy'] = '';
+                // eslint-disable-next-line no-multi-assign
                 response.headers = response._headers = res.headers;
             })
             .on('end', sendModifiedBody)
@@ -236,31 +194,78 @@ function proxyServe(req: any, requrl: string, response: any) {
     retrieveHTTPBody();
 }
 
-function registerEmbeddedBrowseRelativePathHandler(server: express.Express) {
-    server.use('*', (req, res) => {
-        // res.setHeader('Access-Control-Allow-Origin', '*');
-        // res.header('Access-Control-Allow-Methods', 'GET, PUT, PATCH, POST, DELETE');
-        // res.header('Access-Control-Allow-Headers', req.header('access-control-request-headers'));
-        const relativeUrl = req.originalUrl;
-        if (!res.headersSent && req.headers.referer?.includes('corsProxy')) {
-            if (!req.user) res.redirect('/home'); // When no user is logged in, we interpret a relative URL as being a reference to something they don't have access to and redirect to /home
-            // a request for something by a proxied referrer means it must be a relative reference.  So construct a proxied absolute reference here.
-            try {
-                const proxiedRefererUrl = decodeURIComponent(req.headers.referer); // (e.g., http://localhost:<port>/corsProxy/https://en.wikipedia.org/wiki/Engelbart)
-                const dashServerUrl = proxiedRefererUrl.match(/.*corsProxy\//)![0]; // the dash server url (e.g.: http://localhost:<port>/corsProxy/ )
-                const actualReferUrl = proxiedRefererUrl.replace(dashServerUrl, ''); // the url of the referer without the proxy (e.g., : https://en.wikipedia.org/wiki/Engelbart)
-                const absoluteTargetBaseUrl = actualReferUrl.match(/https?:\/\/[^\/]*/)![0]; // the base of the original url (e.g.,  https://en.wikipedia.org)
-                const redirectedProxiedUrl = dashServerUrl + encodeURIComponent(absoluteTargetBaseUrl + relativeUrl); // the new proxied full url (e.g., http://localhost:<port>/corsProxy/https://en.wikipedia.org/<somethingelse>)
-                const redirectUrl = relativeUrl.startsWith('//') ? 'http:' + relativeUrl : redirectedProxiedUrl;
-                res.redirect(redirectUrl);
-            } catch (e) {
-                console.log('Error embed: ', e);
-            }
-        } else if (relativeUrl.startsWith('/search') && !req.headers.referer?.includes('corsProxy')) {
-            // detect search query and use default search engine
-            res.redirect(req.headers.referer + 'corsProxy/' + encodeURIComponent('http://www.google.com' + relativeUrl));
+function registerCorsProxy(server: express.Express) {
+    server.use('/corsProxy', async (req, res) => {
+        res.setHeader('Access-Control-Allow-Origin', '*');
+        res.header('Access-Control-Allow-Methods', 'GET, PUT, PATCH, POST, DELETE');
+        res.header('Access-Control-Allow-Headers', req.header('access-control-request-headers'));
+        const referer = req.headers.referer ? decodeURIComponent(req.headers.referer) : '';
+        let requrlraw = decodeURIComponent(req.url.substring(1));
+        const qsplit = requrlraw.split('?q=');
+        const newqsplit = requrlraw.split('&q=');
+        if (qsplit.length > 1 && newqsplit.length > 1) {
+            const lastq = newqsplit[newqsplit.length - 1];
+            requrlraw = qsplit[0] + '?q=' + lastq.split('&')[0] + '&' + qsplit[1].split('&')[1];
+        }
+        const requrl = requrlraw.startsWith('/') ? referer + requrlraw : requrlraw;
+        // cors weirdness here...
+        // if the referer is a cors page and the cors() route (I think) redirected to /corsProxy/<path> and the requested url path was relative,
+        // then we redirect again to the cors referer and just add the relative path.
+        if (!requrl.startsWith('http') && req.originalUrl.startsWith('/corsProxy') && referer?.includes('corsProxy')) {
+            res.redirect(referer + (referer.endsWith('/') ? '' : '/') + requrl);
         } else {
-            res.end();
+            proxyServe(req, requrl, res);
         }
     });
 }
+
+function registerAuthenticationRoutes(server: express.Express) {
+    server.get('/signup', getSignup);
+    server.post('/signup', postSignup);
+
+    server.get('/login', getLogin);
+    server.post('/login', postLogin);
+
+    server.get('/logout', getLogout);
+
+    server.get('/forgotPassword', getForgot);
+    server.post('/forgotPassword', postForgot);
+
+    const reset = new RouteSubscriber('resetPassword').add('token').build;
+    server.get(reset, getReset);
+    server.post(reset, postReset);
+}
+
+export default async function InitializeServer(routeSetter: RouteSetter) {
+    const isRelease = determineEnvironment();
+    const app = buildWithMiddleware(express());
+    const compiler = webpack(config as any);
+
+    // route table managed by express.  routes are tested sequentially against each of these map rules.  when a match is found, the handler is called to process the request
+    app.use(wdm(compiler, { publicPath: config.output.publicPath }));
+    app.use(whm(compiler));
+    app.get(/^\/+$/, (req, res) => res.redirect(req.user ? '/home' : '/login')); // target urls that consist of one or more '/'s with nothing in between
+    app.use(express.static(publicDirectory, { setHeaders: res => res.setHeader('Access-Control-Allow-Origin', '*') })); // all urls that start with dash's public directory: /files/  (e.g., /files/images, /files/audio, etc)
+    app.use(cors({ origin: (_origin: any, callback: any) => callback(null, true) }));
+    registerAuthenticationRoutes(app); // this adds routes to authenticate a user (login, etc)
+    registerCorsProxy(app); // this adds a /corsProxy/ route to allow clients to get to urls that would otherwise be blocked by cors policies
+    isRelease && !SSL.Loaded && SSL.exit();
+    routeSetter(new RouteManager(app, isRelease)); // this sets up all the regular supervised routes (things like /home, download/upload api's, pdf, search, session, etc)
+    registerEmbeddedBrowseRelativePathHandler(app); // this allows renered web pages which internally have relative paths to find their content
+
+    isRelease && process.env.serverPort && (resolvedPorts.server = Number(process.env.serverPort));
+    const server = isRelease ? createServer(SSL.Credentials, app) : app;
+    await new Promise<void>(resolve => {
+        server.listen(resolvedPorts.server, resolve);
+    });
+    logPort('server', resolvedPorts.server);
+
+    resolvedServerUrl = `${isRelease && process.env.serverName ? `https://${process.env.serverName}.com` : 'http://localhost'}:${resolvedPorts.server}`;
+
+    // initialize the web socket (bidirectional communication: if a user changes
+    // a field on one client, that change must be broadcast to all other clients)
+    await WebSocket.initialize(isRelease, SSL.Credentials);
+
+    // disconnect = async () => new Promise<Error>(resolve => server.close(resolve));
+    return isRelease;
+}
diff --git a/src/server/websocket.ts b/src/server/websocket.ts
index 38134f2c1..cb16bce72 100644
--- a/src/server/websocket.ts
+++ b/src/server/websocket.ts
@@ -3,152 +3,26 @@ import { createServer } from 'https';
 import * as _ from 'lodash';
 import { networkInterfaces } from 'os';
 import { Server, Socket } from 'socket.io';
-import { Utils } from '../Utils';
+import { ServerUtils } from '../ServerUtils';
 import { logPort } from './ActionUtilities';
-import { timeMap } from './ApiManagers/UserManager';
 import { Client } from './Client';
 import { DashStats } from './DashStats';
 import { DocumentsCollection } from './IDatabase';
 import { Diff, GestureContent, MessageStore, MobileDocumentUploadContent, MobileInkOverlayContent, Transferable, Types, UpdateMobileInkOverlayPositionContent, YoutubeQueryInput, YoutubeQueryTypes } from './Message';
 import { Search } from './Search';
+import { resolvedPorts, socketMap, timeMap, userOperations } from './SocketData';
 import { GoogleCredentialsLoader } from './apis/google/CredentialsLoader';
 import YoutubeApi from './apis/youtube/youtubeApiSample';
 import { initializeGuest } from './authentication/DashUserModel';
 import { Database } from './database';
-import { resolvedPorts } from './server_Initialization';
 
 export namespace WebSocket {
+    let CurUser: string | undefined;
+    // eslint-disable-next-line import/no-mutable-exports
     export let _socket: Socket;
+    // eslint-disable-next-line import/no-mutable-exports
+    export let _disconnect: Function;
     export const clients: { [key: string]: Client } = {};
-    export const socketMap = new Map<Socket, string>();
-    export const userOperations = new Map<string, number>();
-    export let disconnect: Function;
-
-    export async function initialize(isRelease: boolean, credentials:any) {
-        let io: Server;
-        if (isRelease) {
-            const { socketPort } = process.env;
-            if (socketPort) {
-                resolvedPorts.socket = Number(socketPort);
-            } 
-            const httpsServer = createServer(credentials);
-            io = new Server(httpsServer, {})
-            httpsServer.listen(resolvedPorts.socket);
-        } else {
-            io = new Server();
-            io.listen(resolvedPorts.socket);
-        }
-        logPort('websocket', resolvedPorts.socket);
-
-        io.on('connection', socket => {
-            _socket = socket;
-            socket.use((_packet, next) => {
-                const userEmail = socketMap.get(socket);
-                if (userEmail) {
-                    timeMap[userEmail] = Date.now();
-                }
-                next();
-            });
-
-            socket.emit(MessageStore.UpdateStats.Message, DashStats.getUpdatedStatsBundle());
-
-            // convenience function to log server messages on the client
-            function log(message?: any, ...optionalParams: any[]) {
-                socket.emit('log', ['Message from server:', message, ...optionalParams]);
-            }
-
-            socket.on('message', function (message, room) {
-                console.log('Client said: ', message);
-                socket.in(room).emit('message', message);
-            });
-
-            socket.on('create or join', function (room) {
-                console.log('Received request to create or join room ' + room);
-
-                const clientsInRoom = socket.rooms.has(room);
-                const numClients = clientsInRoom ? Object.keys(room.sockets).length : 0;
-                console.log('Room ' + room + ' now has ' + numClients + ' client(s)');
-
-                if (numClients === 0) {
-                    socket.join(room);
-                    console.log('Client ID ' + socket.id + ' created room ' + room);
-                    socket.emit('created', room, socket.id);
-                } else if (numClients === 1) {
-                    console.log('Client ID ' + socket.id + ' joined room ' + room);
-                    socket.in(room).emit('join', room);
-                    socket.join(room);
-                    socket.emit('joined', room, socket.id);
-                    socket.in(room).emit('ready');
-                } else {
-                    // max two clients
-                    socket.emit('full', room);
-                }
-            });
-
-            socket.on('ipaddr', function () {
-                const ifaces = networkInterfaces();
-                for (const dev in ifaces) {
-                    ifaces[dev]?.forEach(function (details) {
-                        if (details.family === 'IPv4' && details.address !== '127.0.0.1') {
-                            socket.emit('ipaddr', details.address);
-                        }
-                    });
-                }
-            });
-
-            socket.on('bye', function () {
-                console.log('received bye');
-            });
-
-            socket.on('disconnect', function () {
-                let currentUser = socketMap.get(socket);
-                if (!(currentUser === undefined)) {
-                    let currentUsername = currentUser.split(' ')[0];
-                    DashStats.logUserLogout(currentUsername, socket);
-                    delete timeMap[currentUsername];
-                }
-            });
-
-            Utils.Emit(socket, MessageStore.Foo, 'handshooken');
-
-            Utils.AddServerHandler(socket, MessageStore.Bar, guid => barReceived(socket, guid));
-            Utils.AddServerHandler(socket, MessageStore.SetField, args => setField(socket, args));
-            Utils.AddServerHandlerCallback(socket, MessageStore.GetField, getField);
-            Utils.AddServerHandlerCallback(socket, MessageStore.GetFields, getFields);
-            if (isRelease) {
-                Utils.AddServerHandler(socket, MessageStore.DeleteAll, () => doDelete(false));
-            }
-
-            Utils.AddServerHandler(socket, MessageStore.CreateField, CreateField);
-            Utils.AddServerHandlerCallback(socket, MessageStore.YoutubeApiQuery, HandleYoutubeQuery);
-            Utils.AddServerHandler(socket, MessageStore.UpdateField, diff => UpdateField(socket, diff));
-            Utils.AddServerHandler(socket, MessageStore.DeleteField, id => DeleteField(socket, id));
-            Utils.AddServerHandler(socket, MessageStore.DeleteFields, ids => DeleteFields(socket, ids));
-            Utils.AddServerHandler(socket, MessageStore.GesturePoints, content => processGesturePoints(socket, content));
-            Utils.AddServerHandler(socket, MessageStore.MobileInkOverlayTrigger, content => processOverlayTrigger(socket, content));
-            Utils.AddServerHandler(socket, MessageStore.UpdateMobileInkOverlayPosition, content => processUpdateOverlayPosition(socket, content));
-            Utils.AddServerHandler(socket, MessageStore.MobileDocumentUpload, content => processMobileDocumentUpload(socket, content));
-            Utils.AddServerHandlerCallback(socket, MessageStore.GetRefField, GetRefField);
-            Utils.AddServerHandlerCallback(socket, MessageStore.GetRefFields, GetRefFields);
-
-            /**
-             * Whenever we receive the go-ahead, invoke the import script and pass in
-             * as an emitter and a terminator the functions that simply broadcast a result
-             * or indicate termination to the client via the web socket
-             */
-
-            disconnect = () => {
-                socket.broadcast.emit('connection_terminated', Date.now());
-                socket.disconnect(true);
-            };
-        });
-
-        setInterval(function () {
-            // Utils.Emit(socket, MessageStore.UpdateStats, DashStats.getUpdatedStatsBundle());
-
-            io.emit(MessageStore.UpdateStats.Message, DashStats.getUpdatedStatsBundle());
-        }, DashStats.SAMPLING_INTERVAL);
-    }
 
     function processGesturePoints(socket: Socket, content: GestureContent) {
         socket.broadcast.emit('receiveGesturePoints', content);
@@ -174,8 +48,11 @@ export namespace WebSocket {
                 break;
             case YoutubeQueryTypes.SearchVideo:
                 YoutubeApi.authorizedGetVideos(ProjectCredentials, query.userInput, callback);
+                break;
             case YoutubeQueryTypes.VideoDetails:
                 YoutubeApi.authorizedGetVideoDetails(ProjectCredentials, query.videoIds, callback);
+                break;
+            default:
         }
     }
 
@@ -189,6 +66,9 @@ export namespace WebSocket {
         initializeGuest();
     }
 
+    function printActiveUsers() {
+        socketMap.forEach((user, socket) => !socket.disconnected && console.log(user));
+    }
     function barReceived(socket: Socket, userEmail: string) {
         clients[userEmail] = new Client(userEmail.toString());
         const currentdate = new Date();
@@ -203,7 +83,7 @@ export namespace WebSocket {
     }
 
     function getField([id, callback]: [string, (result?: Transferable) => void]) {
-        Database.Instance.getDocument(id, (result?: Transferable) => callback(result ? result : undefined));
+        Database.Instance.getDocument(id, (result?: Transferable) => callback(result));
     }
 
     function getFields([ids, callback]: [string[], (result: Transferable[]) => void]) {
@@ -248,27 +128,24 @@ export namespace WebSocket {
         list: [
             '_l',
             list => {
-                const results = [];
-                for (const value of list.fields) {
-                    const term = ToSearchTerm(value);
-                    if (term) {
-                        results.push(term.value);
-                    }
-                }
+                const results: any[] = [];
+                // eslint-disable-next-line no-use-before-define
+                list.fields.forEach((value: any) => ToSearchTerm(value) && results.push(ToSearchTerm(value)!.value));
                 return results.length ? results : null;
             },
         ],
     };
 
-    function ToSearchTerm(val: any): { suffix: string; value: any } | undefined {
+    function ToSearchTerm(valIn: any): { suffix: string; value: any } | undefined {
+        let val = valIn;
         if (val === null || val === undefined) {
-            return;
+            return undefined;
         }
         const type = val.__type || typeof val;
 
         let suffix = suffixMap[type];
         if (!suffix) {
-            return;
+            return undefined;
         }
         if (Array.isArray(suffix)) {
             const accessor = suffix[1];
@@ -277,7 +154,7 @@ export namespace WebSocket {
             } else {
                 val = val[accessor];
             }
-            suffix = suffix[0];
+            [suffix] = suffix;
         }
         return { suffix, value: val };
     }
@@ -285,8 +162,28 @@ export namespace WebSocket {
     function getSuffix(value: string | [string, any]): string {
         return typeof value === 'string' ? value : value[0];
     }
+    const pendingOps = new Map<string, { diff: Diff; socket: Socket }[]>();
 
-    function addToListField(socket: Socket, diff: Diff, curListItems?: Transferable): void {
+    function dispatchNextOp(id: string) {
+        const next = pendingOps.get(id)!.shift();
+        if (next) {
+            const { diff, socket } = next;
+            if (diff.diff.$addToSet) {
+                // eslint-disable-next-line no-use-before-define
+                return GetRefFieldLocal([diff.id, (result?: Transferable) => addToListField(socket, diff, result)]); // would prefer to have Mongo handle list additions direclty, but for now handle it on our own
+            }
+            if (diff.diff.$remFromSet) {
+                // eslint-disable-next-line no-use-before-define
+                return GetRefFieldLocal([diff.id, (result?: Transferable) => remFromListField(socket, diff, result)]); // would prefer to have Mongo handle list additions direclty, but for now handle it on our own
+            }
+            // eslint-disable-next-line no-use-before-define
+            return SetField(socket, diff);
+        }
+        return !pendingOps.get(id)!.length && pendingOps.delete(id);
+    }
+
+    function addToListField(socket: Socket, diffIn: Diff, curListItems?: Transferable): void {
+        const diff = diffIn;
         diff.diff.$set = diff.diff.$addToSet;
         delete diff.diff.$addToSet; // convert add to set to a query of the current fields, and then a set of the composition of the new fields with the old ones
         const updatefield = Array.from(Object.keys(diff.diff.$set))[0];
@@ -296,7 +193,7 @@ export namespace WebSocket {
             return;
         }
         const curList = (curListItems as any)?.fields?.[updatefield.replace('fields.', '')]?.fields.filter((item: any) => item !== undefined) || [];
-        diff.diff.$set[updatefield].fields = [...curList, ...newListItems]; //, ...newListItems.filter((newItem: any) => newItem === null || !curList.some((curItem: any) => curItem.fieldId ? curItem.fieldId === newItem.fieldId : curItem.heading ? curItem.heading === newItem.heading : curItem === newItem))];
+        diff.diff.$set[updatefield].fields = [...curList, ...newListItems]; // , ...newListItems.filter((newItem: any) => newItem === null || !curList.some((curItem: any) => curItem.fieldId ? curItem.fieldId === newItem.fieldId : curItem.heading ? curItem.heading === newItem.heading : curItem === newItem))];
         const sendBack = diff.diff.length !== diff.diff.$set[updatefield].fields.length;
         delete diff.diff.length;
         Database.Instance.update(
@@ -305,11 +202,13 @@ export namespace WebSocket {
             () => {
                 if (sendBack) {
                     console.log('Warning: list modified during update. Composite list is being returned.');
-                    const id = socket.id;
-                    (socket as any).id = '';
+                    const { id } = socket;
+                    (socket as any).id = ''; // bcz: HACK.  this prevents the update message from going back to the client that made the change.
                     socket.broadcast.emit(MessageStore.UpdateField.Message, diff);
                     (socket as any).id = id;
-                } else socket.broadcast.emit(MessageStore.UpdateField.Message, diff);
+                } else {
+                    socket.broadcast.emit(MessageStore.UpdateField.Message, diff);
+                }
                 dispatchNextOp(diff.id);
             },
             false
@@ -352,28 +251,28 @@ export namespace WebSocket {
      * items to delete)
      * @param curListItems the server's current copy of the data
      */
-    function remFromListField(socket: Socket, diff: Diff, curListItems?: Transferable): void {
+    function remFromListField(socket: Socket, diffIn: Diff, curListItems?: Transferable): void {
+        const diff = diffIn;
         diff.diff.$set = diff.diff.$remFromSet;
         delete diff.diff.$remFromSet;
         const updatefield = Array.from(Object.keys(diff.diff.$set))[0];
         const remListItems = diff.diff.$set[updatefield].fields;
         const curList = (curListItems as any)?.fields?.[updatefield.replace('fields.', '')]?.fields.filter((f: any) => f !== null) || [];
-        const hint = diff.diff.$set.hint;
+        const { hint } = diff.diff.$set;
 
         if (hint) {
             // indexesToRemove stores the indexes that we mark for deletion, which is later used to filter the list (delete the elements)
-            let indexesToRemove: number[] = [];
+            const indexesToRemove: number[] = [];
             for (let i = 0; i < hint.deleteCount; i++) {
                 if (curList.length > i + hint.start && _.isEqual(curList[i + hint.start], remListItems[i])) {
                     indexesToRemove.push(i + hint.start);
-                    continue;
-                }
-
-                let closestIndex = findClosestIndex(curList, indexesToRemove, remListItems[i], i + hint.start);
-                if (closestIndex !== -1) {
-                    indexesToRemove.push(closestIndex);
                 } else {
-                    console.log('Item to delete was not found - index = -1');
+                    const closestIndex = findClosestIndex(curList, indexesToRemove, remListItems[i], i + hint.start);
+                    if (closestIndex !== -1) {
+                        indexesToRemove.push(closestIndex);
+                    } else {
+                        console.log('Item to delete was not found - index = -1');
+                    }
                 }
             }
 
@@ -398,45 +297,23 @@ export namespace WebSocket {
                 if (sendBack) {
                     // the two copies are different, so the server sends its copy.
                     console.log('SEND BACK');
-                    const id = socket.id;
-                    (socket as any).id = '';
+                    const { id } = socket;
+                    (socket as any).id = ''; // bcz: HACK.  this prevents the update message from going back to the client that made the change.
                     socket.broadcast.emit(MessageStore.UpdateField.Message, diff);
                     (socket as any).id = id;
-                } else socket.broadcast.emit(MessageStore.UpdateField.Message, diff);
+                } else {
+                    socket.broadcast.emit(MessageStore.UpdateField.Message, diff);
+                }
                 dispatchNextOp(diff.id);
             },
             false
         );
     }
 
-    const pendingOps = new Map<string, { diff: Diff; socket: Socket }[]>();
-
-    function dispatchNextOp(id: string) {
-        const next = pendingOps.get(id)!.shift();
-        if (next) {
-            const { diff, socket } = next;
-            if (diff.diff.$addToSet) {
-                return GetRefFieldLocal([diff.id, (result?: Transferable) => addToListField(socket, diff, result)]); // would prefer to have Mongo handle list additions direclty, but for now handle it on our own
-            }
-            if (diff.diff.$remFromSet) {
-                return GetRefFieldLocal([diff.id, (result?: Transferable) => remFromListField(socket, diff, result)]); // would prefer to have Mongo handle list additions direclty, but for now handle it on our own
-            }
-            return GetRefFieldLocal([diff.id, (result?: Transferable) => SetField(socket, diff, result)]);
-        }
-        if (!pendingOps.get(id)!.length) pendingOps.delete(id);
-    }
-
-    function printActiveUsers() {
-        socketMap.forEach((user, socket) => {
-            !socket.disconnected && console.log(user);
-        });
-    }
-    var CurUser: string | undefined = undefined;
-
     function UpdateField(socket: Socket, diff: Diff) {
         const curUser = socketMap.get(socket);
-        if (!curUser) return;
-        let currentUsername = curUser.split(' ')[0];
+        if (!curUser) return false;
+        const currentUsername = curUser.split(' ')[0];
         userOperations.set(currentUsername, userOperations.get(currentUsername) !== undefined ? userOperations.get(currentUsername)! + 1 : 0);
 
         if (CurUser !== socketMap.get(socket)) {
@@ -454,15 +331,18 @@ export namespace WebSocket {
         if (diff.diff.$remFromSet) {
             return GetRefFieldLocal([diff.id, (result?: Transferable) => remFromListField(socket, diff, result)]); // would prefer to have Mongo handle list additions direclty, but for now handle it on our own
         }
-        return GetRefFieldLocal([diff.id, (result?: Transferable) => SetField(socket, diff, result)]);
+        // eslint-disable-next-line no-use-before-define
+        return SetField(socket, diff);
     }
-    function SetField(socket: Socket, diff: Diff, curListItems?: Transferable) {
+    function SetField(socket: Socket, diff: Diff /* , curListItems?: Transferable */) {
         Database.Instance.update(diff.id, diff.diff, () => socket.broadcast.emit(MessageStore.UpdateField.Message, diff), false);
         const docfield = diff.diff.$set || diff.diff.$unset;
         if (docfield) {
             const update: any = { id: diff.id };
             let dynfield = false;
+            // eslint-disable-next-line no-restricted-syntax
             for (let key in docfield) {
+                // eslint-disable-next-line no-continue
                 if (!key.startsWith('fields.')) continue;
                 dynfield = true;
                 const val = docfield[key];
@@ -504,4 +384,124 @@ export namespace WebSocket {
     function CreateField(newValue: any) {
         Database.Instance.insert(newValue);
     }
+    export async function initialize(isRelease: boolean, credentials: any) {
+        let io: Server;
+        if (isRelease) {
+            const { socketPort } = process.env;
+            if (socketPort) {
+                resolvedPorts.socket = Number(socketPort);
+            }
+            const httpsServer = createServer(credentials);
+            io = new Server(httpsServer, {});
+            httpsServer.listen(resolvedPorts.socket);
+        } else {
+            io = new Server();
+            io.listen(resolvedPorts.socket);
+        }
+        logPort('websocket', resolvedPorts.socket);
+
+        io.on('connection', socket => {
+            _socket = socket;
+            socket.use((_packet, next) => {
+                const userEmail = socketMap.get(socket);
+                if (userEmail) {
+                    timeMap[userEmail] = Date.now();
+                }
+                next();
+            });
+
+            socket.emit(MessageStore.UpdateStats.Message, DashStats.getUpdatedStatsBundle());
+
+            socket.on('message', (message, room) => {
+                console.log('Client said: ', message);
+                socket.in(room).emit('message', message);
+            });
+
+            socket.on('create or join', room => {
+                console.log('Received request to create or join room ' + room);
+
+                const clientsInRoom = socket.rooms.has(room);
+                const numClients = clientsInRoom ? Object.keys(room.sockets).length : 0;
+                console.log('Room ' + room + ' now has ' + numClients + ' client(s)');
+
+                if (numClients === 0) {
+                    socket.join(room);
+                    console.log('Client ID ' + socket.id + ' created room ' + room);
+                    socket.emit('created', room, socket.id);
+                } else if (numClients === 1) {
+                    console.log('Client ID ' + socket.id + ' joined room ' + room);
+                    socket.in(room).emit('join', room);
+                    socket.join(room);
+                    socket.emit('joined', room, socket.id);
+                    socket.in(room).emit('ready');
+                } else {
+                    // max two clients
+                    socket.emit('full', room);
+                }
+            });
+
+            socket.on('ipaddr', () => {
+                const ifaces = networkInterfaces();
+                for (const dev in ifaces) {
+                    ifaces[dev]?.forEach(details => {
+                        if (details.family === 'IPv4' && details.address !== '127.0.0.1') {
+                            socket.emit('ipaddr', details.address);
+                        }
+                    });
+                }
+            });
+
+            socket.on('bye', () => {
+                console.log('received bye');
+            });
+
+            socket.on('disconnect', () => {
+                const currentUser = socketMap.get(socket);
+                if (!(currentUser === undefined)) {
+                    const currentUsername = currentUser.split(' ')[0];
+                    DashStats.logUserLogout(currentUsername, socket);
+                    delete timeMap[currentUsername];
+                }
+            });
+
+            ServerUtils.Emit(socket, MessageStore.Foo, 'handshooken');
+
+            ServerUtils.AddServerHandler(socket, MessageStore.Bar, guid => barReceived(socket, guid));
+            ServerUtils.AddServerHandler(socket, MessageStore.SetField, args => setField(socket, args));
+            ServerUtils.AddServerHandlerCallback(socket, MessageStore.GetField, getField);
+            ServerUtils.AddServerHandlerCallback(socket, MessageStore.GetFields, getFields);
+            if (isRelease) {
+                ServerUtils.AddServerHandler(socket, MessageStore.DeleteAll, () => doDelete(false));
+            }
+
+            ServerUtils.AddServerHandler(socket, MessageStore.CreateField, CreateField);
+            ServerUtils.AddServerHandlerCallback(socket, MessageStore.YoutubeApiQuery, HandleYoutubeQuery);
+            ServerUtils.AddServerHandler(socket, MessageStore.UpdateField, diff => UpdateField(socket, diff));
+            ServerUtils.AddServerHandler(socket, MessageStore.DeleteField, id => DeleteField(socket, id));
+            ServerUtils.AddServerHandler(socket, MessageStore.DeleteFields, ids => DeleteFields(socket, ids));
+            ServerUtils.AddServerHandler(socket, MessageStore.GesturePoints, content => processGesturePoints(socket, content));
+            ServerUtils.AddServerHandler(socket, MessageStore.MobileInkOverlayTrigger, content => processOverlayTrigger(socket, content));
+            ServerUtils.AddServerHandler(socket, MessageStore.UpdateMobileInkOverlayPosition, content => processUpdateOverlayPosition(socket, content));
+            ServerUtils.AddServerHandler(socket, MessageStore.MobileDocumentUpload, content => processMobileDocumentUpload(socket, content));
+            ServerUtils.AddServerHandlerCallback(socket, MessageStore.GetRefField, GetRefField);
+            ServerUtils.AddServerHandlerCallback(socket, MessageStore.GetRefFields, GetRefFields);
+
+            /**
+             * Whenever we receive the go-ahead, invoke the import script and pass in
+             * as an emitter and a terminator the functions that simply broadcast a result
+             * or indicate termination to the client via the web socket
+             */
+
+            _disconnect = () => {
+                socket.broadcast.emit('connection_terminated', Date.now());
+                socket.disconnect(true);
+            };
+        });
+
+        setInterval(() => {
+            // Utils.Emit(socket, MessageStore.UpdateStats, DashStats.getUpdatedStatsBundle());
+
+            io.emit(MessageStore.UpdateStats.Message, DashStats.getUpdatedStatsBundle());
+        }, DashStats.SAMPLING_INTERVAL);
+    }
 }
diff --git a/tsconfig.json b/tsconfig.json
index 6cdf87d06..e8bec2744 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -12,7 +12,7 @@
         "allowJs": true,
         "sourceMap": true,
         "outDir": "dist",
-        "lib": ["dom", "es2017"],
+        "lib": ["DOM", "es2017"],
         "typeRoots": ["./src/typings", "node_modules/@types", "./src/extensions/General"],
         "resolveJsonModule": true,
         "moduleResolution": "node"
diff --git a/webpack.config.js b/webpack.config.js
index d92086bc2..6ddd68d97 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -2,13 +2,18 @@
 const path = require('path');
 const webpack = require('webpack');
 const HtmlWebpackPlugin = require('html-webpack-plugin');
-const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
+// eslint-disable-next-line import/no-extraneous-dependencies
+//const ESLintPlugin = require('eslint-webpack-plugin');
+// const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
 const { parsed } = require('dotenv').config();
 
 const plugins = [
     new HtmlWebpackPlugin({
         title: 'Caching',
     }),
+    // new ESLintPlugin({
+    //     extensions: ['ts'],
+    // }),
     // new ForkTsCheckerWebpackPlugin({
     //     typescript: {
     //         // useTypescriptIncrementalApi: true,
-- 
cgit v1.2.3-70-g09d2


From 939e18624af4252551f38c43335ee8ef0acd144c Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Sun, 21 Apr 2024 19:03:49 -0400
Subject: more lint cleanup

---
 .eslintrc.json                                     |   1 +
 package-lock.json                                  |   3 +-
 package.json                                       |   2 +-
 .../apis/google_docs/GooglePhotosClientUtils.ts    |  82 ++-
 src/client/util/BranchingTrailManager.tsx          |  41 +-
 src/client/util/CalendarManager.tsx                |  39 +-
 src/client/util/CurrentUserUtils.ts                |   2 +-
 src/client/util/DragManager.ts                     |  18 +-
 src/client/util/GroupManager.tsx                   |  85 ++-
 src/client/util/HypothesisUtils.ts                 |   2 -
 src/client/util/Import & Export/ImageUtils.ts      |   2 +-
 src/client/util/LinkFollower.ts                    |   7 +-
 src/client/util/LinkManager.ts                     |  28 +-
 src/client/util/ScriptingGlobals.ts                |   1 +
 src/client/util/SearchUtil.ts                      |  20 +-
 src/client/util/SettingsManager.tsx                | 323 +++++----
 src/client/util/SharingManager.tsx                 | 753 +++++++++++----------
 src/client/util/SnappingManager.ts                 |   3 +
 src/client/util/UndoManager.ts                     |  90 +--
 src/client/util/reportManager/ReportManager.tsx    |  38 +-
 src/client/views/ContextMenuItem.tsx               |  15 +-
 src/client/views/DashboardView.tsx                 |  41 +-
 src/client/views/DictationOverlay.tsx              |  55 +-
 src/client/views/DocComponent.tsx                  |   5 +-
 src/client/views/DocumentButtonBar.tsx             | 114 ++--
 src/client/views/EditableView.tsx                  |  23 +-
 src/client/views/FieldsDropdown.tsx                |  16 +-
 src/client/views/FilterPanel.tsx                   |   1 +
 src/client/views/GlobalKeyHandler.ts               |  58 +-
 src/client/views/InkingStroke.tsx                  | 109 +--
 src/client/views/MainView.tsx                      |  42 +-
 src/client/views/MetadataEntryMenu.tsx             | 196 ------
 src/client/views/OverlayView.tsx                   |   7 +-
 src/client/views/PropertiesButtons.tsx             | 182 +++--
 src/client/views/PropertiesView.tsx                |  44 +-
 src/client/views/SidebarAnnos.tsx                  |  13 +-
 src/client/views/StyleProvider.tsx                 |  75 +-
 src/client/views/UndoStack.tsx                     |  19 +-
 src/client/views/animationtimeline/Timeline.tsx    |   1 +
 src/client/views/animationtimeline/Track.tsx       |   2 +-
 .../collections/CollectionMasonryViewFieldRow.tsx  |  96 +--
 src/client/views/collections/CollectionMenu.tsx    |  19 +-
 .../views/collections/CollectionNoteTakingView.tsx |  85 ++-
 .../views/collections/CollectionPileView.tsx       |  13 +-
 .../collections/CollectionStackedTimeline.tsx      | 152 +++--
 src/client/views/collections/CollectionSubView.tsx |  31 +-
 .../views/collections/CollectionTimeView.tsx       |  32 +-
 .../views/collections/CollectionTreeView.tsx       |  79 ++-
 src/client/views/collections/TabDocView.tsx        | 347 +++++-----
 src/client/views/collections/TreeView.tsx          |  46 +-
 .../CollectionFreeFormInfoUI.tsx                   |  33 +-
 .../CollectionFreeFormLayoutEngines.tsx            |  38 +-
 .../collectionFreeForm/CollectionFreeFormView.tsx  | 295 +++++---
 .../collections/collectionFreeForm/MarqueeView.tsx |  10 +-
 .../collectionSchema/SchemaTableCell.tsx           |  33 +-
 src/client/views/global/globalScripts.ts           |  13 +-
 src/client/views/linking/LinkMenuItem.tsx          |  32 +-
 src/client/views/linking/LinkPopup.tsx             |  21 +-
 .../views/nodes/CollectionFreeFormDocumentView.tsx |  44 +-
 src/client/views/nodes/ComparisonBox.tsx           |  17 +-
 src/client/views/nodes/DataVizBox/DataVizBox.tsx   | 115 ++--
 .../nodes/DataVizBox/components/Histogram.tsx      | 230 +++----
 .../nodes/DataVizBox/components/LineChart.tsx      | 107 ++-
 .../views/nodes/DataVizBox/components/TableBox.tsx |  80 ++-
 src/client/views/nodes/DocumentContentsView.tsx    |  33 +-
 src/client/views/nodes/DocumentLinksButton.tsx     |  34 +-
 src/client/views/nodes/DocumentView.tsx            |   2 +-
 src/client/views/nodes/FieldView.tsx               |  10 +-
 src/client/views/nodes/FontIconBox/FontIconBox.tsx |  60 +-
 src/client/views/nodes/ImageBox.tsx                |   7 +-
 src/client/views/nodes/KeyValuePair.tsx            |  13 +-
 src/client/views/nodes/LabelBox.tsx                |  25 +-
 src/client/views/nodes/LinkAnchorBox.tsx           |   6 +-
 src/client/views/nodes/MapBox/MapBox.tsx           |  25 +-
 .../views/nodes/MapboxMapBox/MapboxContainer.tsx   |  31 +-
 src/client/views/nodes/PDFBox.tsx                  | 100 ++-
 .../views/nodes/RecordingBox/RecordingBox.tsx      |  37 +-
 src/client/views/nodes/ScreenshotBox.tsx           |  29 +-
 src/client/views/nodes/VideoBox.tsx                | 103 ++-
 src/client/views/nodes/WebBox.tsx                  | 153 +++--
 .../views/nodes/formattedText/DashFieldView.tsx    | 310 +++++----
 .../nodes/formattedText/FormattedTextBox.scss      |   3 +-
 .../views/nodes/formattedText/FormattedTextBox.tsx |   2 +-
 .../formattedText/ProsemirrorExampleTransfer.ts    |  51 +-
 .../views/nodes/formattedText/RichTextMenu.tsx     | 226 +++----
 .../views/nodes/formattedText/RichTextRules.ts     |   2 +-
 src/client/views/nodes/formattedText/marks_rts.ts  |  30 +-
 src/client/views/nodes/formattedText/nodes_rts.ts  |  14 +-
 src/client/views/nodes/trails/PresBox.tsx          | 635 ++++++++++-------
 src/client/views/nodes/trails/PresElementBox.tsx   |  96 +--
 src/client/views/pdf/Annotation.tsx                |   4 +-
 src/client/views/pdf/GPTPopup/GPTPopup.tsx         |   3 +-
 src/client/views/search/SearchBox.tsx              |  68 +-
 src/client/views/topbar/TopBar.tsx                 |  25 +-
 src/fields/Doc.ts                                  |  56 +-
 src/fields/List.ts                                 |  34 +-
 src/fields/ObjectField.ts                          |   1 +
 src/fields/Proxy.ts                                |  17 +-
 src/fields/Schema.ts                               |   2 +-
 src/fields/Types.ts                                |  15 +-
 src/fields/documentSchemas.ts                      |   3 +-
 src/mobile/ImageUpload.tsx                         |  77 +--
 src/mobile/MobileInterface.tsx                     |   2 +-
 src/server/ApiManagers/UploadManager.ts            |   4 +-
 104 files changed, 3740 insertions(+), 3134 deletions(-)
 delete mode 100644 src/client/views/MetadataEntryMenu.tsx

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

diff --git a/.eslintrc.json b/.eslintrc.json
index 0c4e375a9..780626412 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -53,6 +53,7 @@
         "react/destructuring-assignment": 0,
         "no-restricted-globals": ["error", "event"],
         "no-param-reassign": ["error", { "props": false }],
+        "import/no-cycle": 0,
         "no-alert": 0,
         "radix": "off"
     },
diff --git a/package-lock.json b/package-lock.json
index cc1a0ea64..c616bb6ea 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -183,6 +183,7 @@
                 "react-measure": "^2.5.2",
                 "react-resizable": "^3.0.5",
                 "react-select": "^5.8.0",
+                "react-type-animation": "^3.2.0",
                 "react-xarrows": "^2.0.2",
                 "readline": "^1.3.0",
                 "recharts": "^2.10.3",
@@ -287,7 +288,6 @@
                 "jsdom": "^24.0.0",
                 "mocha": "^10.2.0",
                 "prettier": "^3.1.0",
-                "react-type-animation": "^3.2.0",
                 "scss-loader": "0.0.1",
                 "style-loader": "^4.0.0",
                 "ts-loader": "^9.5.1",
@@ -30110,7 +30110,6 @@
             "version": "3.2.0",
             "resolved": "https://registry.npmjs.org/react-type-animation/-/react-type-animation-3.2.0.tgz",
             "integrity": "sha512-WXTe0i3rRNKjmggPvT5ntye1QBt0ATGbijeW6V3cQe2W0jaMABXXlPPEdtofnS9tM7wSRHchEvI9SUw+0kUohw==",
-            "dev": true,
             "peerDependencies": {
                 "prop-types": "^15.5.4",
                 "react": ">= 15.0.0",
diff --git a/package.json b/package.json
index 3f2f5a70f..a3ba6bdd9 100644
--- a/package.json
+++ b/package.json
@@ -83,7 +83,6 @@
         "jsdom": "^24.0.0",
         "mocha": "^10.2.0",
         "prettier": "^3.1.0",
-        "react-type-animation": "^3.2.0",
         "scss-loader": "0.0.1",
         "style-loader": "^4.0.0",
         "ts-loader": "^9.5.1",
@@ -267,6 +266,7 @@
         "react-measure": "^2.5.2",
         "react-resizable": "^3.0.5",
         "react-select": "^5.8.0",
+        "react-type-animation": "^3.2.0",
         "react-xarrows": "^2.0.2",
         "readline": "^1.3.0",
         "recharts": "^2.10.3",
diff --git a/src/client/apis/google_docs/GooglePhotosClientUtils.ts b/src/client/apis/google_docs/GooglePhotosClientUtils.ts
index 757031fec..07a2708ec 100644
--- a/src/client/apis/google_docs/GooglePhotosClientUtils.ts
+++ b/src/client/apis/google_docs/GooglePhotosClientUtils.ts
@@ -1,3 +1,5 @@
+/* eslint-disable no-use-before-define */
+import Photos = require('googlephotos');
 import { AssertionError } from 'assert';
 import { EditorState } from 'prosemirror-state';
 import { ClientUtils } from '../../../ClientUtils';
@@ -5,14 +7,12 @@ import { Doc, DocListCastAsync, Opt } from '../../../fields/Doc';
 import { Id } from '../../../fields/FieldSymbols';
 import { RichTextField } from '../../../fields/RichTextField';
 import { RichTextUtils } from '../../../fields/RichTextUtils';
-import { Cast, StrCast } from '../../../fields/Types';
-import { ImageField } from '../../../fields/URLField';
+import { Cast, ImageCast, StrCast } from '../../../fields/Types';
 import { MediaItem, NewMediaItemResult } from '../../../server/apis/google/SharedTypes';
 import { Networking } from '../../Network';
 import { DocUtils, Docs, DocumentOptions } from '../../documents/Documents';
 import { FormattedTextBox } from '../../views/nodes/formattedText/FormattedTextBox';
 import { GoogleAuthenticationManager } from '../GoogleAuthenticationManager';
-import Photos = require('googlephotos');
 
 export namespace GooglePhotos {
     const endpoint = async () => new Photos(await GoogleAuthenticationManager.Instance.fetchOrGenerateAccessToken());
@@ -76,17 +76,16 @@ export namespace GooglePhotos {
         export const CollectionToAlbum = async (options: AlbumCreationOptions): Promise<Opt<AlbumCreationResult>> => {
             const { collection, title, descriptionKey, tag } = options;
             const dataDocument = Doc.GetProto(collection);
-            const images = ((await DocListCastAsync(dataDocument.data)) || []).filter(doc => Cast(doc.data, ImageField));
+            const images = ((await DocListCastAsync(dataDocument.data)) || []).filter(doc => ImageCast(doc.data));
             if (!images || !images.length) {
                 return undefined;
             }
-            const resolved = title ? title : StrCast(collection.title) || `Dash Collection (${collection[Id]}`;
+            const resolved = title || StrCast(collection.title) || `Dash Collection (${collection[Id]}`;
             const { id, productUrl } = await Create.Album(resolved);
             const response = await Transactions.UploadImages(images, { id }, descriptionKey);
             if (response) {
                 const { results, failed } = response;
-                let index: Opt<number>;
-                while ((index = failed.pop()) !== undefined) {
+                for (let index = failed.pop(); index !== undefined; index = failed.pop()) {
                     Doc.RemoveDocFromList(dataDocument, 'data', images.splice(index, 1)[0]);
                 }
                 const mediaItems: MediaItem[] = results.map(item => item.mediaItem);
@@ -97,13 +96,12 @@ export namespace GooglePhotos {
                 for (let i = 0; i < images.length; i++) {
                     const image = Doc.GetProto(images[i]);
                     const mediaItem = mediaItems[i];
-                    if (!mediaItem) {
-                        continue;
+                    if (mediaItem) {
+                        image.googlePhotosId = mediaItem.id;
+                        image.googlePhotosAlbumUrl = productUrl;
+                        image.googlePhotosUrl = mediaItem.productUrl || mediaItem.baseUrl;
+                        idMapping[mediaItem.id] = image;
                     }
-                    image.googlePhotosId = mediaItem.id;
-                    image.googlePhotosAlbumUrl = productUrl;
-                    image.googlePhotosUrl = mediaItem.productUrl || mediaItem.baseUrl;
-                    idMapping[mediaItem.id] = image;
                 }
                 collection.googlePhotosAlbumUrl = productUrl;
                 collection.googlePhotosIdMapping = idMapping;
@@ -114,6 +112,7 @@ export namespace GooglePhotos {
                 Transactions.AddTextEnrichment(collection, `Find me at ${ClientUtils.prepend(`/doc/${collection[Id]}?sharing=true`)}`);
                 return { albumId: id, mediaItems };
             }
+            return undefined;
         };
     }
 
@@ -124,7 +123,7 @@ export namespace GooglePhotos {
             await GoogleAuthenticationManager.Instance.fetchOrGenerateAccessToken();
             const response = await Query.ContentSearch(requested);
             const uploads = await Transactions.WriteMediaItemsToServer(response);
-            const children = uploads.map((upload: Transactions.UploadInformation) => Docs.Create.ImageDocument(ClientUtils.fileUrl(upload.fileNames.clean) /*, {"data_contentSize":upload.contentSize}*/));
+            const children = uploads.map((upload: Transactions.UploadInformation) => Docs.Create.ImageDocument(ClientUtils.fileUrl(upload.fileNames.clean) /* , {"data_contentSize":upload.contentSize} */));
             const options = { _width: 500, _height: 500 };
             return constructor(children, options);
         };
@@ -144,7 +143,7 @@ export namespace GooglePhotos {
             const images = (await DocListCastAsync(collection.data))!.map(Doc.GetProto);
             images?.forEach(image => tagMapping.set(image[Id], ContentCategories.NONE));
             const values = Object.values(ContentCategories).filter(value => value !== ContentCategories.NONE);
-            for (const value of values) {
+            values.forEach(async value => {
                 const searched = (await ContentSearch({ included: [value] }))?.mediaItems?.map(({ id }) => id);
                 searched?.forEach(async id => {
                     const image = await Cast(idMapping[id], Doc);
@@ -154,7 +153,7 @@ export namespace GooglePhotos {
                         !tags?.includes(value) && tagMapping.set(key, tags + delimiter + value);
                     }
                 });
-            }
+            });
             images?.forEach(image => {
                 const concatenated = tagMapping.get(image[Id])!;
                 const tags = concatenated.split(delimiter);
@@ -200,9 +199,10 @@ export namespace GooglePhotos {
         export const AlbumSearch = async (albumId: string, pageSize = 100): Promise<MediaItem[]> => {
             const photos = await endpoint();
             const mediaItems: MediaItem[] = [];
-            let nextPageTokenStored: Opt<string> = undefined;
+            let nextPageTokenStored: Opt<string>;
             const found = 0;
             do {
+                // eslint-disable-next-line no-await-in-loop
                 const response: any = await photos.mediaItems.search(albumId, pageSize, nextPageTokenStored);
                 mediaItems.push(...response.mediaItems);
                 nextPageTokenStored = response.nextPageToken;
@@ -222,7 +222,7 @@ export namespace GooglePhotos {
             excluded.length && excluded.forEach(category => contentFilter.addExcludedContentCategories(category));
             filters.setContentFilter(contentFilter);
 
-            const date = options.date;
+            const { date } = options;
             if (date) {
                 const dateFilter = new photos.DateFilter();
                 if (date instanceof Date) {
@@ -240,15 +240,11 @@ export namespace GooglePhotos {
             });
         };
 
-        export const GetImage = async (mediaItemId: string): Promise<Transactions.MediaItem> => {
-            return (await endpoint()).mediaItems.get(mediaItemId);
-        };
+        export const GetImage = async (mediaItemId: string): Promise<Transactions.MediaItem> => (await endpoint()).mediaItems.get(mediaItemId);
     }
 
     namespace Create {
-        export const Album = async (title: string) => {
-            return (await endpoint()).albums.create(title);
-        };
+        export const Album = async (title: string) => (await endpoint()).albums.create(title);
     }
 
     export namespace Transactions {
@@ -278,6 +274,7 @@ export namespace GooglePhotos {
                     return enrichmentItem.id;
                 }
             }
+            return undefined;
         };
 
         export const WriteMediaItemsToServer = async (body: { mediaItems: any[] }): Promise<UploadInformation[]> => {
@@ -291,9 +288,12 @@ export namespace GooglePhotos {
                 return undefined;
             }
             const baseUrls: string[] = await Promise.all(
-                response.results.map(item => {
-                    return new Promise<string>(resolve => Query.GetImage(item.mediaItem.id).then(item => resolve(item.baseUrl)));
-                })
+                response.results.map(
+                    item =>
+                        new Promise<string>(resolve => {
+                            Query.GetImage(item.mediaItem.id).then(item => resolve(item.baseUrl));
+                        })
+                )
             );
             return baseUrls;
         };
@@ -303,27 +303,25 @@ export namespace GooglePhotos {
             failed: number[];
         }
 
-        export const UploadImages = async (sources: Doc[], album?: AlbumReference, descriptionKey = 'caption'): Promise<Opt<ImageUploadResults>> => {
+        export const UploadImages = async (sources: Doc[], albumIn?: AlbumReference, descriptionKey = 'caption'): Promise<Opt<ImageUploadResults>> => {
             await GoogleAuthenticationManager.Instance.fetchOrGenerateAccessToken();
-            if (album && 'title' in album) {
-                album = await Create.Album(album.title);
-            }
+            const album = albumIn && 'title' in albumIn ? await Create.Album(albumIn.title) : albumIn;
             const media: MediaInput[] = [];
-            for (const source of sources) {
-                const data = Cast(Doc.GetProto(source).data, ImageField);
-                if (!data) {
-                    return;
-                }
-                const url = data.url.href;
-                const target = Doc.MakeEmbedding(source);
-                const description = parseDescription(target, descriptionKey);
-                await DocUtils.makeCustomViewClicked(target, Docs.Create.FreeformDocument);
-                media.push({ url, description });
-            }
+            sources
+                .filter(source => ImageCast(Doc.GetProto(source).data))
+                .forEach(async source => {
+                    const data = ImageCast(Doc.GetProto(source).data);
+                    const url = data.url.href;
+                    const target = Doc.MakeEmbedding(source);
+                    const description = parseDescription(target, descriptionKey);
+                    await DocUtils.makeCustomViewClicked(target, Docs.Create.FreeformDocument);
+                    media.push({ url, description });
+                });
             if (media.length) {
                 const results = await Networking.PostToServer('/googlePhotosMediaPost', { media, album });
                 return results;
             }
+            return undefined;
         };
 
         const parseDescription = (document: Doc, descriptionKey: string) => {
diff --git a/src/client/util/BranchingTrailManager.tsx b/src/client/util/BranchingTrailManager.tsx
index 02879e3c4..28c00644f 100644
--- a/src/client/util/BranchingTrailManager.tsx
+++ b/src/client/util/BranchingTrailManager.tsx
@@ -1,18 +1,31 @@
+/* eslint-disable react/no-unused-class-component-methods */
+/* eslint-disable react/no-array-index-key */
 import { action, computed, makeObservable, observable } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
 import { Doc } from '../../fields/Doc';
 import { Id } from '../../fields/FieldSymbols';
-import { PresBox } from '../views/nodes/trails';
 import { OverlayView } from '../views/OverlayView';
+import { PresBox } from '../views/nodes/trails';
 import { DocumentManager } from './DocumentManager';
-import { Docs } from '../documents/Documents';
-import { nullAudio } from '../../fields/URLField';
 
 @observer
 export class BranchingTrailManager extends React.Component {
+    // eslint-disable-next-line no-use-before-define
     public static Instance: BranchingTrailManager;
 
+    // stack of the history
+    @observable private slideHistoryStack: String[] = [];
+    @observable private containsSet: Set<String> = new Set<String>();
+    // docId to Doc map
+    @observable private docIdToDocMap: Map<String, Doc> = new Map<String, Doc>();
+
+    // prev pres to copmare with
+    @observable private prevPresId: String | null = null;
+    @action setPrevPres = action((newId: String | null) => {
+        this.prevPresId = newId;
+    });
+
     constructor(props: any) {
         super(props);
         makeObservable(this);
@@ -22,7 +35,7 @@ export class BranchingTrailManager extends React.Component {
     }
 
     setupUi = () => {
-        OverlayView.Instance.addWindow(<BranchingTrailManager></BranchingTrailManager>, { x: 100, y: 150, width: 1000, title: 'Branching Trail' });
+        OverlayView.Instance.addWindow(<BranchingTrailManager />, { x: 100, y: 150, width: 1000, title: 'Branching Trail' });
         // OverlayView.Instance.forceUpdate();
         console.log(OverlayView.Instance);
         // let hi = Docs.Create.TextDocument("beee", {
@@ -36,23 +49,11 @@ export class BranchingTrailManager extends React.Component {
         console.log(DocumentManager._overlayViews);
     };
 
-    // stack of the history
-    @observable private slideHistoryStack: String[] = [];
     @action setSlideHistoryStack = action((newArr: String[]) => {
         this.slideHistoryStack = newArr;
     });
 
-    @observable private containsSet: Set<String> = new Set<String>();
-
-    // prev pres to copmare with
-    @observable private prevPresId: String | null = null;
-    @action setPrevPres = action((newId: String | null) => {
-        this.prevPresId = newId;
-    });
-
-    // docId to Doc map
-    @observable private docIdToDocMap: Map<String, Doc> = new Map<String, Doc>();
-
+    // eslint-disable-next-line react/sort-comp
     observeDocumentChange = (targetDoc: Doc, pres: PresBox) => {
         const presId = pres.Document[Id];
         if (this.prevPresId === presId) {
@@ -106,7 +107,7 @@ export class BranchingTrailManager extends React.Component {
         if (this.slideHistoryStack.length === 0) {
             Doc.UserDoc().isBranchingMode = false;
         }
-        //PresBox.NavigateToTarget(targetDoc, targetDoc);
+        // PresBox.NavigateToTarget(targetDoc, targetDoc);
     };
 
     @computed get trailBreadcrumbs() {
@@ -116,11 +117,11 @@ export class BranchingTrailManager extends React.Component {
                     const [presId, targetDocId] = info.split(',');
                     const doc = this.docIdToDocMap.get(targetDocId);
                     if (!doc) {
-                        return <></>;
+                        return null;
                     }
                     return (
                         <span key={targetDocId}>
-                            <button key={index} onPointerDown={e => this.clickHandler(e, targetDocId, index)}>
+                            <button type="button" key={index} onPointerDown={e => this.clickHandler(e, targetDocId, index)}>
                                 {presId.slice(0, 3) + ':' + doc.title}
                             </button>
                             -{'>'}
diff --git a/src/client/util/CalendarManager.tsx b/src/client/util/CalendarManager.tsx
index 6e9094b3a..46aa4d238 100644
--- a/src/client/util/CalendarManager.tsx
+++ b/src/client/util/CalendarManager.tsx
@@ -1,4 +1,10 @@
+/* eslint-disable jsx-a11y/no-static-element-interactions */
+/* eslint-disable jsx-a11y/click-events-have-key-events */
+import { DateRangePicker, Provider, defaultTheme } from '@adobe/react-spectrum';
+import { IconLookup, faPlus } from '@fortawesome/free-solid-svg-icons';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { TextField } from '@mui/material';
+import { Button } from 'browndash-components';
 import { action, computed, makeObservable, observable, runInAction } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
@@ -6,21 +12,16 @@ import Select from 'react-select';
 import { Doc, DocListCast } from '../../fields/Doc';
 import { DocData } from '../../fields/DocSymbols';
 import { StrCast } from '../../fields/Types';
+import { Docs } from '../documents/Documents';
 import { DictationOverlay } from '../views/DictationOverlay';
 import { MainViewModal } from '../views/MainViewModal';
+import { ObservableReactComponent } from '../views/ObservableReactComponent';
 import { DocumentView } from '../views/nodes/DocumentView';
 import { TaskCompletionBox } from '../views/nodes/TaskCompletedBox';
 import './CalendarManager.scss';
 import { DocumentManager } from './DocumentManager';
 import { SelectionManager } from './SelectionManager';
 import { SettingsManager } from './SettingsManager';
-// import { DateRange, Range, RangeKeyDict } from 'react-date-range';
-import { DateRangePicker, Provider, defaultTheme } from '@adobe/react-spectrum';
-import { IconLookup, faPlus } from '@fortawesome/free-solid-svg-icons';
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { Button } from 'browndash-components';
-import { Docs } from '../documents/Documents';
-import { ObservableReactComponent } from '../views/ObservableReactComponent';
 // import 'react-date-range/dist/styles.css';
 // import 'react-date-range/dist/theme/default.css';
 
@@ -47,6 +48,7 @@ const formatCalendarDateToString = (calendarDate: any) => {
 
 @observer
 export class CalendarManager extends ObservableReactComponent<{}> {
+    // eslint-disable-next-line no-use-before-define
     public static Instance: CalendarManager;
     @observable private isOpen = false;
     @observable private targetDoc: Doc | undefined = undefined; // the target document
@@ -83,10 +85,10 @@ export class CalendarManager extends ObservableReactComponent<{}> {
         this.creationType = type;
     };
 
-    public open = (target?: DocumentView, target_doc?: Doc) => {
+    public open = (target?: DocumentView, targetDoc?: Doc) => {
         console.log('hi');
         runInAction(() => {
-            this.targetDoc = target_doc || target?.Document;
+            this.targetDoc = targetDoc || target?.Document;
             this.targetDocView = target;
             DictationOverlay.Instance.hasActiveModal = true;
             this.isOpen = this.targetDoc !== undefined;
@@ -117,7 +119,7 @@ export class CalendarManager extends ObservableReactComponent<{}> {
     @action
     handleSelectChange = (option: any) => {
         if (option) {
-            let selectOpt = option as CalendarSelectOptions;
+            const selectOpt = option as CalendarSelectOptions;
             this.selectedExistingCalendarOption = selectOpt;
             this.calendarName = selectOpt.value; // or label
         }
@@ -136,7 +138,7 @@ export class CalendarManager extends ObservableReactComponent<{}> {
 
     // TODO: Make undoable
     private addToCalendar = () => {
-        let docs = SelectionManager.Views.length < 2 ? [this.targetDoc] : SelectionManager.Views.map(docView => docView.Document);
+        const docs = SelectionManager.Views.length < 2 ? [this.targetDoc] : SelectionManager.Views.map(docView => docView.Document);
         const targetDoc = this.layoutDocAcls ? docs[0] : docs[0]?.[DocData]; // doc to add to calendar
 
         console.log(targetDoc);
@@ -159,7 +161,7 @@ export class CalendarManager extends ObservableReactComponent<{}> {
                 }
             } else {
                 // find existing calendar based on selected name (should technically always find one)
-                const existingCalendar = this.existingCalendars.find(calendar => StrCast(calendar.title) === this.calendarName);
+                const existingCalendar = this.existingCalendars.find(findCal => StrCast(findCal.title) === this.calendarName);
                 if (existingCalendar) calendar = existingCalendar;
                 else {
                     this.errorMessage = 'Must select an existing calendar';
@@ -252,11 +254,9 @@ export class CalendarManager extends ObservableReactComponent<{}> {
 
     @computed
     get calendarInterface() {
-        let docs = SelectionManager.Views.length < 2 ? [this.targetDoc] : SelectionManager.Views.map(docView => docView.Document);
+        const docs = SelectionManager.Views.length < 2 ? [this.targetDoc] : SelectionManager.Views.map(docView => docView.Document);
         const targetDoc = this.layoutDocAcls ? docs[0] : docs[0]?.[DocData];
 
-        const currentDate = new Date();
-
         return (
             <div
                 className="calendar-interface"
@@ -268,10 +268,10 @@ export class CalendarManager extends ObservableReactComponent<{}> {
                     <b>{this.focusOn(docs.length < 2 ? StrCast(targetDoc?.title, 'this document') : '-multiple-')}</b>
                 </p>
                 <div className="creation-type-container">
-                    <div className={`calendar-creation ${this.creationType === 'new-calendar' ? 'calendar-creation-selected' : ''}`} onClick={e => this.setInterationType('new-calendar')}>
+                    <div className={`calendar-creation ${this.creationType === 'new-calendar' ? 'calendar-creation-selected' : ''}`} onClick={() => this.setInterationType('new-calendar')}>
                         Add to New Calendar
                     </div>
-                    <div className={`calendar-creation ${this.creationType === 'existing-calendar' ? 'calendar-creation-selected' : ''}`} onClick={e => this.setInterationType('existing-calendar')}>
+                    <div className={`calendar-creation ${this.creationType === 'existing-calendar' ? 'calendar-creation-selected' : ''}`} onClick={() => this.setInterationType('existing-calendar')}>
                         Add to Existing calendar
                     </div>
                 </div>
@@ -317,7 +317,8 @@ export class CalendarManager extends ObservableReactComponent<{}> {
                                     color: StrCast(Doc.UserDoc().userColor),
                                     width: '100%',
                                 }),
-                            }}></Select>
+                            }}
+                        />
                     )}
                 </div>
                 <div className="description-container">
@@ -351,6 +352,6 @@ export class CalendarManager extends ObservableReactComponent<{}> {
     }
 
     render() {
-        return <MainViewModal contents={this.calendarInterface} isDisplayed={this.isOpen} interactive={true} dialogueBoxDisplayedOpacity={this.dialogueBoxOpacity} overlayDisplayedOpacity={this.overlayOpacity} closeOnExternalClick={this.close} />;
+        return <MainViewModal contents={this.calendarInterface} isDisplayed={this.isOpen} interactive dialogueBoxDisplayedOpacity={this.dialogueBoxOpacity} overlayDisplayedOpacity={this.overlayOpacity} closeOnExternalClick={this.close} />;
     }
 }
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index 27ae5c9a0..6dba8027d 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -709,7 +709,7 @@ pie title Minerals in my tap water
         return [
             { title: "Back",    toolTip: "Go back",         btnType: ButtonType.ClickButton,  icon: "arrow-left",              scripts: { onClick: '{ return webBack(); }' }},
             { title: "Forward", toolTip: "Go forward",      btnType: ButtonType.ClickButton,  icon: "arrow-right",             scripts: { onClick: '{ return webForward(); }'}},
-            { title: "URL",     toolTip: "URL", width: 250, btnType: ButtonType.EditableText, icon: "lock", ignoreClick: true, scripts: { script:  '{ return webSetURL(value, _readOnly_); }'} },
+            { title: "URL",     toolTip: "URL", width: 250, btnType: ButtonType.EditText,     icon: "lock", ignoreClick: true, scripts: { script:  '{ return webSetURL(value, _readOnly_); }'} },
         ];
     }
     static videoTools() {
diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts
index 62f055f1a..3890b7845 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -495,18 +495,20 @@ export namespace DragManager {
                     .filter(pb => pb.width && pb.height)
                     .map((pb, i) => pb.getContext('2d')!.drawImage(pdfBoxSrc[i], 0, 0));
             }
-            [dragElement, ...Array.from(dragElement.getElementsByTagName('*'))].forEach(ele => {
-                (ele as any).style && ((ele as any).style.pointerEvents = 'none');
-            });
+            [dragElement, ...Array.from(dragElement.getElementsByTagName('*'))]
+                .map(dele => (dele as any).style)
+                .forEach(style => {
+                    style && (style.pointerEvents = 'none');
+                });
 
             dragDiv.appendChild(dragElement);
             if (dragElement !== ele) {
-                const children = [Array.from(ele.children), Array.from(dragElement.children)];
-                while (children[0].length) {
-                    const childs = [children[0].pop(), children[1].pop()];
+                const dragChildren = [Array.from(ele.children), Array.from(dragElement.children)];
+                while (dragChildren[0].length) {
+                    const childs = [dragChildren[0].pop(), dragChildren[1].pop()];
                     if (childs[0]?.children) {
-                        children[0].push(...Array.from(childs[0].children));
-                        children[1].push(...Array.from(childs[1]!.children));
+                        dragChildren[0].push(...Array.from(childs[0].children));
+                        dragChildren[1].push(...Array.from(childs[1]!.children));
                     }
                     if (childs[0]?.scrollTop) childs[1]!.scrollTop = childs[0].scrollTop;
                 }
diff --git a/src/client/util/GroupManager.tsx b/src/client/util/GroupManager.tsx
index c261c0f1e..8d84dbad8 100644
--- a/src/client/util/GroupManager.tsx
+++ b/src/client/util/GroupManager.tsx
@@ -1,3 +1,5 @@
+/* eslint-disable jsx-a11y/no-static-element-interactions */
+/* eslint-disable jsx-a11y/click-events-have-key-events */
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { Button, IconButton, Size, Type } from 'browndash-components';
 import { action, computed, makeObservable, observable } from 'mobx';
@@ -30,6 +32,7 @@ export interface UserOptions {
 
 @observer
 export class GroupManager extends ObservableReactComponent<{}> {
+    // eslint-disable-next-line no-use-before-define
     static Instance: GroupManager;
     @observable isOpen: boolean = false; // whether the GroupManager is to be displayed or not.
     @observable private users: string[] = []; // list of users populated from the database.
@@ -160,7 +163,7 @@ export class GroupManager extends ObservableReactComponent<{}> {
     addGroup(groupDoc: Doc): boolean {
         if (this.GroupManagerDoc) {
             Doc.AddDocToList(this.GroupManagerDoc, 'data', groupDoc);
-            this.GroupManagerDoc['data_modificationDate'] = new DateField();
+            this.GroupManagerDoc.data_modificationDate = new DateField();
             return true;
         }
         return false;
@@ -202,7 +205,7 @@ export class GroupManager extends ObservableReactComponent<{}> {
             !memberList.includes(email) && memberList.push(email);
             groupDoc.members = JSON.stringify(memberList);
             SharingManager.Instance.shareWithAddedMember(groupDoc, email);
-            this.GroupManagerDoc && (this.GroupManagerDoc['data_modificationDate'] = new DateField());
+            this.GroupManagerDoc && (this.GroupManagerDoc.data_modificationDate = new DateField());
         }
     }
 
@@ -216,10 +219,9 @@ export class GroupManager extends ObservableReactComponent<{}> {
             const memberList = JSON.parse(StrCast(groupDoc.members));
             const index = memberList.indexOf(email);
             if (index !== -1) {
-                const user = memberList.splice(index, 1)[0];
                 groupDoc.members = JSON.stringify(memberList);
                 SharingManager.Instance.removeMember(groupDoc, email);
-                this.GroupManagerDoc && (this.GroupManagerDoc['data_modificationDate'] = new DateField());
+                this.GroupManagerDoc && (this.GroupManagerDoc.data_modificationDate = new DateField());
             }
         }
     }
@@ -275,7 +277,9 @@ export class GroupManager extends ObservableReactComponent<{}> {
         TaskCompletionBox.textDisplayed = 'Group created!';
         TaskCompletionBox.taskCompleted = true;
         setTimeout(
-            action(() => (TaskCompletionBox.taskCompleted = false)),
+            action(() => {
+                TaskCompletionBox.taskCompleted = false;
+            }),
             2000
         );
     };
@@ -292,7 +296,7 @@ export class GroupManager extends ObservableReactComponent<{}> {
                     </p>
                     <div className="close-button">
                         <Button
-                            icon={<FontAwesomeIcon icon={'times'} size={'lg'} />}
+                            icon={<FontAwesomeIcon icon="times" size="lg" />}
                             onClick={action(() => {
                                 this.createGroupModalOpen = false;
                                 TaskCompletionBox.taskCompleted = false;
@@ -302,7 +306,17 @@ export class GroupManager extends ObservableReactComponent<{}> {
                     </div>
                 </div>
                 <div className="group-input" style={{ border: StrCast(Doc.UserDoc().userColor) }}>
-                    <input ref={this.inputRef} onKeyDown={this.handleKeyDown} autoFocus type="text" placeholder="Group name" onChange={action(() => (this.buttonColour = this.inputRef.current?.value ? 'black' : '#979797'))} />
+                    <input
+                        ref={this.inputRef}
+                        onKeyDown={this.handleKeyDown}
+                        // eslint-disable-next-line jsx-a11y/no-autofocus
+                        autoFocus
+                        type="text"
+                        placeholder="Group name"
+                        onChange={action(() => {
+                            this.buttonColour = this.inputRef.current?.value ? 'black' : '#979797';
+                        })}
+                    />
                 </div>
                 <div style={{ border: StrCast(Doc.UserDoc().userColor) }}>
                     <Select
@@ -310,7 +324,7 @@ export class GroupManager extends ObservableReactComponent<{}> {
                         isMulti
                         options={this.options}
                         onChange={this.handleChange}
-                        placeholder={'Select users'}
+                        placeholder="Select users"
                         value={this.selectedUsers}
                         closeMenuOnSelect={false}
                         styles={{
@@ -335,8 +349,8 @@ export class GroupManager extends ObservableReactComponent<{}> {
                         }}
                     />
                 </div>
-                <div className={'create-button'}>
-                    <Button text={'Create'} type={Type.TERT} color={StrCast(Doc.UserDoc().userColor)} onClick={this.createGroup} />
+                <div className="create-button">
+                    <Button text="Create" type={Type.TERT} color={StrCast(Doc.UserDoc().userColor)} onClick={this.createGroup} />
                 </div>
             </div>
         );
@@ -344,7 +358,7 @@ export class GroupManager extends ObservableReactComponent<{}> {
         return (
             <MainViewModal
                 isDisplayed={this.createGroupModalOpen}
-                interactive={true}
+                interactive
                 contents={contents}
                 dialogueBoxStyle={{ width: '90%', height: '70%' }}
                 closeOnExternalClick={action(() => {
@@ -372,28 +386,59 @@ export class GroupManager extends ObservableReactComponent<{}> {
         return (
             <div className="group-interface" style={{ background: SettingsManager.userBackgroundColor, color: SettingsManager.userColor }}>
                 {this.groupCreationModal}
-                {this.currentGroup ? <GroupMemberView group={this.currentGroup} onCloseButtonClick={action(() => (this.currentGroup = undefined))} /> : null}
+                {this.currentGroup ? (
+                    <GroupMemberView
+                        group={this.currentGroup}
+                        onCloseButtonClick={action(() => {
+                            this.currentGroup = undefined;
+                        })}
+                    />
+                ) : null}
                 <div className="group-heading">
                     <p>
                         <b>Manage Groups</b>
                     </p>
-                    <Button icon={<FontAwesomeIcon icon={'plus'} />} iconPlacement={'left'} text={'Create Group'} type={Type.TERT} color={StrCast(Doc.UserDoc().userColor)} onClick={action(() => (this.createGroupModalOpen = true))} />
-                    <div className={'close-button'}>
-                        <Button icon={<FontAwesomeIcon icon={'times'} size={'lg'} />} onClick={this.close} color={StrCast(Doc.UserDoc().userColor)} />
+                    <Button
+                        icon={<FontAwesomeIcon icon="plus" />}
+                        iconPlacement="left"
+                        text="Create Group"
+                        type={Type.TERT}
+                        color={StrCast(Doc.UserDoc().userColor)}
+                        onClick={action(() => {
+                            this.createGroupModalOpen = true;
+                        })}
+                    />
+                    <div className="close-button">
+                        <Button icon={<FontAwesomeIcon icon="times" size="lg" />} onClick={this.close} color={StrCast(Doc.UserDoc().userColor)} />
                     </div>
                 </div>
                 <div className="main-container">
-                    <div className="sort-groups" onClick={action(() => (this.groupSort = this.groupSort === 'ascending' ? 'descending' : this.groupSort === 'descending' ? 'none' : 'ascending'))}>
+                    <div
+                        className="sort-groups"
+                        onClick={action(() => {
+                            this.groupSort = this.groupSort === 'ascending' ? 'descending' : this.groupSort === 'descending' ? 'none' : 'ascending';
+                        })}>
                         Name
                         <IconButton icon={<FontAwesomeIcon icon={this.groupSort === 'ascending' ? 'caret-up' : this.groupSort === 'descending' ? 'caret-down' : 'caret-right'} />} size={Size.XSMALL} color={StrCast(Doc.UserDoc().userColor)} />
                     </div>
-                    <div className={'style-divider'} style={{ background: StrCast(Doc.UserDoc().userColor) }} />
+                    <div className="style-divider" style={{ background: StrCast(Doc.UserDoc().userColor) }} />
                     <div className="group-body" style={{ background: StrCast(Doc.UserDoc().userBackgroundColor), color: StrCast(Doc.UserDoc().userColor) }}>
                         {groups.map(group => (
                             <div className="group-row" key={StrCast(group.title || group.groupName)}>
                                 <div className="group-name">{StrCast(group.title || group.groupName)}</div>
-                                <div className="group-info" onClick={action(() => (this.currentGroup = group))}>
-                                    <IconButton icon={<FontAwesomeIcon icon={'info-circle'} />} size={Size.XSMALL} color={StrCast(Doc.UserDoc().userColor)} onClick={action(() => (this.currentGroup = group))} />
+                                <div
+                                    className="group-info"
+                                    onClick={action(() => {
+                                        this.currentGroup = group;
+                                    })}>
+                                    <IconButton
+                                        icon={<FontAwesomeIcon icon="info-circle" />}
+                                        size={Size.XSMALL}
+                                        color={StrCast(Doc.UserDoc().userColor)}
+                                        onClick={action(() => {
+                                            this.currentGroup = group;
+                                        })}
+                                    />
                                 </div>
                             </div>
                         ))}
@@ -404,6 +449,6 @@ export class GroupManager extends ObservableReactComponent<{}> {
     }
 
     render() {
-        return <MainViewModal contents={this.groupInterface} isDisplayed={this.isOpen} interactive={true} dialogueBoxStyle={{ zIndex: 1002 }} overlayStyle={{ zIndex: 1001 }} closeOnExternalClick={this.close} />;
+        return <MainViewModal contents={this.groupInterface} isDisplayed={this.isOpen} interactive dialogueBoxStyle={{ zIndex: 1002 }} overlayStyle={{ zIndex: 1001 }} closeOnExternalClick={this.close} />;
     }
 }
diff --git a/src/client/util/HypothesisUtils.ts b/src/client/util/HypothesisUtils.ts
index 48d3ac046..6dc96f1b5 100644
--- a/src/client/util/HypothesisUtils.ts
+++ b/src/client/util/HypothesisUtils.ts
@@ -3,12 +3,10 @@ import { simulateMouseClick } from '../../ClientUtils';
 import { Doc, Opt } from '../../fields/Doc';
 import { Cast, StrCast } from '../../fields/Types';
 import { WebField } from '../../fields/URLField';
-import { DocumentType } from '../documents/DocumentTypes';
 import { Docs } from '../documents/Documents';
 import { DocumentLinksButton } from '../views/nodes/DocumentLinksButton';
 import { DocumentView } from '../views/nodes/DocumentView';
 import { DocumentManager } from './DocumentManager';
-import { SearchUtil } from './SearchUtil';
 import { SelectionManager } from './SelectionManager';
 
 export namespace Hypothesis {
diff --git a/src/client/util/Import & Export/ImageUtils.ts b/src/client/util/Import & Export/ImageUtils.ts
index dfad9755c..8d4eefa7e 100644
--- a/src/client/util/Import & Export/ImageUtils.ts	
+++ b/src/client/util/Import & Export/ImageUtils.ts	
@@ -16,7 +16,7 @@ export namespace ImageUtils {
     };
     export const ExtractImgInfo = async (document: Doc): Promise<imgInfo | undefined> => {
         const field = Cast(document.data, ImageField);
-        return field ? await Networking.PostToServer('/inspectImage', { source: field.url.href }) : undefined;
+        return field ? Networking.PostToServer('/inspectImage', { source: field.url.href }) : undefined;
     };
 
     export const AssignImgInfo = (document: Doc, data?: imgInfo) => {
diff --git a/src/client/util/LinkFollower.ts b/src/client/util/LinkFollower.ts
index ba715aa1f..85bada8c9 100644
--- a/src/client/util/LinkFollower.ts
+++ b/src/client/util/LinkFollower.ts
@@ -53,7 +53,7 @@ export class LinkFollower {
         const linkWithoutTargetDoc = traverseBacklink === undefined ? fwdLinkWithoutTargetView ?? backLinkWithoutTargetView : traverseBacklink ? backLinkWithoutTargetView : fwdLinkWithoutTargetView;
         const linkDocList = linkWithoutTargetDoc && !sourceDoc.followAllLinks ? [linkWithoutTargetDoc] : traverseBacklink === undefined ? fwdLinks.concat(backLinks) : traverseBacklink ? backLinks : fwdLinks;
         const followLinks = sourceDoc.followLinkToggle || sourceDoc.followAllLinks ? linkDocList : linkDocList.slice(0, 1);
-        var count = 0;
+        let count = 0;
         const allFinished = () => ++count === followLinks.length && finished?.();
         if (!followLinks.length) {
             finished?.();
@@ -69,7 +69,7 @@ export class LinkFollower {
                         ? linkDoc.link_anchor_2
                         : linkDoc.link_anchor_1
             ) as Doc;
-            const srcAnchor = LinkManager.getOppositeAnchor(linkDoc, target) ?? sourceDoc;
+            const srcAnchor: Doc = LinkManager.getOppositeAnchor(linkDoc, target) ?? sourceDoc;
             if (target) {
                 const doFollow = (canToggle?: boolean) => {
                     const toggleTarget = canToggle && BoolCast(sourceDoc.followLinkToggle);
@@ -113,10 +113,12 @@ export class LinkFollower {
                 }
                 const moveTo = [NumCast(sourceDoc.x) + NumCast(sourceDoc.followLinkXoffset), NumCast(sourceDoc.y) + NumCast(sourceDoc.followLinkYoffset)];
                 if (srcAnchor.followLinkXoffset !== undefined && moveTo[0] !== target.x) {
+                    // eslint-disable-next-line prefer-destructuring
                     target.x = moveTo[0];
                     movedTarget = true;
                 }
                 if (srcAnchor.followLinkYoffset !== undefined && moveTo[1] !== target.y) {
+                    // eslint-disable-next-line prefer-destructuring
                     target.y = moveTo[1];
                     movedTarget = true;
                 }
@@ -130,6 +132,7 @@ export class LinkFollower {
     }
 }
 
+// eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function followLink(doc: Doc, altKey: boolean) {
     SelectionManager.DeselectAll();
     return LinkFollower.FollowLink(undefined, doc, altKey) ? undefined : { select: true };
diff --git a/src/client/util/LinkManager.ts b/src/client/util/LinkManager.ts
index 82cd791cc..c986bd674 100644
--- a/src/client/util/LinkManager.ts
+++ b/src/client/util/LinkManager.ts
@@ -25,6 +25,7 @@ import { ScriptingGlobals } from './ScriptingGlobals';
  * - user defined kvps
  */
 export class LinkManager {
+    // eslint-disable-next-line no-use-before-define
     @observable static _instance: LinkManager;
     @observable.shallow userLinkDBs: Doc[] = [];
     @observable public currentLink: Opt<Doc> = undefined;
@@ -61,7 +62,7 @@ export class LinkManager {
                         Promise.all(lAnchs.map(lAnch => PromiseValue(lAnch?.proto as Doc))).then((lAnchProtos: Opt<Doc>[]) =>
                             Promise.all(lAnchProtos.map(lAnchProto => PromiseValue(lAnchProto?.proto as Doc))).then(
                                 link &&
-                                    action(lAnchProtoProtos => {
+                                    action(() => {
                                         Doc.AddDocToList(Doc.UserDoc(), 'links', link);
                                         lAnchs[0]?.[DocData][DirectLinks].add(link);
                                         lAnchs[1]?.[DocData][DirectLinks].add(link);
@@ -78,7 +79,7 @@ export class LinkManager {
                     Promise.all([lproto?.link_anchor_1 as Doc, lproto?.link_anchor_2 as Doc].map(PromiseValue)).then((lAnchs: Opt<Doc>[]) =>
                         Promise.all(lAnchs.map(lAnch => PromiseValue(lAnch?.proto as Doc))).then((lAnchProtos: Opt<Doc>[]) =>
                             Promise.all(lAnchProtos.map(lAnchProto => PromiseValue(lAnchProto?.proto as Doc))).then(
-                                action(lAnchProtoProtos => {
+                                action(() => {
                                     link && lAnchs[0] && lAnchs[0][DocData][DirectLinks].delete(link);
                                     link && lAnchs[1] && lAnchs[1][DocData][DirectLinks].delete(link);
                                 })
@@ -100,7 +101,8 @@ export class LinkManager {
                                 (change as any).added.forEach((link: any) => addLinkToDoc(toRealField(link)));
                                 (change as any).removed.forEach((link: any) => remLinkFromDoc(toRealField(link)));
                                 break;
-                            case 'update': //let oldValue = change.oldValue;
+                            case 'update': // let oldValue = change.oldValue;
+                            default:
                         }
                     },
                     true
@@ -120,6 +122,8 @@ export class LinkManager {
                                     added?.forEach((link: any) => addLinkToDoc(toRealField(link)));
                                     removed?.forEach((link: any) => remLinkFromDoc(toRealField(link)));
                                 });
+                                break;
+                            default:
                         }
                     },
                     true
@@ -133,7 +137,8 @@ export class LinkManager {
                     case 'splice':
                         (change as any).added.forEach(watchUserLinkDB);
                         break;
-                    case 'update': //let oldValue = change.oldValue;
+                    case 'update': // let oldValue = change.oldValue;
+                    default:
                 }
             },
             true
@@ -143,7 +148,7 @@ export class LinkManager {
     }
 
     public createlink_relationshipLists = () => {
-        //create new lists for link relations and their associated colors if the lists don't already exist
+        // create new lists for link relations and their associated colors if the lists don't already exist
         !Doc.UserDoc().link_relationshipList && (Doc.UserDoc().link_relationshipList = new List<string>());
         !Doc.UserDoc().link_ColorList && (Doc.UserDoc().link_ColorList = new List<string>());
         !Doc.UserDoc().link_relationshipSizes && (Doc.UserDoc().link_relationshipSizes = new List<number>());
@@ -153,6 +158,7 @@ export class LinkManager {
         Doc.AddDocToList(Doc.UserDoc(), 'links', linkDoc);
         if (!checkExists || !DocListCast(Doc.LinkDBDoc().data).includes(linkDoc)) {
             Doc.AddDocToList(Doc.LinkDBDoc(), 'data', linkDoc);
+            // eslint-disable-next-line no-use-before-define
             setTimeout(UPDATE_SERVER_CACHE, 100);
         }
     }
@@ -203,7 +209,7 @@ export class LinkManager {
         this.relatedLinker(anchor).forEach(link => {
             if (link.link_relationship && link.link_relationship !== '-ungrouped-') {
                 const relation = StrCast(link.link_relationship);
-                const anchorRelation = relation.indexOf(':') !== -1 ? relation.split(':')[Doc.AreProtosEqual(Cast(link.link_anchor_1, Doc, null), anchor) ? 0 : 1] : relation;
+                const anchorRelation: string = relation.indexOf(':') !== -1 ? relation.split(':')[Doc.AreProtosEqual(Cast(link.link_anchor_1, Doc, null), anchor) ? 0 : 1] : relation;
                 const group = anchorGroups.get(anchorRelation);
                 anchorGroups.set(anchorRelation, group ? [...group, link] : [link]);
             } else {
@@ -234,14 +240,7 @@ export class LinkManager {
         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);
-        // const a2 = DocCast(linkDoc.link_anchor_2);
-        // if (linkDoc.link_matchEmbeddings) {
-        //     return [a2, a2.annotationOn].includes(anchor) ? '2' : '1';
-        // }
-        // if (Doc.AreProtosEqual(a2, anchor) || Doc.AreProtosEqual(a2.annotationOn as Doc, anchor)) return '2';
-        // return Doc.AreProtosEqual(a1, anchor) || Doc.AreProtosEqual(a1.annotationOn as Doc, anchor) ? '1' : '2';
+        return undefined;
     }
 }
 
@@ -282,6 +281,7 @@ export function UPDATE_SERVER_CACHE() {
 }
 
 ScriptingGlobals.add(
+    // eslint-disable-next-line prefer-arrow-callback
     function links(doc: any) {
         return new List(LinkManager.Links(doc));
     },
diff --git a/src/client/util/ScriptingGlobals.ts b/src/client/util/ScriptingGlobals.ts
index bc159ed65..ac524394a 100644
--- a/src/client/util/ScriptingGlobals.ts
+++ b/src/client/util/ScriptingGlobals.ts
@@ -41,6 +41,7 @@ export namespace ScriptingGlobals {
         if (n === undefined || n === 'undefined') {
             return false;
         }
+        // eslint-disable-next-line no-prototype-builtins
         if (_scriptingGlobals.hasOwnProperty(n)) {
             throw new Error(`Global with name ${n} is already registered, choose another name`);
         }
diff --git a/src/client/util/SearchUtil.ts b/src/client/util/SearchUtil.ts
index 65b9a977d..609fedfa9 100644
--- a/src/client/util/SearchUtil.ts
+++ b/src/client/util/SearchUtil.ts
@@ -8,16 +8,16 @@ import { DocOptions, FInfo } from '../documents/Documents';
 export namespace SearchUtil {
     export type HighlightingResult = { [id: string]: { [key: string]: string[] } };
 
-    export function SearchCollection(collectionDoc: Opt<Doc>, query: string, matchKeyNames: boolean, onlyKeys?: string[]) {
+    export function SearchCollection(collectionDoc: Opt<Doc>, queryIn: string, matchKeyNames: boolean, onlyKeys?: string[]) {
         const blockedTypes = [DocumentType.PRESELEMENT, DocumentType.CONFIG, DocumentType.KVP, DocumentType.FONTICON, DocumentType.BUTTON, DocumentType.SCRIPTING];
         const blockedKeys = matchKeyNames
             ? []
             : Object.entries(DocOptions)
-                  .filter(([key, info]: [string, FInfo]) => !info?.searchable())
+                  .filter(([, info]: [string, FInfo]) => !info?.searchable())
                   .map(([key]) => key);
 
-        const exact = query.startsWith('=');
-        query = query.toLowerCase().split('=').lastElement();
+        const exact = queryIn.startsWith('=');
+        const query = queryIn.toLowerCase().split('=').lastElement();
 
         const results = new ObservableMap<Doc, string[]>();
         if (collectionDoc) {
@@ -51,7 +51,11 @@ export namespace SearchUtil {
      */
     export function documentKeys(doc: Doc) {
         const keys: { [key: string]: boolean } = {};
-        Doc.GetAllPrototypes(doc).map(proto => Object.keys(proto).forEach(key => (keys[key] = false)));
+        Doc.GetAllPrototypes(doc).map(proto =>
+            Object.keys(proto).forEach(key => {
+                keys[key] = false;
+            })
+        );
         return Array.from(Object.keys(keys));
     }
 
@@ -62,12 +66,14 @@ export namespace SearchUtil {
      * This method iterates through an array of docs and all docs within those docs, calling
      * the function func on each doc.
      */
-    export function foreachRecursiveDoc(docs: Doc[], func: (depth: number, doc: Doc) => void) {
+    export function foreachRecursiveDoc(docsIn: Doc[], func: (depth: number, doc: Doc) => void) {
+        let docs = docsIn;
         let newarray: Doc[] = [];
-        var depth = 0;
+        let depth = 0;
         const visited: Doc[] = [];
         while (docs.length > 0) {
             newarray = [];
+            // eslint-disable-next-line no-loop-func
             docs.filter(d => d && !visited.includes(d)).forEach(d => {
                 visited.push(d);
                 const fieldKey = Doc.LayoutFieldKey(d);
diff --git a/src/client/util/SettingsManager.tsx b/src/client/util/SettingsManager.tsx
index f983c29b7..8347844f7 100644
--- a/src/client/util/SettingsManager.tsx
+++ b/src/client/util/SettingsManager.tsx
@@ -1,3 +1,5 @@
+/* eslint-disable jsx-a11y/no-static-element-interactions */
+/* eslint-disable jsx-a11y/click-events-have-key-events */
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { Button, ColorPicker, Dropdown, DropdownType, EditableText, Group, NumberDropdown, Size, Toggle, ToggleType, Type } from 'browndash-components';
 import { action, computed, makeObservable, observable, reaction, runInAction } from 'mobx';
@@ -17,8 +19,8 @@ import { MainViewModal } from '../views/MainViewModal';
 import { FontIconBox } from '../views/nodes/FontIconBox/FontIconBox';
 import { GroupManager } from './GroupManager';
 import './SettingsManager.scss';
-import { undoBatch } from './UndoManager';
 import { SnappingManager } from './SnappingManager';
+import { undoable } from './UndoManager';
 
 export enum ColorScheme {
     Dark = 'Dark',
@@ -38,24 +40,104 @@ export class SettingsManager extends React.Component<{}> {
     // eslint-disable-next-line no-use-before-define
     public static Instance: SettingsManager;
     static _settingsStyle = addStyleSheet();
-    @observable public isOpen = false;
-    @observable private passwordResultText = '';
-    @observable private playgroundMode = false;
+    @observable private _passwordResultText = '';
+    @observable private _playgroundMode = false;
 
-    @observable private curr_password = '';
-    @observable private new_password = '';
-    @observable private new_confirm = '';
+    @observable private _curr_password = '';
+    @observable private _new_password = '';
+    @observable private _new_confirm = '';
     @observable private _lastPressedSidebarBtn: Opt<Doc> = undefined; // bcz: this is a hack to handle highlighting buttons in the leftpanel menu .. need to find a cleaner approach
-    @observable activeTab = 'Accounts';
+    @observable private _activeTab = 'Accounts';
+    @observable private _isOpen = false;
 
     @observable public propertiesWidth: number = 0;
 
+    private googleAuthorize = action(() => GoogleAuthenticationManager.Instance.fetchOrGenerateAccessToken(true));
+
+    public closeMgr = action(() => {
+        this._isOpen = false;
+    });
+    public openMgr = action(() => {
+        this._isOpen = true;
+    });
+
+    private matchSystem = undoable(() => {
+        if (Doc.UserDoc().userThemeSystem) {
+            if (window.matchMedia('(prefers-color-scheme: dark)').matches) this.changeColorScheme(ColorScheme.Dark);
+            if (window.matchMedia('(prefers-color-scheme: light)').matches) this.changeColorScheme(ColorScheme.Light);
+        }
+    }, 'match system theme');
+    private setFreeformScrollMode = undoable((mode: string) => {
+        Doc.UserDoc().freeformScrollMode = mode;
+    }, 'set scroll mode');
+    private selectUserMode = undoable((mode: string) => {
+        Doc.noviceMode = mode === 'Novice';
+    }, 'change user mode');
+    private changeFontFamily = undoable((font: string) => {
+        Doc.UserDoc().fontFamily = font;
+    }, 'change font family');
+    private switchUserBackgroundColor = undoable((color: string) => {
+        Doc.UserDoc().userBackgroundColor = color;
+        addStyleSheetRule(SettingsManager._settingsStyle, 'lm_header', { background: `${color} !important` });
+    }, 'change background color');
+    private switchUserColor = undoable((color: string) => {
+        Doc.UserDoc().userColor = color;
+    }, 'change user color');
+    switchUserVariantColor = undoable((color: string) => {
+        Doc.UserDoc().userVariantColor = color;
+    }, 'change variant color');
+    userThemeSystemToggle = undoable(() => {
+        Doc.UserDoc().userThemeSystem = !Doc.UserDoc().userThemeSystem;
+        this.matchSystem();
+    }, 'change theme color');
+    playgroundModeToggle = undoable(
+        action(() => {
+            this._playgroundMode = !this._playgroundMode;
+            if (this._playgroundMode) {
+                DocServer.Control.makeReadOnly();
+                addStyleSheetRule(SettingsManager._settingsStyle, 'topbar-inner-container', { background: 'red !important' });
+            } else ClientUtils.CurrentUserEmail() !== 'guest' && DocServer.Control.makeEditable();
+        }),
+        'set playgorund mode'
+    );
+    changeColorScheme = undoable(
+        action((scheme: string) => {
+            Doc.UserDoc().userTheme = scheme;
+            switch (scheme) {
+                case ColorScheme.Light:
+                    this.switchUserColor('#323232');
+                    this.switchUserBackgroundColor('#DFDFDF');
+                    this.switchUserVariantColor('#BDDDF5');
+                    break;
+                case ColorScheme.Dark:
+                    this.switchUserColor('#DFDFDF');
+                    this.switchUserBackgroundColor('#323232');
+                    this.switchUserVariantColor('#4476F7');
+                    break;
+                case ColorScheme.CoolBlue:
+                    this.switchUserColor('#ADEAFF');
+                    this.switchUserBackgroundColor('#060A15');
+                    this.switchUserVariantColor('#3C51FF');
+                    break;
+                case ColorScheme.Cupcake:
+                    this.switchUserColor('#3BC7FF');
+                    this.switchUserBackgroundColor('#fffdf7');
+                    this.switchUserVariantColor('#FFD7F3');
+                    break;
+                case ColorScheme.Custom:
+                    break;
+                default:
+            }
+        }),
+        'change color scheme'
+    );
+
     constructor(props: {}) {
         super(props);
         makeObservable(this);
         SettingsManager.Instance = this;
         this.matchSystem();
-        window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', e => {
+        window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
             if (Doc.UserDoc().userThemeSystem) {
                 if (window.matchMedia('(prefers-color-scheme: dark)').matches) this.changeColorScheme(ColorScheme.Dark);
                 if (window.matchMedia('(prefers-color-scheme: light)').matches) this.changeColorScheme(ColorScheme.Light);
@@ -73,26 +155,9 @@ export class SettingsManager extends React.Component<{}> {
         );
         SnappingManager.SettingsStyle = SettingsManager._settingsStyle;
     }
-    matchSystem = () => {
-        if (Doc.UserDoc().userThemeSystem) {
-            if (window.matchMedia('(prefers-color-scheme: dark)').matches) this.changeColorScheme(ColorScheme.Dark);
-            if (window.matchMedia('(prefers-color-scheme: light)').matches) this.changeColorScheme(ColorScheme.Light);
-        }
-    };
-
-    public close = action(() => (this.isOpen = false));
-    public open = action(() => (this.isOpen = true));
 
-    private googleAuthorize = action(() => GoogleAuthenticationManager.Instance.fetchOrGenerateAccessToken(true));
-    private changePassword = async () => {
-        if (!(this.curr_password && this.new_password && this.new_confirm)) {
-            runInAction(() => (this.passwordResultText = "Error: Hey, we're missing some fields!"));
-        } else {
-            const passwordBundle = { curr_pass: this.curr_password, new_pass: this.new_password, new_confirm: this.new_confirm };
-            const { error } = await Networking.PostToServer('/internalResetPassword', passwordBundle);
-            runInAction(() => (this.passwordResultText = error ? 'Error: ' + error[0].msg + '...' : 'Password successfully updated!'));
-        }
-    };
+    public get LastPressedBtn() { return this._lastPressedSidebarBtn; } // prettier-ignore
+    public set LastPressedBtn(state:Doc|undefined) { this._lastPressedSidebarBtn = state; } // prettier-ignore
 
     @computed public static get userColor() {
         return StrCast(Doc.UserDoc().userColor);
@@ -106,60 +171,6 @@ export class SettingsManager extends React.Component<{}> {
         return StrCast(Doc.UserDoc().userBackgroundColor);
     }
 
-    public get LastPressedBtn() { return this._lastPressedSidebarBtn; } // prettier-ignore
-    public SetLastPressedBtn = (state?:Doc) => runInAction(() => (this._lastPressedSidebarBtn = state)); // prettier-ignore
-
-    @undoBatch selectUserMode = action((mode: string) => (Doc.noviceMode = mode === 'Novice'));
-    @undoBatch changelayout_showTitle = action((e: React.ChangeEvent) => (Doc.UserDoc().layout_showTitle = (e.currentTarget as any).value ? 'title' : undefined));
-    @undoBatch changeFontFamily = action((font: string) => (Doc.UserDoc().fontFamily = font));
-    @undoBatch changeFontSize = action((val: number) => (Doc.UserDoc().fontSize = val));
-    @undoBatch switchUserBackgroundColor = action((color: string) => {
-        Doc.UserDoc().userBackgroundColor = color;
-        addStyleSheetRule(SettingsManager._settingsStyle, 'lm_header', { background: `${color} !important` });
-    });
-    @undoBatch switchUserColor = action((color: string) => (Doc.UserDoc().userColor = color));
-    @undoBatch switchUserVariantColor = action((color: string) => (Doc.UserDoc().userVariantColor = color));
-    @undoBatch userThemeSystemToggle = action(() => {
-        Doc.UserDoc().userThemeSystem = !Doc.UserDoc().userThemeSystem;
-        this.matchSystem();
-    });
-    @undoBatch playgroundModeToggle = action(() => {
-        this.playgroundMode = !this.playgroundMode;
-        if (this.playgroundMode) {
-            DocServer.Control.makeReadOnly();
-            addStyleSheetRule(SettingsManager._settingsStyle, 'topbar-inner-container', { background: 'red !important' });
-        } else ClientUtils.CurrentUserEmail() !== 'guest' && DocServer.Control.makeEditable();
-    });
-
-    @undoBatch
-    changeColorScheme = action((scheme: string) => {
-        Doc.UserDoc().userTheme = scheme;
-        switch (scheme) {
-            case ColorScheme.Light:
-                this.switchUserColor('#323232');
-                this.switchUserBackgroundColor('#DFDFDF');
-                this.switchUserVariantColor('#BDDDF5');
-                break;
-            case ColorScheme.Dark:
-                this.switchUserColor('#DFDFDF');
-                this.switchUserBackgroundColor('#323232');
-                this.switchUserVariantColor('#4476F7');
-                break;
-            case ColorScheme.CoolBlue:
-                this.switchUserColor('#ADEAFF');
-                this.switchUserBackgroundColor('#060A15');
-                this.switchUserVariantColor('#3C51FF');
-                break;
-            case ColorScheme.Cupcake:
-                this.switchUserColor('#3BC7FF');
-                this.switchUserBackgroundColor('#fffdf7');
-                this.switchUserVariantColor('#FFD7F3');
-                break;
-            case ColorScheme.Custom:
-                break;
-        }
-    });
-
     @computed get colorsContent() {
         const schemeMap = Array.from(Object.keys(ColorScheme));
         const userTheme = StrCast(Doc.UserDoc().userTheme);
@@ -227,7 +238,9 @@ export class SettingsManager extends React.Component<{}> {
                     formLabel="Show document header"
                     formLabelPlacement="right"
                     toggleType={ToggleType.SWITCH}
-                    onClick={e => (Doc.UserDoc().layout_showTitle = Doc.UserDoc().layout_showTitle ? undefined : 'author_date')}
+                    onClick={() => {
+                        Doc.UserDoc().layout_showTitle = Doc.UserDoc().layout_showTitle ? undefined : 'author_date';
+                    }}
                     toggleStatus={Doc.UserDoc().layout_showTitle !== undefined}
                     size={Size.XSMALL}
                     color={SettingsManager.userColor}
@@ -236,8 +249,10 @@ export class SettingsManager extends React.Component<{}> {
                     formLabel="Show Full Toolbar"
                     formLabelPlacement="right"
                     toggleType={ToggleType.SWITCH}
-                    onClick={e => (Doc.UserDoc()['documentLinksButton-fullMenu'] = !Doc.UserDoc()['documentLinksButton-fullMenu'])}
-                    toggleStatus={BoolCast(Doc.UserDoc()['documentLinksButton-fullMenu'])}
+                    onClick={() => {
+                        Doc.UserDoc().documentLinksButton_fullMenu = !Doc.UserDoc().documentLinksButton_fullMenu;
+                    }}
+                    toggleStatus={BoolCast(Doc.UserDoc().documentLinksButton_fullMenu)}
                     size={Size.XSMALL}
                     color={SettingsManager.userColor}
                 />
@@ -245,7 +260,9 @@ export class SettingsManager extends React.Component<{}> {
                     formLabel="Show Button Labels"
                     formLabelPlacement="right"
                     toggleType={ToggleType.SWITCH}
-                    onClick={e => (FontIconBox.ShowIconLabels = !FontIconBox.ShowIconLabels)}
+                    onClick={() => {
+                        FontIconBox.ShowIconLabels = !FontIconBox.ShowIconLabels;
+                    }}
                     toggleStatus={FontIconBox.ShowIconLabels}
                     size={Size.XSMALL}
                     color={SettingsManager.userColor}
@@ -254,7 +271,9 @@ export class SettingsManager extends React.Component<{}> {
                     formLabel="Recognize Ink Gestures"
                     formLabelPlacement="right"
                     toggleType={ToggleType.SWITCH}
-                    onClick={e => (GestureOverlay.RecognizeGestures = !GestureOverlay.RecognizeGestures)}
+                    onClick={() => {
+                        GestureOverlay.RecognizeGestures = !GestureOverlay.RecognizeGestures;
+                    }}
                     toggleStatus={GestureOverlay.RecognizeGestures}
                     size={Size.XSMALL}
                     color={SettingsManager.userColor}
@@ -263,7 +282,9 @@ export class SettingsManager extends React.Component<{}> {
                     formLabel="Hide Labels In Ink Shapes"
                     formLabelPlacement="right"
                     toggleType={ToggleType.SWITCH}
-                    onClick={e => (Doc.UserDoc().activeInkHideTextLabels = !Doc.UserDoc().activeInkHideTextLabels)}
+                    onClick={() => {
+                        Doc.UserDoc().activeInkHideTextLabels = !Doc.UserDoc().activeInkHideTextLabels;
+                    }}
                     toggleStatus={BoolCast(Doc.UserDoc().activeInkHideTextLabels)}
                     size={Size.XSMALL}
                     color={SettingsManager.userColor}
@@ -272,7 +293,9 @@ export class SettingsManager extends React.Component<{}> {
                     formLabel="Open Ink Docs in Lightbox"
                     formLabelPlacement="right"
                     toggleType={ToggleType.SWITCH}
-                    onClick={e => (Doc.UserDoc().openInkInLightbox = !Doc.UserDoc().openInkInLightbox)}
+                    onClick={() => {
+                        Doc.UserDoc().openInkInLightbox = !Doc.UserDoc().openInkInLightbox;
+                    }}
                     toggleStatus={BoolCast(Doc.UserDoc().openInkInLightbox)}
                     size={Size.XSMALL}
                     color={SettingsManager.userColor}
@@ -281,7 +304,9 @@ export class SettingsManager extends React.Component<{}> {
                     formLabel="Show Link Lines"
                     formLabelPlacement="right"
                     toggleType={ToggleType.SWITCH}
-                    onClick={e => (Doc.UserDoc().showLinkLines = !Doc.UserDoc().showLinkLines)}
+                    onClick={() => {
+                        Doc.UserDoc().showLinkLines = !Doc.UserDoc().showLinkLines;
+                    }}
                     toggleStatus={BoolCast(Doc.UserDoc().showLinkLines)}
                     size={Size.XSMALL}
                     color={SettingsManager.userColor}
@@ -296,7 +321,9 @@ export class SettingsManager extends React.Component<{}> {
                         step={2}
                         type={Type.TERT}
                         unit="px"
-                        setNumber={val => console.log('GOT: ' + (Doc.UserDoc().headerHeight = val))}
+                        setNumber={val => {
+                            Doc.UserDoc().headerHeight = val;
+                        }}
                     />
                 </Group>
             </div>
@@ -320,7 +347,6 @@ export class SettingsManager extends React.Component<{}> {
 
     @computed get textContent() {
         const fontFamilies = ['Times New Roman', 'Arial', 'Georgia', 'Comic Sans MS', 'Tahoma', 'Impact', 'Crimson Text', 'Roboto'];
-        const fontSizes = ['7px', '8px', '9px', '10px', '12px', '14px', '16px', '18px', '20px', '24px', '32px', '48px', '72px'];
 
         return (
             <div className="tab-content appearances-content">
@@ -338,19 +364,19 @@ export class SettingsManager extends React.Component<{}> {
                                 type={Type.PRIM}
                                 number={NumCast(Doc.UserDoc().fontSize, Number(StrCast(Doc.UserDoc().fontSize).replace('px', '')))}
                                 unit="px"
-                                setNumber={val => (Doc.UserDoc().fontSize = val + 'px')}
+                                setNumber={val => {
+                                    Doc.UserDoc().fontSize = val + 'px';
+                                }}
                             />
                             <Dropdown
-                                items={fontFamilies.map(val => {
-                                    return {
-                                        text: val,
-                                        val: val,
-                                        style: {
-                                            fontFamily: val,
-                                        },
-                                    };
-                                })}
-                                closeOnSelect={true}
+                                items={fontFamilies.map(val => ({
+                                    text: val,
+                                    val: val,
+                                    style: {
+                                        fontFamily: val,
+                                    },
+                                }))}
+                                closeOnSelect
                                 dropdownType={DropdownType.SELECT}
                                 type={Type.TERT}
                                 selectedVal={StrCast(Doc.UserDoc().fontFamily)}
@@ -367,28 +393,13 @@ export class SettingsManager extends React.Component<{}> {
         );
     }
 
-    @action
-    changeVal = (value: string, pass: string) => {
-        switch (pass) {
-            case 'curr':
-                this.curr_password = value;
-                break;
-            case 'new':
-                this.new_password = value;
-                break;
-            case 'conf':
-                this.new_confirm = value;
-                break;
-        }
-    };
-
     @computed get passwordContent() {
         return (
             <div className="password-content">
                 <EditableText placeholder="Current password" type={Type.SEC} color={SettingsManager.userColor} val="" setVal={val => this.changeVal(val as string, 'curr')} fillWidth password />
                 <EditableText placeholder="New password" type={Type.SEC} color={SettingsManager.userColor} val="" setVal={val => this.changeVal(val as string, 'new')} fillWidth password />
                 <EditableText placeholder="Confirm new password" type={Type.SEC} color={SettingsManager.userColor} val="" setVal={val => this.changeVal(val as string, 'conf')} fillWidth password />
-                {!this.passwordResultText ? null : <div className={`${this.passwordResultText.startsWith('Error') ? 'error' : 'success'}-text`}>{this.passwordResultText}</div>}
+                {!this._passwordResultText ? null : <div className={`${this._passwordResultText.startsWith('Error') ? 'error' : 'success'}-text`}>{this._passwordResultText}</div>}
                 <Button type={Type.SEC} text="Forgot Password" color={SettingsManager.userColor} />
                 <Button type={Type.TERT} text="Submit" onClick={this.changePassword} color={SettingsManager.userColor} />
             </div>
@@ -418,10 +429,6 @@ export class SettingsManager extends React.Component<{}> {
         );
     }
 
-    setFreeformScrollMode = (mode: string) => {
-        Doc.UserDoc().freeformScrollMode = mode;
-    };
-
     @computed get modesContent() {
         return (
             <div className="tab-content modes-content">
@@ -430,7 +437,7 @@ export class SettingsManager extends React.Component<{}> {
                     <div className="tab-column-content">
                         <Dropdown
                             formLabel="Mode"
-                            closeOnSelect={true}
+                            closeOnSelect
                             items={[
                                 {
                                     text: 'Novice',
@@ -454,7 +461,7 @@ export class SettingsManager extends React.Component<{}> {
                             color={SettingsManager.userColor}
                             fillWidth
                         />
-                        <Toggle formLabel="Playground Mode" toggleType={ToggleType.SWITCH} toggleStatus={this.playgroundMode} onClick={this.playgroundModeToggle} color={SettingsManager.userColor} />
+                        <Toggle formLabel="Playground Mode" toggleType={ToggleType.SWITCH} toggleStatus={this._playgroundMode} onClick={this.playgroundModeToggle} color={SettingsManager.userColor} />
                     </div>
                     <div className="tab-column-title" style={{ marginTop: 20, marginBottom: 10 }}>
                         Freeform Navigation
@@ -462,7 +469,7 @@ export class SettingsManager extends React.Component<{}> {
                     <div className="tab-column-content">
                         <Dropdown
                             formLabel="Scroll Mode"
-                            closeOnSelect={true}
+                            closeOnSelect
                             items={[
                                 {
                                     text: 'Scroll to Pan',
@@ -493,10 +500,28 @@ export class SettingsManager extends React.Component<{}> {
                             formLabel="Default access private"
                             color={SettingsManager.userColor}
                             toggleStatus={BoolCast(Doc.defaultAclPrivate)}
-                            onClick={action(() => (Doc.defaultAclPrivate = !Doc.defaultAclPrivate))}
+                            onClick={action(() => {
+                                Doc.defaultAclPrivate = !Doc.defaultAclPrivate;
+                            })}
+                        />
+                        <Toggle
+                            toggleType={ToggleType.SWITCH}
+                            formLabel="Enable Sharing UI"
+                            color={SettingsManager.userColor}
+                            toggleStatus={BoolCast(Doc.IsSharingEnabled)}
+                            onClick={action(() => {
+                                Doc.IsSharingEnabled = !Doc.IsSharingEnabled;
+                            })}
+                        />
+                        <Toggle
+                            toggleType={ToggleType.SWITCH}
+                            formLabel="Disable Info UI"
+                            color={SettingsManager.userColor}
+                            toggleStatus={BoolCast(Doc.IsInfoUIDisabled)}
+                            onClick={action(() => {
+                                Doc.IsInfoUIDisabled = !Doc.IsInfoUIDisabled;
+                            })}
                         />
-                        <Toggle toggleType={ToggleType.SWITCH} formLabel="Enable Sharing UI" color={SettingsManager.userColor} toggleStatus={BoolCast(Doc.IsSharingEnabled)} onClick={action(() => (Doc.IsSharingEnabled = !Doc.IsSharingEnabled))} />
-                        <Toggle toggleType={ToggleType.SWITCH} formLabel="Disable Info UI" color={SettingsManager.userColor} toggleStatus={BoolCast(Doc.IsInfoUIDisabled)} onClick={action(() => (Doc.IsInfoUIDisabled = !Doc.IsInfoUIDisabled))} />
                     </div>
                 </div>
             </div>
@@ -518,7 +543,7 @@ export class SettingsManager extends React.Component<{}> {
                 <div className="settings-panel" style={{ background: SettingsManager.userColor }}>
                     <div className="settings-tabs">
                         {tabs.map(tab => {
-                            const isActive = this.activeTab === tab.title;
+                            const isActive = this._activeTab === tab.title;
                             return (
                                 <div
                                     key={tab.title}
@@ -527,7 +552,9 @@ export class SettingsManager extends React.Component<{}> {
                                         color: isActive ? SettingsManager.userColor : SettingsManager.userBackgroundColor,
                                     }}
                                     className={'tab-control ' + (isActive ? 'active' : 'inactive')}
-                                    onClick={action(() => (this.activeTab = tab.title))}>
+                                    onClick={action(() => {
+                                        this._activeTab = tab.title;
+                                    })}>
                                     {tab.title}
                                 </div>
                             );
@@ -544,12 +571,12 @@ export class SettingsManager extends React.Component<{}> {
                 </div>
 
                 <div className="close-button">
-                    <Button icon={<FontAwesomeIcon icon="times" size="lg" />} onClick={this.close} color={SettingsManager.userColor} />
+                    <Button icon={<FontAwesomeIcon icon="times" size="lg" />} onClick={this.closeMgr} color={SettingsManager.userColor} />
                 </div>
 
                 <div className="settings-content" style={{ color: SettingsManager.userColor, background: SettingsManager.userBackgroundColor }}>
                     {tabs.map(tab => (
-                        <div key={tab.title} className={'tab-section ' + (this.activeTab === tab.title ? 'active' : 'inactive')}>
+                        <div key={tab.title} className={'tab-section ' + (this._activeTab === tab.title ? 'active' : 'inactive')}>
                             {tab.ele}
                         </div>
                     ))}
@@ -558,13 +585,37 @@ export class SettingsManager extends React.Component<{}> {
         );
     }
 
+    private changePassword = async () => {
+        if (!(this._curr_password && this._new_password && this._new_confirm)) {
+            runInAction(() => {
+                this._passwordResultText = "Error: Hey, we're missing some fields!";
+            });
+        } else {
+            const passwordBundle = { curr_pass: this._curr_password, new_pass: this._new_password, new_confirm: this._new_confirm };
+            const { error } = await Networking.PostToServer('/internalResetPassword', passwordBundle);
+            runInAction(() => {
+                this._passwordResultText = error ? 'Error: ' + error[0].msg + '...' : 'Password successfully updated!';
+            });
+        }
+    };
+
+    @action
+    changeVal = (value: string, pass: string) => {
+        switch (pass) {
+            case 'curr': this._curr_password = value; break;
+            case 'new':  this._new_password = value;  break;
+            case 'conf': this._new_confirm = value;  break;
+            default:
+        } // prettier-ignore
+    };
+
     render() {
         return (
             <MainViewModal
                 contents={this.settingsInterface}
-                isDisplayed={this.isOpen}
-                interactive={true}
-                closeOnExternalClick={this.close}
+                isDisplayed={this._isOpen}
+                interactive
+                closeOnExternalClick={this.closeMgr}
                 dialogueBoxStyle={{ width: 'fit-content', height: '300px', background: Cast(Doc.UserDoc().userColor, 'string', null) }}
             />
         );
diff --git a/src/client/util/SharingManager.tsx b/src/client/util/SharingManager.tsx
index ade4cc218..6676e4e03 100644
--- a/src/client/util/SharingManager.tsx
+++ b/src/client/util/SharingManager.tsx
@@ -1,3 +1,6 @@
+/* eslint-disable jsx-a11y/label-has-associated-control */
+/* eslint-disable jsx-a11y/no-static-element-interactions */
+/* eslint-disable jsx-a11y/click-events-have-key-events */
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { Button, IconButton, Size, Type } from 'browndash-components';
 import { concat, intersection } from 'lodash';
@@ -65,7 +68,10 @@ interface ValidatedUser {
 
 @observer
 export class SharingManager extends React.Component<{}> {
+    // eslint-disable-next-line no-use-before-define
     public static Instance: SharingManager;
+    private shareDocumentButtonRef: React.RefObject<HTMLButtonElement> = React.createRef(); // ref for the share button, used for the position of the popup
+    private populating: boolean = false; // whether the list of users is populating or not
     @observable private isOpen = false; // whether the SharingManager modal is open or not
     @observable public users: ValidatedUser[] = []; // the list of users with sharing docs
     @observable private targetDoc: Doc | undefined = undefined; // the document being shared
@@ -77,11 +83,9 @@ export class SharingManager extends React.Component<{}> {
     @observable private permissions: SharingPermissions = SharingPermissions.Edit; // the permission with which to share with other users
     @observable private individualSort: 'ascending' | 'descending' | 'none' = 'none'; // sorting options for the list of individuals
     @observable private groupSort: 'ascending' | 'descending' | 'none' = 'none'; // sorting options for the list of groups
-    private shareDocumentButtonRef: React.RefObject<HTMLButtonElement> = React.createRef(); // ref for the share button, used for the position of the popup
     // if both showUserOptions and showGroupOptions are false then both are displayed
     @observable private showUserOptions: boolean = false; // whether to show individuals as options when sharing (in the react-select component)
     @observable private showGroupOptions: boolean = false; // // whether to show groups as options when sharing (in the react-select component)
-    private populating: boolean = false; // whether the list of users is populating or not
     @observable private upgradeNested: boolean = false; // whether child docs in a collection/dashboard should be changed to be less private - initially selected so default is upgrade all
     @observable private layoutDocAcls: boolean = false; // whether the layout doc or data doc's acls are to be used
     @observable private myDocAcls: boolean = false; // whether the My Docs checkbox is selected or not
@@ -90,33 +94,6 @@ export class SharingManager extends React.Component<{}> {
     //     return this.targetDoc ? this.targetDoc["acl-" + PublicKey] !== SharingPermissions.None : false;
     // }
 
-    public open = (target?: DocumentView, target_doc?: Doc) => {
-        this.populateUsers();
-        runInAction(() => {
-            this.targetDocView = target;
-            this.targetDoc = target_doc || target?.Document;
-            DictationOverlay.Instance.hasActiveModal = true;
-            this.isOpen = this.targetDoc !== undefined;
-            this.permissions = SharingPermissions.Augment;
-            this.upgradeNested = true;
-        });
-    };
-
-    public close = action(() => {
-        this.isOpen = false;
-        this.selectedUsers = null; // resets the list of users and selected users (in the react-select component)
-        TaskCompletionBox.taskCompleted = false;
-        setTimeout(
-            action(() => {
-                // this.copied = false;
-                DictationOverlay.Instance.hasActiveModal = false;
-                this.targetDoc = undefined;
-            }),
-            500
-        );
-        this.layoutDocAcls = false;
-    });
-
     constructor(props: {}) {
         super(props);
         makeObservable(this);
@@ -131,32 +108,334 @@ export class SharingManager extends React.Component<{}> {
     }
 
     /**
-     * Populates the list of validated users (this.users) by adding registered users which have a sharingDocument.
+     * Handles changes in the users selected in react-select
      */
-    populateUsers = async () => {
-        if (!this.populating && Doc.UserDoc()[Id] !== Utils.GuestID()) {
-            this.populating = true;
-            const userList = await RequestPromise.get(ClientUtils.prepend('/getUsers'));
-            const raw = (JSON.parse(userList) as User[]).filter(user => user.email !== 'guest' && user.email !== ClientUtils.CurrentUserEmail());
-            runInAction(() => {
-                FieldLoader.ServerLoadStatus.message = 'users';
+    @action
+    handleUsersChange = (selectedOptions: any) => {
+        this.selectedUsers = selectedOptions as UserOptions[];
+    };
+
+    /**
+     * Handles changes in the permission chosen to share with someone with
+     */
+    handlePermissionsChange = undoable(
+        action((event: React.ChangeEvent<HTMLSelectElement>) => {
+            this.permissions = event.currentTarget.value as SharingPermissions;
+        }),
+        'permission change'
+    );
+
+    /**
+     * @returns the main interface of the SharingManager.
+     */
+    @computed get sharingInterface() {
+        if (!this.targetDoc) return null;
+        TraceMobx();
+        const groupList = GroupManager.Instance?.allGroups || [];
+
+        const sortedUsers = this.users
+            .slice()
+            .sort(this.sortUsers)
+            .map(({ user: { email } }) => ({ label: email, value: indType + email }));
+        const sortedGroups = groupList
+            .slice()
+            .sort(this.sortGroups)
+            .map(({ title }) => ({ label: StrCast(title), value: groupType + StrCast(title) }));
+
+        // the next block handles the users shown (individuals/groups/both)
+        const options: GroupedOptions[] = [];
+        if (GroupManager.Instance) {
+            if ((this.showUserOptions && this.showGroupOptions) || (!this.showUserOptions && !this.showGroupOptions)) {
+                options.push({ label: 'Individuals', options: sortedUsers }, { label: 'Groups', options: sortedGroups });
+            } else if (this.showUserOptions) options.push({ label: 'Individuals', options: sortedUsers });
+            else options.push({ label: 'Groups', options: sortedGroups });
+        }
+
+        const users = this.individualSort === 'ascending' ? this.users.slice().sort(this.sortUsers) : this.individualSort === 'descending' ? this.users.slice().sort(this.sortUsers).reverse() : this.users;
+        const groups = this.groupSort === 'ascending' ? groupList.slice().sort(this.sortGroups) : this.groupSort === 'descending' ? groupList.slice().sort(this.sortGroups).reverse() : groupList;
+
+        let docs = SelectionManager.Views.length < 2 ? [this.targetDoc] : SelectionManager.Views.map(docView => docView.Document);
+
+        if (this.myDocAcls) {
+            const newDocs: Doc[] = [];
+            SearchUtil.foreachRecursiveDoc(docs, (depth, doc) => newDocs.push(doc));
+            docs = newDocs.filter(doc => GetEffectiveAcl(doc) === AclAdmin);
+        }
+
+        const targetDoc = this.layoutDocAcls ? docs[0] : docs[0]?.[DocData];
+
+        // tslint:disable-next-line: no-unnecessary-callback-wrapper
+        const effectiveAcls = docs.map(doc => GetEffectiveAcl(doc));
+        const admin = this.myDocAcls ? Boolean(docs.length) : effectiveAcls.every(acl => acl === AclAdmin);
+
+        // users in common between all docs
+        const commonKeys = intersection(docs).reduce((list, doc) => (doc?.[DocAcl] ? [...list, ...Object.keys(doc[DocAcl])] : list), [] as string[]);
+
+        // the list of users shared with
+        const userListContents = users
+            // .filter(({ user }) => (docs.length > 1 ? commonKeys.includes(`acl-${normalizeEmail(user.email)}`) : docs[0]?.author !== user.email))
+            .filter(({ user }) => docs[0]?.author !== user.email)
+            .map(({ user, linkDatabase, sharingDoc, userColor }) => {
+                const userKey = `acl-${normalizeEmail(user.email)}`;
+                const uniform = docs.every(doc => doc?.[DocAcl]?.[userKey] === docs[0]?.[DocAcl]?.[userKey]);
+                // const permissions = uniform ? StrCast(targetDoc?.[userKey]) : '-multiple-';
+                let permissions = targetDoc[DocAcl][userKey] ? HierarchyMapping.get(targetDoc[DocAcl][userKey])?.name : StrCast(targetDoc[userKey]);
+                permissions = uniform ? StrCast(targetDoc?.[userKey]) : '-multiple-';
+
+                return !permissions ? null : (
+                    <div key={userKey} className="container">
+                        <span className="padding">{user.email}</span>
+                        <div className="edit-actions">
+                            {admin || this.myDocAcls ? (
+                                <select className={`permissions-dropdown-${permissions}`} value={permissions} onChange={e => this.setInternalSharing({ user, linkDatabase, sharingDoc, userColor }, e.currentTarget.value, undefined)}>
+                                    {this.sharingOptions(uniform)}
+                                </select>
+                            ) : (
+                                <div className={`permissions-dropdown-${permissions}`}>
+                                    {concat(ReverseHierarchyMap.get(permissions)?.image, ' ', permissions)}
+                                    &nbsp;
+                                </div>
+                            )}
+                        </div>
+                    </div>
+                );
             });
-            const docs = await DocServer.GetRefFields(raw.reduce((list, user) => [...list, user.sharingDocumentId, user.linkDatabaseId], [] as string[]));
-            raw.map(
-                action((newUser: User) => {
-                    const sharingDoc = docs[newUser.sharingDocumentId];
-                    const linkDatabase = docs[newUser.linkDatabaseId];
-                    if (sharingDoc instanceof Doc && linkDatabase instanceof Doc) {
-                        if (!this.users.find(user => user.user.email === newUser.email)) {
-                            this.users.push({ user: newUser, sharingDoc, linkDatabase, userColor: StrCast(sharingDoc.userColor) });
-                            //LinkManager.addLinkDB(linkDatabase);
-                        }
-                    }
-                })
+
+        // checks if every doc has the same author
+        const sameAuthor = docs.every(doc => doc?.author === docs[0]?.author);
+
+        // the owner of the doc and the current user are placed at the top of the user list.
+        const userKey = `acl-${normalizeEmail(ClientUtils.CurrentUserEmail())}`;
+        const curUserPermission = StrCast(targetDoc[userKey]);
+        // const curUserPermission = HierarchyMapping.get(effectiveAcls[0])!.name
+        userListContents.unshift(
+            sameAuthor ? (
+                <div key="owner" className="container">
+                    <span className="padding">{targetDoc?.author === ClientUtils.CurrentUserEmail() ? 'Me' : StrCast(targetDoc?.author)}</span>
+                    <div className="edit-actions">
+                        <div className="permissions-dropdown">Owner</div>
+                    </div>
+                </div>
+            ) : null,
+            sameAuthor && targetDoc?.author !== ClientUtils.CurrentUserEmail() ? (
+                <div key="me" className="container">
+                    <span className="padding">Me</span>
+                    <div className="edit-actions">
+                        <div className={`permissions-dropdown-${curUserPermission}`}>
+                            {effectiveAcls.every(acl => acl === effectiveAcls[0]) ? concat(ReverseHierarchyMap.get(curUserPermission!)?.image, ' ', curUserPermission) : '-multiple-'}
+                            &nbsp;
+                        </div>
+                    </div>
+                </div>
+            ) : null
+        );
+
+        // the list of groups shared with
+        const groupListMap: (Doc | { title: string })[] = groups.filter(({ title }) => (docs.length > 1 ? commonKeys.includes(`acl-${normalizeEmail(StrCast(title))}`) : true));
+        groupListMap.unshift({ title: 'Guest' }); // , { title: "ALL" });
+        const groupListContents = groupListMap.map(group => {
+            const groupKey = `acl-${StrCast(group.title)}`;
+            const uniform = docs.every(doc => doc?.[DocAcl]?.[groupKey] === docs[0]?.[DocAcl]?.[groupKey]);
+            const permissions = uniform ? StrCast(targetDoc?.[groupKey]) : '-multiple-';
+
+            return !permissions ? null : (
+                <div key={groupKey} className="container" style={{ background: SettingsManager.userBackgroundColor, color: SettingsManager.userColor }}>
+                    <div className="padding">{StrCast(group.title)}</div>
+                    &nbsp;
+                    {group instanceof Doc ? (
+                        <IconButton
+                            icon={<FontAwesomeIcon icon="info-circle" />}
+                            size={Size.XSMALL}
+                            color={SettingsManager.userColor}
+                            onClick={action(() => {
+                                GroupManager.Instance.currentGroup = group;
+                            })}
+                        />
+                    ) : null}
+                    <div className="edit-actions">
+                        {admin || this.myDocAcls ? (
+                            <select className={`permissions-dropdown-${permissions}`} value={permissions} onChange={e => this.setInternalGroupSharing(group, e.currentTarget.value)}>
+                                {this.sharingOptions(uniform, group.title === 'Guest')}
+                            </select>
+                        ) : (
+                            <div className={`permissions-dropdown-${permissions}`}>
+                                {concat(ReverseHierarchyMap.get(permissions)?.image, ' ', permissions)}
+                                &nbsp;
+                            </div>
+                        )}
+                    </div>
+                </div>
             );
-            this.populating = false;
-        }
-    };
+        });
+        return (
+            <div className="sharing-interface">
+                {GroupManager.Instance?.currentGroup ? (
+                    <GroupMemberView
+                        group={GroupManager.Instance.currentGroup}
+                        onCloseButtonClick={action(() => {
+                            GroupManager.Instance.currentGroup = undefined;
+                        })}
+                    />
+                ) : null}
+                <div
+                    className="sharing-contents"
+                    style={{
+                        background: SettingsManager.userBackgroundColor,
+                        color: StrCast(Doc.UserDoc().userColor),
+                    }}>
+                    <p className="share-title" style={{ color: SettingsManager.userColor }}>
+                        <div className="share-info" onClick={() => window.open('https://brown-dash.github.io/Dash-Documentation/features/collaboration/', '_blank')}>
+                            <FontAwesomeIcon icon="question-circle" size="sm" onClick={() => window.open('https://brown-dash.github.io/Dash-Documentation/features/collaboration/', '_blank')} />
+                        </div>
+                        <b>Share </b>
+                        {this.focusOn(docs.length < 2 ? StrCast(targetDoc?.title, 'this document') : '-multiple-')}
+                    </p>
+                    <div className="share-copy-link">
+                        <Button type={Type.TERT} color={SettingsManager.userColor} icon={<FontAwesomeIcon icon="copy" size="sm" />} iconPlacement="left" text="Copy Guest URL" onClick={this.copyURL} />
+                    </div>
+                    <div className="close-button">
+                        <Button icon={<FontAwesomeIcon icon="times" size="lg" />} onClick={this.close} color={SettingsManager.userColor} />
+                    </div>
+                    {admin ? (
+                        <div className="share-container">
+                            <div className="share-setup" style={{ border: StrCast(Doc.UserDoc().userColor) }}>
+                                <Select
+                                    className="user-search"
+                                    placeholder="Enter user or group name..."
+                                    isMulti
+                                    isSearchable
+                                    closeMenuOnSelect={false}
+                                    options={options}
+                                    onKeyDown={e => e.stopPropagation()}
+                                    onChange={this.handleUsersChange}
+                                    value={this.selectedUsers}
+                                    styles={{
+                                        control: () => ({
+                                            display: 'inline-flex',
+                                            width: '100%',
+                                        }),
+                                        indicatorSeparator: () => ({
+                                            display: 'inline-flex',
+                                            visibility: 'hidden',
+                                        }),
+                                        indicatorsContainer: () => ({
+                                            display: 'inline-flex',
+                                            textDecorationColor: 'black',
+                                        }),
+                                        valueContainer: () => ({
+                                            display: 'inline-flex',
+                                            fontStyle: StrCast(Doc.UserDoc().userColor),
+                                            color: StrCast(Doc.UserDoc().userColor),
+                                            width: '100%',
+                                        }),
+                                    }}
+                                />
+                                <div className="permissions-select">
+                                    <select className={`permissions-dropdown-${this.permissions}`} onChange={this.handlePermissionsChange} value={this.permissions}>
+                                        {this.sharingOptions(true)}
+                                    </select>
+                                </div>
+                                <div className="share-button">
+                                    <Button text="SHARE" type={Type.TERT} color={SettingsManager.userColor} onClick={this.share} />
+                                </div>
+                            </div>
+                            <div className="sort-checkboxes">
+                                <input
+                                    type="checkbox"
+                                    onChange={action(() => {
+                                        this.showUserOptions = !this.showUserOptions;
+                                    })}
+                                />{' '}
+                                <label style={{ marginRight: 10 }}>Individuals</label>
+                                <input
+                                    type="checkbox"
+                                    onChange={action(() => {
+                                        this.showGroupOptions = !this.showGroupOptions;
+                                    })}
+                                />{' '}
+                                <label>Groups</label>
+                            </div>
+
+                            <div className="acl-container">
+                                {Doc.noviceMode ? null : (
+                                    <div className="layoutDoc-acls">
+                                        <input
+                                            type="checkbox"
+                                            onChange={action(() => {
+                                                this.upgradeNested = !this.upgradeNested;
+                                            })}
+                                            checked={this.upgradeNested}
+                                        />{' '}
+                                        <label>Upgrade Nested </label>
+                                        <input
+                                            type="checkbox"
+                                            onChange={action(() => {
+                                                this.layoutDocAcls = !this.layoutDocAcls;
+                                            })}
+                                            checked={this.layoutDocAcls}
+                                        />{' '}
+                                        <label>Layout</label>
+                                    </div>
+                                )}
+                            </div>
+                        </div>
+                    ) : (
+                        <div className="share-container">
+                            <div className="acl-container">
+                                <div className="layoutDoc-acls">
+                                    <input
+                                        type="checkbox"
+                                        onChange={action(() => {
+                                            this.layoutDocAcls = !this.layoutDocAcls;
+                                        })}
+                                        checked={this.layoutDocAcls}
+                                    />{' '}
+                                    <label>Layout</label>
+                                </div>
+                            </div>
+                        </div>
+                    )}
+                    <div className="main-container" style={{ color: StrCast(Doc.UserDoc().userColor), border: StrCast(Doc.UserDoc().userColor) }}>
+                        <div className="individual-container">
+                            <div
+                                className="user-sort"
+                                onClick={action(() => {
+                                    this.individualSort = this.individualSort === 'ascending' ? 'descending' : this.individualSort === 'descending' ? 'none' : 'ascending';
+                                })}>
+                                <div className="title-individual">
+                                    Individuals
+                                    <IconButton
+                                        icon={<FontAwesomeIcon icon={this.individualSort === 'ascending' ? 'caret-up' : this.individualSort === 'descending' ? 'caret-down' : 'caret-right'} />}
+                                        size={Size.XSMALL}
+                                        color={StrCast(Doc.UserDoc().userColor)}
+                                    />
+                                </div>
+                            </div>
+                            <div className="users-list">{userListContents}</div>
+                        </div>
+                        <div className="group-container">
+                            <div
+                                className="user-sort"
+                                onClick={action(() => {
+                                    this.groupSort = this.groupSort === 'ascending' ? 'descending' : this.groupSort === 'descending' ? 'none' : 'ascending';
+                                })}>
+                                <div className="title-group">
+                                    Groups
+                                    <IconButton icon={<FontAwesomeIcon icon="info-circle" />} size={Size.XSMALL} color={StrCast(Doc.UserDoc().userColor)} onClick={action(() => GroupManager.Instance.open())} />
+                                    <IconButton
+                                        icon={<FontAwesomeIcon icon={this.groupSort === 'ascending' ? 'caret-up' : this.groupSort === 'descending' ? 'caret-down' : 'caret-right'} />}
+                                        size={Size.XSMALL}
+                                        color={StrCast(Doc.UserDoc().userColor)}
+                                    />
+                                </div>
+                            </div>
+                            <div className="groups-list">{groupListContents}</div>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        );
+    }
 
     /**
      * Shares the document with a user.
@@ -200,12 +479,69 @@ export class SharingManager extends React.Component<{}> {
             }
         });
     }, 'set group permissions');
+    /**
+     * Populates the list of validated users (this.users) by adding registered users which have a sharingDocument.
+     */
+    populateUsers = async () => {
+        if (!this.populating && Doc.UserDoc()[Id] !== Utils.GuestID()) {
+            this.populating = true;
+            const userList = await RequestPromise.get(ClientUtils.prepend('/getUsers'));
+            const raw = (JSON.parse(userList) as User[]).filter(user => user.email !== 'guest' && user.email !== ClientUtils.CurrentUserEmail());
+            runInAction(() => {
+                FieldLoader.ServerLoadStatus.message = 'users';
+            });
+            const docs = await DocServer.GetRefFields(raw.reduce((list, user) => [...list, user.sharingDocumentId, user.linkDatabaseId], [] as string[]));
+            raw.map(
+                action((newUser: User) => {
+                    const sharingDoc = docs[newUser.sharingDocumentId];
+                    const linkDatabase = docs[newUser.linkDatabaseId];
+                    if (sharingDoc instanceof Doc && linkDatabase instanceof Doc) {
+                        if (!this.users.find(user => user.user.email === newUser.email)) {
+                            this.users.push({ user: newUser, sharingDoc, linkDatabase, userColor: StrCast(sharingDoc.userColor) });
+                            // LinkManager.addLinkDB(linkDatabase);
+                        }
+                    }
+                })
+            );
+            this.populating = false;
+        }
+    };
+
+    // eslint-disable-next-line react/sort-comp
+    public close = action(() => {
+        this.isOpen = false;
+        this.selectedUsers = null; // resets the list of users and selected users (in the react-select component)
+        TaskCompletionBox.taskCompleted = false;
+        setTimeout(
+            action(() => {
+                // this.copied = false;
+                DictationOverlay.Instance.hasActiveModal = false;
+                this.targetDoc = undefined;
+            }),
+            500
+        );
+        this.layoutDocAcls = false;
+    });
+
+    // eslint-disable-next-line react/no-unused-class-component-methods
+    public open = (target?: DocumentView, targetDoc?: Doc) => {
+        this.populateUsers();
+        runInAction(() => {
+            this.targetDocView = target;
+            this.targetDoc = targetDoc || target?.Document;
+            DictationOverlay.Instance.hasActiveModal = true;
+            this.isOpen = this.targetDoc !== undefined;
+            this.permissions = SharingPermissions.Augment;
+            this.upgradeNested = true;
+        });
+    };
 
     /**
      * Shares the documents shared with a group with a new user who has been added to that group.
      * @param group
      * @param emailId
      */
+    // eslint-disable-next-line react/no-unused-class-component-methods
     shareWithAddedMember = (group: Doc, emailId: string, retry: boolean = true) => {
         const user = this.users.find(({ user: { email } }) => email === emailId)!;
         const self = this;
@@ -231,6 +567,7 @@ export class SharingManager extends React.Component<{}> {
     /**
      * Called from the properties sidebar to change permissions of a user.
      */
+    // eslint-disable-next-line react/no-unused-class-component-methods
     shareFromPropertiesSidebar = undoable((shareWith: string, permission: SharingPermissions, docs: Doc[], layout: boolean) => {
         if (layout) this.layoutDocAcls = true;
         if (shareWith !== 'Guest') {
@@ -254,6 +591,7 @@ export class SharingManager extends React.Component<{}> {
      * @param group
      * @param emailId
      */
+    // eslint-disable-next-line react/no-unused-class-component-methods
     removeMember = (group: Doc, emailId: string) => {
         const user: ValidatedUser = this.users.find(({ user: { email } }) => email === emailId)!;
 
@@ -277,6 +615,7 @@ export class SharingManager extends React.Component<{}> {
      * Removes a group's permissions from documents that have been shared with it.
      * @param group
      */
+    // eslint-disable-next-line react/no-unused-class-component-methods
     removeGroup = (group: Doc) => {
         if (group.docsShared) {
             DocListCast(group.docsShared).forEach(doc => {
@@ -299,25 +638,12 @@ export class SharingManager extends React.Component<{}> {
     //     targetDoc["acl-" + PublicKey] = permission;
     // }s
 
-    /**
-     * Copies the Public sharing url to the user's clipboard.
-     */
-    private copyURL = (e: any) => {
-        ClientUtils.CopyText(ClientUtils.shareUrl(this.targetDoc![Id]));
-    };
-
-    /**
-     * Returns the SharingPermissions (Admin, Can Edit etc) access that's used to share
-     */
-    private sharingOptions(uniform: boolean, showGuestOptions?: boolean) {
-        const dropdownValues: string[] = showGuestOptions ? [SharingPermissions.None, SharingPermissions.View] : Object.values(SharingPermissions);
-        if (!uniform) dropdownValues.unshift('-multiple-');
-        return dropdownValues.map(permission => (
-            <option key={permission} value={permission}>
-                {concat(ReverseHierarchyMap.get(permission)?.image, ' ', permission)}
-            </option>
-        ));
-    }
+    /**
+     * Copies the Public sharing url to the user's clipboard.
+     */
+    private copyURL = () => {
+        ClientUtils.CopyText(ClientUtils.shareUrl(this.targetDoc![Id]));
+    };
 
     private focusOn = (contents: string) => {
         const title = this.targetDoc ? StrCast(this.targetDoc.title) : '';
@@ -350,24 +676,6 @@ export class SharingManager extends React.Component<{}> {
         );
     };
 
-    /**
-     * Handles changes in the users selected in react-select
-     */
-    @action
-    handleUsersChange = (selectedOptions: any) => {
-        this.selectedUsers = selectedOptions as UserOptions[];
-    };
-
-    /**
-     * Handles changes in the permission chosen to share with someone with
-     */
-    handlePermissionsChange = undoable(
-        action((event: React.ChangeEvent<HTMLSelectElement>) => {
-            this.permissions = event.currentTarget.value as SharingPermissions;
-        }),
-        'permission change'
-    );
-
     /**
      * Calls the relevant method for sharing, displays the popup, and resets the relevant variables.
      */
@@ -389,7 +697,9 @@ export class SharingManager extends React.Component<{}> {
                     TaskCompletionBox.textDisplayed = 'Document shared!';
                     TaskCompletionBox.taskCompleted = true;
                     setTimeout(
-                        action(() => (TaskCompletionBox.taskCompleted = false)),
+                        action(() => {
+                            TaskCompletionBox.taskCompleted = false;
+                        }),
                         2000
                     );
                 }
@@ -418,263 +728,20 @@ export class SharingManager extends React.Component<{}> {
         const g2 = StrCast(group2.title);
         return g1 < g2 ? -1 : g1 === g2 ? 0 : 1;
     };
-
     /**
-     * @returns the main interface of the SharingManager.
+     * Returns the SharingPermissions (Admin, Can Edit etc) access that's used to share
      */
-    @computed get sharingInterface() {
-        if (!this.targetDoc) return null;
-        TraceMobx();
-        const groupList = GroupManager.Instance?.allGroups || [];
-
-        const sortedUsers = this.users
-            .slice()
-            .sort(this.sortUsers)
-            .map(({ user: { email } }) => ({ label: email, value: indType + email }));
-        const sortedGroups = groupList
-            .slice()
-            .sort(this.sortGroups)
-            .map(({ title }) => ({ label: StrCast(title), value: groupType + StrCast(title) }));
-
-        // the next block handles the users shown (individuals/groups/both)
-        const options: GroupedOptions[] = [];
-        if (GroupManager.Instance) {
-            if ((this.showUserOptions && this.showGroupOptions) || (!this.showUserOptions && !this.showGroupOptions)) {
-                options.push({ label: 'Individuals', options: sortedUsers }, { label: 'Groups', options: sortedGroups });
-            } else if (this.showUserOptions) options.push({ label: 'Individuals', options: sortedUsers });
-            else options.push({ label: 'Groups', options: sortedGroups });
-        }
-
-        const users = this.individualSort === 'ascending' ? this.users.slice().sort(this.sortUsers) : this.individualSort === 'descending' ? this.users.slice().sort(this.sortUsers).reverse() : this.users;
-        const groups = this.groupSort === 'ascending' ? groupList.slice().sort(this.sortGroups) : this.groupSort === 'descending' ? groupList.slice().sort(this.sortGroups).reverse() : groupList;
-
-        let docs = SelectionManager.Views.length < 2 ? [this.targetDoc] : SelectionManager.Views.map(docView => docView.Document);
-
-        if (this.myDocAcls) {
-            const newDocs: Doc[] = [];
-            SearchUtil.foreachRecursiveDoc(docs, (depth, doc) => newDocs.push(doc));
-            docs = newDocs.filter(doc => GetEffectiveAcl(doc) === AclAdmin);
-        }
-
-        const targetDoc = this.layoutDocAcls ? docs[0] : docs[0]?.[DocData];
-
-        // tslint:disable-next-line: no-unnecessary-callback-wrapper
-        const effectiveAcls = docs.map(doc => GetEffectiveAcl(doc));
-        const admin = this.myDocAcls ? Boolean(docs.length) : effectiveAcls.every(acl => acl === AclAdmin);
-
-        // users in common between all docs
-        const commonKeys = intersection(docs).reduce((list, doc) => (doc?.[DocAcl] ? [...list, ...Object.keys(doc[DocAcl])] : list), [] as string[]);
-
-        // the list of users shared with
-        const userListContents = users
-            // .filter(({ user }) => (docs.length > 1 ? commonKeys.includes(`acl-${normalizeEmail(user.email)}`) : docs[0]?.author !== user.email))
-            .filter(({ user }) => docs[0]?.author !== user.email)
-            .map(({ user, linkDatabase, sharingDoc, userColor }) => {
-                const userKey = `acl-${normalizeEmail(user.email)}`;
-                const uniform = docs.every(doc => doc?.[DocAcl]?.[userKey] === docs[0]?.[DocAcl]?.[userKey]);
-                // const permissions = uniform ? StrCast(targetDoc?.[userKey]) : '-multiple-';
-                let permissions = targetDoc[DocAcl][userKey] ? HierarchyMapping.get(targetDoc[DocAcl][userKey])?.name : StrCast(targetDoc[userKey]);
-                permissions = uniform ? StrCast(targetDoc?.[userKey]) : '-multiple-';
-
-                return !permissions ? null : (
-                    <div key={userKey} className={'container'}>
-                        <span className={'padding'}>{user.email}</span>
-                        <div className="edit-actions">
-                            {admin || this.myDocAcls ? (
-                                <select className={`permissions-dropdown-${permissions}`} value={permissions} onChange={e => this.setInternalSharing({ user, linkDatabase, sharingDoc, userColor }, e.currentTarget.value, undefined)}>
-                                    {this.sharingOptions(uniform)}
-                                </select>
-                            ) : (
-                                <div className={`permissions-dropdown-${permissions}`}>
-                                    {concat(ReverseHierarchyMap.get(permissions)?.image, ' ', permissions)}
-                                    &nbsp;
-                                </div>
-                            )}
-                        </div>
-                    </div>
-                );
-            });
-
-        // checks if every doc has the same author
-        const sameAuthor = docs.every(doc => doc?.author === docs[0]?.author);
-
-        // the owner of the doc and the current user are placed at the top of the user list.
-        const userKey = `acl-${normalizeEmail(ClientUtils.CurrentUserEmail())}`;
-        const curUserPermission = StrCast(targetDoc[userKey]);
-        // const curUserPermission = HierarchyMapping.get(effectiveAcls[0])!.name
-        userListContents.unshift(
-            sameAuthor ? (
-                <div key={'owner'} className={'container'}>
-                    <span className="padding">{targetDoc?.author === ClientUtils.CurrentUserEmail() ? 'Me' : StrCast(targetDoc?.author)}</span>
-                    <div className="edit-actions">
-                        <div className={'permissions-dropdown'}>Owner</div>
-                    </div>
-                </div>
-            ) : null,
-            sameAuthor && targetDoc?.author !== ClientUtils.CurrentUserEmail() ? (
-                <div key={'me'} className={'container'}>
-                    <span className={'padding'}>Me</span>
-                    <div className="edit-actions">
-                        <div className={`permissions-dropdown-${curUserPermission}`}>
-                            {effectiveAcls.every(acl => acl === effectiveAcls[0]) ? concat(ReverseHierarchyMap.get(curUserPermission!)?.image, ' ', curUserPermission) : '-multiple-'}
-                            &nbsp;
-                        </div>
-                    </div>
-                </div>
-            ) : null
-        );
-
-        // the list of groups shared with
-        const groupListMap: (Doc | { title: string })[] = groups.filter(({ title }) => (docs.length > 1 ? commonKeys.includes(`acl-${normalizeEmail(StrCast(title))}`) : true));
-        groupListMap.unshift({ title: 'Guest' }); //, { title: "ALL" });
-        const groupListContents = groupListMap.map(group => {
-            let groupKey = `acl-${StrCast(group.title)}`;
-            const uniform = docs.every(doc => doc?.[DocAcl]?.[groupKey] === docs[0]?.[DocAcl]?.[groupKey]);
-            const permissions = uniform ? StrCast(targetDoc?.[groupKey]) : '-multiple-';
-
-            return !permissions ? null : (
-                <div key={groupKey} className={'container'} style={{ background: SettingsManager.userBackgroundColor, color: SettingsManager.userColor }}>
-                    <div className={'padding'}>{StrCast(group.title)}</div>
-                    &nbsp;
-                    {group instanceof Doc ? <IconButton icon={<FontAwesomeIcon icon={'info-circle'} />} size={Size.XSMALL} color={SettingsManager.userColor} onClick={action(() => (GroupManager.Instance.currentGroup = group))} /> : null}
-                    <div className={'edit-actions'}>
-                        {admin || this.myDocAcls ? (
-                            <select className={`permissions-dropdown-${permissions}`} value={permissions} onChange={e => this.setInternalGroupSharing(group, e.currentTarget.value)}>
-                                {this.sharingOptions(uniform, group.title === 'Guest')}
-                            </select>
-                        ) : (
-                            <div className={`permissions-dropdown-${permissions}`}>
-                                {concat(ReverseHierarchyMap.get(permissions)?.image, ' ', permissions)}
-                                &nbsp;
-                            </div>
-                        )}
-                    </div>
-                </div>
-            );
-        });
-        return (
-            <div className="sharing-interface">
-                {GroupManager.Instance?.currentGroup ? <GroupMemberView group={GroupManager.Instance.currentGroup} onCloseButtonClick={action(() => (GroupManager.Instance.currentGroup = undefined))} /> : null}
-                <div
-                    className="sharing-contents"
-                    style={{
-                        background: SettingsManager.userBackgroundColor,
-                        color: StrCast(Doc.UserDoc().userColor),
-                    }}>
-                    <p className="share-title" style={{ color: SettingsManager.userColor }}>
-                        <div className="share-info" onClick={() => window.open('https://brown-dash.github.io/Dash-Documentation/features/collaboration/', '_blank')}>
-                            <FontAwesomeIcon icon={'question-circle'} size={'sm'} onClick={() => window.open('https://brown-dash.github.io/Dash-Documentation/features/collaboration/', '_blank')} />
-                        </div>
-                        <b>Share </b>
-                        {this.focusOn(docs.length < 2 ? StrCast(targetDoc?.title, 'this document') : '-multiple-')}
-                    </p>
-                    <div className="share-copy-link">
-                        <Button type={Type.TERT} color={SettingsManager.userColor} icon={<FontAwesomeIcon icon={'copy'} size="sm" />} iconPlacement={'left'} text={'Copy Guest URL'} onClick={this.copyURL} />
-                    </div>
-                    <div className="close-button">
-                        <Button icon={<FontAwesomeIcon icon={'times'} size={'lg'} />} onClick={this.close} color={SettingsManager.userColor} />
-                    </div>
-                    {admin ? (
-                        <div className="share-container">
-                            <div className="share-setup" style={{ border: StrCast(Doc.UserDoc().userColor) }}>
-                                <Select
-                                    className="user-search"
-                                    placeholder="Enter user or group name..."
-                                    isMulti
-                                    isSearchable
-                                    closeMenuOnSelect={false}
-                                    options={options}
-                                    onKeyDown={e => e.stopPropagation()}
-                                    onChange={this.handleUsersChange}
-                                    value={this.selectedUsers}
-                                    styles={{
-                                        control: () => ({
-                                            display: 'inline-flex',
-                                            width: '100%',
-                                        }),
-                                        indicatorSeparator: () => ({
-                                            display: 'inline-flex',
-                                            visibility: 'hidden',
-                                        }),
-                                        indicatorsContainer: () => ({
-                                            display: 'inline-flex',
-                                            textDecorationColor: 'black',
-                                        }),
-                                        valueContainer: () => ({
-                                            display: 'inline-flex',
-                                            fontStyle: StrCast(Doc.UserDoc().userColor),
-                                            color: StrCast(Doc.UserDoc().userColor),
-                                            width: '100%',
-                                        }),
-                                    }}
-                                />
-                                <div className="permissions-select">
-                                    <select className={`permissions-dropdown-${this.permissions}`} onChange={this.handlePermissionsChange} value={this.permissions}>
-                                        {this.sharingOptions(true)}
-                                    </select>
-                                </div>
-                                <div className="share-button">
-                                    <Button text={'SHARE'} type={Type.TERT} color={SettingsManager.userColor} onClick={this.share} />
-                                </div>
-                            </div>
-                            <div className="sort-checkboxes">
-                                <input type="checkbox" onChange={action(() => (this.showUserOptions = !this.showUserOptions))} /> <label style={{ marginRight: 10 }}>Individuals</label>
-                                <input type="checkbox" onChange={action(() => (this.showGroupOptions = !this.showGroupOptions))} /> <label>Groups</label>
-                            </div>
-
-                            <div className="acl-container">
-                                {Doc.noviceMode ? null : (
-                                    <div className="layoutDoc-acls">
-                                        <input type="checkbox" onChange={action(() => (this.upgradeNested = !this.upgradeNested))} checked={this.upgradeNested} /> <label>Upgrade Nested </label>
-                                        <input type="checkbox" onChange={action(() => (this.layoutDocAcls = !this.layoutDocAcls))} checked={this.layoutDocAcls} /> <label>Layout</label>
-                                    </div>
-                                )}
-                            </div>
-                        </div>
-                    ) : (
-                        <div className="share-container">
-                            <div className="acl-container">
-                                <div className="layoutDoc-acls">
-                                    <input type="checkbox" onChange={action(() => (this.layoutDocAcls = !this.layoutDocAcls))} checked={this.layoutDocAcls} /> <label>Layout</label>
-                                </div>
-                            </div>
-                        </div>
-                    )}
-                    <div className="main-container" style={{ color: StrCast(Doc.UserDoc().userColor), border: StrCast(Doc.UserDoc().userColor) }}>
-                        <div className={'individual-container'}>
-                            <div className="user-sort" onClick={action(() => (this.individualSort = this.individualSort === 'ascending' ? 'descending' : this.individualSort === 'descending' ? 'none' : 'ascending'))}>
-                                <div className="title-individual">
-                                    Individuals
-                                    <IconButton
-                                        icon={<FontAwesomeIcon icon={this.individualSort === 'ascending' ? 'caret-up' : this.individualSort === 'descending' ? 'caret-down' : 'caret-right'} />}
-                                        size={Size.XSMALL}
-                                        color={StrCast(Doc.UserDoc().userColor)}
-                                    />
-                                </div>
-                            </div>
-                            <div className="users-list">{userListContents}</div>
-                        </div>
-                        <div className={'group-container'}>
-                            <div className="user-sort" onClick={action(() => (this.groupSort = this.groupSort === 'ascending' ? 'descending' : this.groupSort === 'descending' ? 'none' : 'ascending'))}>
-                                <div className="title-group">
-                                    Groups
-                                    <IconButton icon={<FontAwesomeIcon icon={'info-circle'} />} size={Size.XSMALL} color={StrCast(Doc.UserDoc().userColor)} onClick={action(() => GroupManager.Instance.open())} />
-                                    <IconButton
-                                        icon={<FontAwesomeIcon icon={this.groupSort === 'ascending' ? 'caret-up' : this.groupSort === 'descending' ? 'caret-down' : 'caret-right'} />}
-                                        size={Size.XSMALL}
-                                        color={StrCast(Doc.UserDoc().userColor)}
-                                    />
-                                </div>
-                            </div>
-                            <div className={'groups-list'}>{groupListContents}</div>
-                        </div>
-                    </div>
-                </div>
-            </div>
-        );
+    private sharingOptions(uniform: boolean, showGuestOptions?: boolean) {
+        const dropdownValues: string[] = showGuestOptions ? [SharingPermissions.None, SharingPermissions.View] : Object.values(SharingPermissions);
+        if (!uniform) dropdownValues.unshift('-multiple-');
+        return dropdownValues.map(permission => (
+            <option key={permission} value={permission}>
+                {concat(ReverseHierarchyMap.get(permission)?.image, ' ', permission)}
+            </option>
+        ));
     }
 
     render() {
-        return <MainViewModal contents={this.sharingInterface} isDisplayed={this.isOpen} interactive={true} dialogueBoxDisplayedOpacity={this.dialogueBoxOpacity} overlayDisplayedOpacity={this.overlayOpacity} closeOnExternalClick={this.close} />;
+        return <MainViewModal contents={this.sharingInterface} isDisplayed={this.isOpen} interactive dialogueBoxDisplayedOpacity={this.dialogueBoxOpacity} overlayDisplayedOpacity={this.overlayOpacity} closeOnExternalClick={this.close} />;
     }
 }
diff --git a/src/client/util/SnappingManager.ts b/src/client/util/SnappingManager.ts
index eb47bbe88..3da85191f 100644
--- a/src/client/util/SnappingManager.ts
+++ b/src/client/util/SnappingManager.ts
@@ -10,6 +10,7 @@ export class SnappingManager {
     @observable _shiftKey = false;
     @observable _ctrlKey = false;
     @observable _metaKey = false;
+    @observable _showPresPaths = false;
     @observable _isLinkFollowing = false;
     @observable _isDragging: boolean = false;
     @observable _isResizing: string | undefined = undefined; // the string is the Id of the document being resized
@@ -36,6 +37,7 @@ export class SnappingManager {
     public static get ShiftKey()       { return this.Instance._shiftKey; } // prettier-ignore
     public static get CtrlKey()        { return this.Instance._ctrlKey; } // prettier-ignore
     public static get MetaKey()        { return this.Instance._metaKey; } // prettier-ignore
+    public static get ShowPresPaths()  { return this.Instance._showPresPaths; } // prettier-ignore
     public static get IsLinkFollowing(){ return this.Instance._isLinkFollowing; } // prettier-ignore
     public static get IsDragging()     { return this.Instance._isDragging; } // prettier-ignore
     public static get IsResizing()     { return this.Instance._isResizing; } // prettier-ignore
@@ -44,6 +46,7 @@ export class SnappingManager {
     public static SetShiftKey       = (down: boolean) => runInAction(() => {this.Instance._shiftKey = down}); // prettier-ignore
     public static SetCtrlKey        = (down: boolean) => runInAction(() => {this.Instance._ctrlKey = down}); // prettier-ignore
     public static SetMetaKey        = (down: boolean) => runInAction(() => {this.Instance._metaKey = down}); // prettier-ignore
+    public static SetShowPresPaths  = (paths:boolean) => runInAction(() => {this.Instance._showPresPaths = paths}); // prettier-ignore
     public static SetIsLinkFollowing= (follow:boolean)=> runInAction(() => {this.Instance._isLinkFollowing = follow}); // prettier-ignore
     public static SetIsDragging     = (drag: boolean) => runInAction(() => {this.Instance._isDragging = drag}); // prettier-ignore
     public static SetIsResizing     = (docid?:string) => runInAction(() => {this.Instance._isResizing = docid}); // prettier-ignore
diff --git a/src/client/util/UndoManager.ts b/src/client/util/UndoManager.ts
index 4e941508d..956c0e674 100644
--- a/src/client/util/UndoManager.ts
+++ b/src/client/util/UndoManager.ts
@@ -1,8 +1,11 @@
+/* eslint-disable prefer-spread */
+/* eslint-disable no-use-before-define */
 import { action, observable, runInAction } from 'mobx';
 import { Without } from '../../Utils';
 import { RichTextField } from '../../fields/RichTextField';
 
-export let printToConsole = false; // Doc.MyDockedBtns.linearView_IsOpen
+// eslint-disable-next-line prefer-const
+let printToConsole = false; // Doc.MyDockedBtns.linearView_IsOpen
 
 function getBatchName(target: any, key: string | symbol): string {
     const keyName = key.toString();
@@ -38,10 +41,11 @@ function propertyDecorator(target: any, key: string | symbol) {
 }
 
 export function undoable(fn: (...args: any[]) => any, batchName: string): (...args: any[]) => any {
-    return function () {
+    return function (...fargs) {
         const batch = UndoManager.StartBatch(batchName);
         try {
-            return fn.apply(undefined, arguments as any);
+            // eslint-disable-next-line prefer-rest-params
+            return fn.apply(undefined, fargs);
         } finally {
             batch.end();
         }
@@ -49,13 +53,15 @@ export function undoable(fn: (...args: any[]) => any, batchName: string): (...ar
 }
 
 export function undoBatch(target: any, key: string | symbol, descriptor?: TypedPropertyDescriptor<any>): any;
+// eslint-disable-next-line no-redeclare
 export function undoBatch(fn: (...args: any[]) => any): (...args: any[]) => any;
+// eslint-disable-next-line no-redeclare
 export function undoBatch(target: any, key?: string | symbol, descriptor?: TypedPropertyDescriptor<any>): any {
     if (!key) {
-        return function () {
+        return function (...fargs: any[]) {
             const batch = UndoManager.StartBatch('');
             try {
-                return target.apply(undefined, arguments);
+                return target.apply(undefined, fargs);
             } finally {
                 batch.end();
             }
@@ -63,7 +69,7 @@ export function undoBatch(target: any, key?: string | symbol, descriptor?: Typed
     }
     if (!descriptor) {
         propertyDecorator(target, key);
-        return;
+        return undefined;
     }
     const oldFunction = descriptor.value;
 
@@ -87,14 +93,18 @@ export namespace UndoManager {
     }
     type UndoBatch = UndoEvent[];
 
-    export let undoStackNames: string[] = observable([]);
-    export let redoStackNames: string[] = observable([]);
-    export let undoStack: UndoBatch[] = observable([]);
-    export let redoStack: UndoBatch[] = observable([]);
     let currentBatch: UndoBatch | undefined;
-    export let batchCounter = observable.box(0);
     let undoing = false;
-    export let tempEvents: UndoEvent[] | undefined = undefined;
+    let tempEvents: UndoEvent[] | undefined;
+    export const undoStackNames: string[] = observable([]);
+    export const redoStackNames: string[] = observable([]);
+    export const undoStack: UndoBatch[] = observable([]);
+    export const redoStack: UndoBatch[] = observable([]);
+    export const batchCounter = observable.box(0);
+    let _fieldPrinter: (val: any) => string = val => val?.toString();
+    export function SetFieldPrinter(printer: (val: any) => string) {
+        _fieldPrinter = printer;
+    }
 
     export function AddEvent(event: UndoEvent, value?: any): void {
         if (currentBatch && batchCounter.get() && !undoing) {
@@ -103,8 +113,8 @@ export namespace UndoManager {
                     '                         '.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))
+                        ' = ' + // prettier-ignore
+                        (value instanceof RichTextField ? value.Text : value instanceof Array ? value.map(_fieldPrinter).join(',') : _fieldPrinter(value))
                 );
             currentBatch.push(event);
             tempEvents?.push(event);
@@ -130,21 +140,22 @@ export namespace UndoManager {
     }
     export function FilterBatches(fieldTypes: string[]) {
         const fieldCounts: { [key: string]: number } = {};
-        const lastStack = UndoManager.undoStack.slice(-1)[0]; //.lastElement();
+        const lastStack = UndoManager.undoStack.slice(-1)[0]; // .lastElement();
         if (lastStack) {
-            lastStack.forEach(ev => fieldTypes.includes(ev.prop) && (fieldCounts[ev.prop] = (fieldCounts[ev.prop] || 0) + 1));
+            lastStack.forEach(ev => {
+                fieldTypes.includes(ev.prop) && (fieldCounts[ev.prop] = (fieldCounts[ev.prop] || 0) + 1);
+            });
             const fieldCount2: { [key: string]: number } = {};
-            runInAction(
-                () =>
-                    (UndoManager.undoStack[UndoManager.undoStack.length - 1] = lastStack.filter(ev => {
-                        if (fieldTypes.includes(ev.prop)) {
-                            fieldCount2[ev.prop] = (fieldCount2[ev.prop] || 0) + 1;
-                            if (fieldCount2[ev.prop] === 1 || fieldCount2[ev.prop] === fieldCounts[ev.prop]) return true;
-                            return false;
-                        }
-                        return true;
-                    }))
-            );
+            runInAction(() => {
+                UndoManager.undoStack[UndoManager.undoStack.length - 1] = lastStack.filter(ev => {
+                    if (fieldTypes.includes(ev.prop)) {
+                        fieldCount2[ev.prop] = (fieldCount2[ev.prop] || 0) + 1;
+                        if (fieldCount2[ev.prop] === 1 || fieldCount2[ev.prop] === fieldCounts[ev.prop]) return true;
+                        return false;
+                    }
+                    return true;
+                });
+            });
         }
     }
     export function TraceOpenBatches() {
@@ -161,11 +172,10 @@ export namespace UndoManager {
             if (this.disposed) {
                 console.log('WARNING: undo batch already disposed');
                 return false;
-            } else {
-                this.disposed = true;
-                openBatches.splice(openBatches.indexOf(this));
-                return EndBatch(this.batchName, cancel);
             }
+            this.disposed = true;
+            openBatches.splice(openBatches.indexOf(this));
+            return EndBatch(this.batchName, cancel);
         };
 
         end = () => this.dispose(false);
@@ -183,7 +193,7 @@ export namespace UndoManager {
 
     const EndBatch = action((batchName: string, cancel: boolean = false) => {
         runInAction(() => batchCounter.set(batchCounter.get() - 1));
-        printToConsole && console.log('                          '.slice(0, batchCounter.get()) + 'End ' + batchName + ' (' + currentBatch?.length + ')');
+        printToConsole && console.log('                          '.slice(0, batchCounter.get()) + 'End ' + batchName + ' (' + (currentBatch?.length ?? 0) + ')');
         if (batchCounter.get() === 0 && currentBatch?.length) {
             if (!cancel) {
                 undoStack.push(currentBatch);
@@ -200,10 +210,10 @@ export namespace UndoManager {
     export function StartTempBatch() {
         tempEvents = [];
     }
-    export function EndTempBatch<T>(success: boolean) {
+    export function EndTempBatch(success: boolean) {
         UndoManager.UndoTempBatch(success);
     }
-    //TODO Make this return the return value
+    // TODO Make this return the return value
     export function RunInBatch<T>(fn: () => T, batchName: string) {
         const batch = StartBatch(batchName);
         try {
@@ -235,9 +245,11 @@ export namespace UndoManager {
         }
 
         undoing = true;
-        for (let i = commands.length - 1; i >= 0; i--) {
-            commands[i].undo();
-        }
+        // eslint-disable-next-line prettier/prettier
+        commands
+            .slice()
+            .reverse()
+            .forEach(command => command.undo());
         undoing = false;
 
         redoStackNames.push(names ?? '???');
@@ -256,9 +268,7 @@ export namespace UndoManager {
         }
 
         undoing = true;
-        for (const command of commands) {
-            command.redo();
-        }
+        commands.forEach(command => command.redo());
         undoing = false;
 
         undoStackNames.push(names ?? '???');
diff --git a/src/client/util/reportManager/ReportManager.tsx b/src/client/util/reportManager/ReportManager.tsx
index 02b3ee32c..2224e642d 100644
--- a/src/client/util/reportManager/ReportManager.tsx
+++ b/src/client/util/reportManager/ReportManager.tsx
@@ -1,3 +1,6 @@
+/* eslint-disable jsx-a11y/label-has-associated-control */
+/* eslint-disable jsx-a11y/media-has-caption */
+/* eslint-disable react/no-unused-class-component-methods */
 import { Octokit } from '@octokit/core';
 import { Button, Dropdown, DropdownType, IconButton, Type } from 'browndash-components';
 import { action, makeObservable, observable } from 'mobx';
@@ -13,7 +16,7 @@ import { ClientUtils } from '../../../ClientUtils';
 import { Doc } from '../../../fields/Doc';
 import { StrCast } from '../../../fields/Types';
 import { MainViewModal } from '../../views/MainViewModal';
-import '.././SettingsManager.scss';
+import '../SettingsManager.scss';
 import { SettingsManager } from '../SettingsManager';
 import './ReportManager.scss';
 import { Filter, FormInput, FormTextArea, IssueCard, IssueView } from './ReportManagerComponents';
@@ -25,10 +28,12 @@ import { BugType, FileData, Priority, ReportForm, ViewState, bugDropdownItems, d
  */
 @observer
 export class ReportManager extends React.Component<{}> {
+    // eslint-disable-next-line no-use-before-define
     public static Instance: ReportManager;
     @observable private isOpen = false;
 
     @observable private query = '';
+    // eslint-disable-next-line react/sort-comp
     @action private setQuery = (q: string) => {
         this.query = q;
     };
@@ -83,7 +88,9 @@ export class ReportManager extends React.Component<{}> {
         this.formData = newData;
     });
 
-    public close = action(() => (this.isOpen = false));
+    public close = action(() => {
+        this.isOpen = false;
+    });
     public open = action(async () => {
         this.isOpen = true;
         if (this.shownIssues.length === 0) {
@@ -165,7 +172,7 @@ export class ReportManager extends React.Component<{}> {
      * @returns JSX element of a piece of media (image, video, audio)
      */
     private getMediaPreview = (fileData: FileData): JSX.Element => {
-        const file = fileData.file;
+        const { file } = fileData;
         const mimeType = file.type;
         const preview = URL.createObjectURL(file);
 
@@ -180,7 +187,8 @@ export class ReportManager extends React.Component<{}> {
                     </div>
                 </div>
             );
-        } else if (mimeType.startsWith('video/')) {
+        }
+        if (mimeType.startsWith('video/')) {
             return (
                 <div key={fileData._id} className="report-media-wrapper">
                     <div className="report-media-content">
@@ -194,7 +202,8 @@ export class ReportManager extends React.Component<{}> {
                     </div>
                 </div>
             );
-        } else if (mimeType.startsWith('audio/')) {
+        }
+        if (mimeType.startsWith('audio/')) {
             return (
                 <div key={fileData._id} className="report-audio-wrapper">
                     <audio src={preview} controls />
@@ -204,7 +213,7 @@ export class ReportManager extends React.Component<{}> {
                 </div>
             );
         }
-        return <></>;
+        return <div />;
     };
 
     /**
@@ -307,8 +316,8 @@ export class ReportManager extends React.Component<{}> {
                 <div className="report-selects">
                     <Dropdown
                         color={StrCast(Doc.UserDoc().userColor)}
-                        formLabel={'Type'}
-                        closeOnSelect={true}
+                        formLabel="Type"
+                        closeOnSelect
                         items={bugDropdownItems}
                         selectedVal={this.formData.type}
                         setSelectedVal={val => {
@@ -320,8 +329,8 @@ export class ReportManager extends React.Component<{}> {
                     />
                     <Dropdown
                         color={StrCast(Doc.UserDoc().userColor)}
-                        formLabel={'Priority'}
-                        closeOnSelect={true}
+                        formLabel="Priority"
+                        closeOnSelect
                         items={priorityDropdownItems}
                         selectedVal={this.formData.priority}
                         setSelectedVal={val => {
@@ -347,7 +356,7 @@ export class ReportManager extends React.Component<{}> {
                         text="Submit"
                         type={Type.TERT}
                         color={StrCast(Doc.UserDoc().userVariantColor)}
-                        icon={<ReactLoading type="spin" color={'#ffffff'} width={20} height={20} />}
+                        icon={<ReactLoading type="spin" color="#ffffff" width={20} height={20} />}
                         iconPlacement="right"
                         onClick={() => {
                             this.reportIssue();
@@ -364,7 +373,7 @@ export class ReportManager extends React.Component<{}> {
                     />
                 )}
                 <div style={{ position: 'absolute', top: '4px', right: '4px' }}>
-                    <IconButton color={StrCast(Doc.UserDoc().userColor)} tooltip="close" icon={<CgClose size={'16px'} />} onClick={this.close} />
+                    <IconButton color={StrCast(Doc.UserDoc().userColor)} tooltip="close" icon={<CgClose size="16px" />} onClick={this.close} />
                 </div>
             </div>
         );
@@ -376,9 +385,8 @@ export class ReportManager extends React.Component<{}> {
     private reportComponent = () => {
         if (this.viewState === ViewState.VIEW) {
             return this.viewIssuesComponent();
-        } else {
-            return this.reportIssueComponent();
         }
+        return this.reportIssueComponent();
     };
 
     render() {
@@ -386,7 +394,7 @@ export class ReportManager extends React.Component<{}> {
             <MainViewModal
                 contents={this.reportComponent()}
                 isDisplayed={this.isOpen}
-                interactive={true}
+                interactive
                 closeOnExternalClick={this.close}
                 dialogueBoxStyle={{ width: 'auto', minWidth: '300px', height: '85vh', maxHeight: '90vh', background: StrCast(Doc.UserDoc().userBackgroundColor), borderRadius: '8px' }}
             />
diff --git a/src/client/views/ContextMenuItem.tsx b/src/client/views/ContextMenuItem.tsx
index 5760872fb..eb1030eec 100644
--- a/src/client/views/ContextMenuItem.tsx
+++ b/src/client/views/ContextMenuItem.tsx
@@ -1,3 +1,4 @@
+/* eslint-disable react/jsx-props-no-spreading */
 import { IconProp } from '@fortawesome/fontawesome-svg-core';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { action, makeObservable, observable, runInAction } from 'mobx';
@@ -17,6 +18,7 @@ export interface OriginalMenuProps {
 
 export interface SubmenuProps {
     description: string;
+    // eslint-disable-next-line no-use-before-define
     subitems: ContextMenuProps[];
     noexpand?: boolean;
     addDivider?: boolean;
@@ -37,7 +39,9 @@ export class ContextMenuItem extends ObservableReactComponent<ContextMenuProps &
     }
 
     componentDidMount() {
-        runInAction(() => (this._items.length = 0));
+        runInAction(() => {
+            this._items.length = 0;
+        });
         if ((this._props as SubmenuProps)?.subitems) {
             (this._props as SubmenuProps).subitems?.forEach(i => runInAction(() => this._items.push(i)));
         }
@@ -83,7 +87,9 @@ export class ContextMenuItem extends ObservableReactComponent<ContextMenuProps &
             return;
         }
         this.currentTimeout = setTimeout(
-            action(() => (this.overItem = false)),
+            action(() => {
+                this.overItem = false;
+            }),
             ContextMenuItem.timeout
         );
     };
@@ -147,10 +153,10 @@ export class ContextMenuItem extends ObservableReactComponent<ContextMenuProps &
                     ) : null}
                     <div className="contextMenu-description" onMouseEnter={this.onPointerEnter} style={{ alignItems: 'center', alignSelf: 'center' }}>
                         {this._props.description}
-                        <FontAwesomeIcon icon={'angle-right'} size="lg" style={{ position: 'absolute', right: '10px' }} />
+                        <FontAwesomeIcon icon="angle-right" size="lg" style={{ position: 'absolute', right: '10px' }} />
                     </div>
                     <div
-                        className={`contextMenu-item-background`}
+                        className="contextMenu-item-background"
                         style={{
                             background: SnappingManager.userColor,
                         }}
@@ -159,5 +165,6 @@ export class ContextMenuItem extends ObservableReactComponent<ContextMenuProps &
                 </div>
             );
         }
+        return null;
     }
 }
diff --git a/src/client/views/DashboardView.tsx b/src/client/views/DashboardView.tsx
index 14abd5f89..25415a4f0 100644
--- a/src/client/views/DashboardView.tsx
+++ b/src/client/views/DashboardView.tsx
@@ -1,3 +1,5 @@
+/* eslint-disable jsx-a11y/no-static-element-interactions */
+/* eslint-disable jsx-a11y/click-events-have-key-events */
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { Button, ColorPicker, EditableText, Size, Type } from 'browndash-components';
 import { action, computed, makeObservable, observable } from 'mobx';
@@ -48,10 +50,18 @@ export class DashboardView extends ObservableReactComponent<{}> {
     @observable private selectedDashboardGroup = DashboardGroup.MyDashboards;
     @observable private newDashboardName = '';
     @observable private newDashboardColor = '#AFAFAF';
-    @action abortCreateNewDashboard = () => (this.openModal = false);
-    @action setNewDashboardName = (name: string) => (this.newDashboardName = name);
-    @action setNewDashboardColor = (color: string) => (this.newDashboardColor = color);
-    @action selectDashboardGroup = (group: DashboardGroup) => (this.selectedDashboardGroup = group);
+    @action abortCreateNewDashboard = () => {
+        this.openModal = false;
+    };
+    @action setNewDashboardName = (name: string) => {
+        this.newDashboardName = name;
+    };
+    @action setNewDashboardColor = (color: string) => {
+        this.newDashboardColor = color;
+    };
+    @action selectDashboardGroup = (group: DashboardGroup) => {
+        this.selectedDashboardGroup = group;
+    };
 
     clickDashboard = (e: React.MouseEvent, dashboard: Doc) => {
         if (this.selectedDashboardGroup === DashboardGroup.SharedDashboards) {
@@ -138,9 +148,9 @@ export class DashboardView extends ObservableReactComponent<{}> {
             <>
                 <div className="dashboard-view">
                     <div className="left-menu">
-                        <Button text="My Dashboards" active={this.selectedDashboardGroup === DashboardGroup.MyDashboards} color={color} align={'flex-start'} onClick={() => this.selectDashboardGroup(DashboardGroup.MyDashboards)} fillWidth />
+                        <Button text="My Dashboards" active={this.selectedDashboardGroup === DashboardGroup.MyDashboards} color={color} align="flex-start" onClick={() => this.selectDashboardGroup(DashboardGroup.MyDashboards)} fillWidth />
                         <Button
-                            text={'Shared Dashboards' + ' (' + this.getDashboards(DashboardGroup.SharedDashboards).length + ')'}
+                            text={'Shared Dashboards (' + this.getDashboards(DashboardGroup.SharedDashboards).length + ')'}
                             active={this.selectedDashboardGroup === DashboardGroup.SharedDashboards}
                             color={this.getDashboards(DashboardGroup.SharedDashboards).some(dash => !DocListCast(Doc.MySharedDocs.viewed).includes(dash)) ? 'green' : color}
                             align="flex-start"
@@ -165,14 +175,22 @@ export class DashboardView extends ObservableReactComponent<{}> {
                                           onContextMenu={e => this.onContextMenu(dashboard, e)}
                                           onClick={e => this.clickDashboard(e, dashboard)}>
                                           <img
+                                              alt=""
                                               src={
                                                   href ??
                                                   'https://media.istockphoto.com/photos/hot-air-balloons-flying-over-the-botan-canyon-in-turkey-picture-id1297349747?b=1&k=20&m=1297349747&s=170667a&w=0&h=oH31fJty_4xWl_JQ4OIQWZKP8C6ji9Mz7L4XmEnbqRU='
                                               }
                                           />
                                           <div className="info">
-                                              <EditableText type={Type.PRIM} color={color} val={StrCast(dashboard.title)} setVal={val => (dashboard[DocData].title = val)} />
-                                              {this.selectedDashboardGroup === DashboardGroup.SharedDashboards && this.isUnviewedSharedDashboard(dashboard) ? <div>unviewed</div> : <div></div>}
+                                              <EditableText
+                                                  type={Type.PRIM}
+                                                  color={color}
+                                                  val={StrCast(dashboard.title)}
+                                                  setVal={val => {
+                                                      dashboard[DocData].title = val;
+                                                  }}
+                                              />
+                                              {this.selectedDashboardGroup === DashboardGroup.SharedDashboards && this.isUnviewedSharedDashboard(dashboard) ? <div>unviewed</div> : <div />}
                                               <div
                                                   className="more"
                                                   onPointerDown={e => {
@@ -208,7 +226,7 @@ export class DashboardView extends ObservableReactComponent<{}> {
                         )}
                     </div>
                 </div>
-                <MainViewModal contents={this.namingInterface} isDisplayed={this.openModal} interactive={true} closeOnExternalClick={this.abortCreateNewDashboard} dialogueBoxStyle={{ width: '400px', height: '180px', color: Colors.LIGHT_GRAY }} />
+                <MainViewModal contents={this.namingInterface} isDisplayed={this.openModal} interactive closeOnExternalClick={this.abortCreateNewDashboard} dialogueBoxStyle={{ width: '400px', height: '180px', color: Colors.LIGHT_GRAY }} />
             </>
         );
     }
@@ -252,6 +270,7 @@ export class DashboardView extends ObservableReactComponent<{}> {
                 //     }
                 //     CollectionView.SetSafeMode(true);
             } else if (state.nro || state.nro === null || state.readonly === false) {
+                /* empty */
             } else if (doc.readOnly) {
                 DocServer.Control.makeReadOnly();
             } else {
@@ -274,7 +293,7 @@ export class DashboardView extends ObservableReactComponent<{}> {
 
     public static resetDashboard = (dashboard: Doc) => {
         const config = StrCast(dashboard.dockingConfig);
-        const matches = config.match(/\"documentId\":\"[a-z0-9-]+\"/g);
+        const matches = config.match(/"documentId":"[a-z0-9-]+"/g);
         const docids = matches?.map(m => m.replace('"documentId":"', '').replace('"', '')) ?? [];
 
         const components =
@@ -368,7 +387,7 @@ export class DashboardView extends ObservableReactComponent<{}> {
             title: `Untitled Tab 1`,
         };
 
-        const title = name ? name : `Dashboard ${dashboardCount}`;
+        const title = name || `Dashboard ${dashboardCount}`;
         const freeformDoc = Doc.GuestTarget || Docs.Create.FreeformDocument([], freeformOptions);
         const dashboardDoc = Docs.Create.StandardCollectionDockingDocument([{ doc: freeformDoc, initialWidth: 600 }], { title: title }, id, 'row');
 
diff --git a/src/client/views/DictationOverlay.tsx b/src/client/views/DictationOverlay.tsx
index e098bc361..b242acdba 100644
--- a/src/client/views/DictationOverlay.tsx
+++ b/src/client/views/DictationOverlay.tsx
@@ -7,14 +7,14 @@ import { MainViewModal } from './MainViewModal';
 
 @observer
 export class DictationOverlay extends React.Component {
+    // eslint-disable-next-line no-use-before-define
     public static Instance: DictationOverlay;
     @observable private _dictationState = DictationManager.placeholder;
     @observable private _dictationSuccessState: boolean | undefined = undefined;
     @observable private _dictationDisplayState = false;
     @observable private _dictationListeningState: DictationManager.Controls.ListeningUIStatus = false;
 
-    public isPointerDown = false;
-    public overlayTimeout: NodeJS.Timeout | undefined;
+    // eslint-disable-next-line react/no-unused-class-component-methods
     public hasActiveModal = false;
 
     constructor(props: any) {
@@ -23,47 +23,32 @@ export class DictationOverlay extends React.Component {
         DictationOverlay.Instance = this;
     }
 
-    public initiateDictationFade = () => {
-        const duration = DictationManager.Commands.dictationFadeDuration;
-        this.overlayTimeout = setTimeout(() => {
-            this.dictationOverlayVisible = false;
-            this.dictationSuccess = undefined;
-            DictationOverlay.Instance.hasActiveModal = false;
-            setTimeout(() => (this.dictatedPhrase = DictationManager.placeholder), 500);
-        }, duration);
-    };
-    public cancelDictationFade = () => {
-        if (this.overlayTimeout) {
-            clearTimeout(this.overlayTimeout);
-            this.overlayTimeout = undefined;
-        }
-    };
-
-    @computed public get dictatedPhrase() {
-        return this._dictationState;
-    }
-    @computed public get dictationSuccess() {
-        return this._dictationSuccessState;
-    }
-    @computed public get dictationOverlayVisible() {
-        return this._dictationDisplayState;
-    }
-    @computed public get isListening() {
-        return this._dictationListeningState;
-    }
-
+    @computed public get dictatedPhrase() { return this._dictationState; } // prettier-ignore
     public set dictatedPhrase(value: string) {
-        runInAction(() => (this._dictationState = value));
+        runInAction(() => {
+            this._dictationState = value;
+        });
     }
+    @computed public get dictationSuccess() { return this._dictationSuccessState; } // prettier-ignore
     public set dictationSuccess(value: boolean | undefined) {
-        runInAction(() => (this._dictationSuccessState = value));
+        runInAction(() => { this._dictationSuccessState = value; }); // prettier-ignore
     }
+    @computed public get dictationOverlayVisible() { return this._dictationDisplayState; } // prettier-ignore
     public set dictationOverlayVisible(value: boolean) {
-        runInAction(() => (this._dictationDisplayState = value));
+        runInAction(() => { this._dictationDisplayState = value; }); // prettier-ignore
     }
+    @computed public get isListening() { return this._dictationListeningState; } // prettier-ignore
     public set isListening(value: DictationManager.Controls.ListeningUIStatus) {
-        runInAction(() => (this._dictationListeningState = value));
+        runInAction(() => { this._dictationListeningState = value; }); // prettier-ignore
     }
+    public initiateDictationFade = () => {
+        setTimeout(() => {
+            this.dictationOverlayVisible = false;
+            this.dictationSuccess = undefined;
+            DictationOverlay.Instance.hasActiveModal = false;
+            setTimeout(() => { this.dictatedPhrase = DictationManager.placeholder; }, 500); // prettier-ignore
+        }, DictationManager.Commands.dictationFadeDuration);
+    };
 
     render() {
         const success = this.dictationSuccess;
diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx
index 348b18129..fae3610ca 100644
--- a/src/client/views/DocComponent.tsx
+++ b/src/client/views/DocComponent.tsx
@@ -326,7 +326,10 @@ export function ViewBoxAnnotatableComponent<P extends FieldViewProps>() {
 
         isAnyChildContentActive = () => this._isAnyChildContentActive;
 
-        whenChildContentsActiveChanged = action((isActive: boolean) => this._props.whenChildContentsActiveChanged((this._isAnyChildContentActive = isActive)));
+        whenChildContentsActiveChanged = action((isActive: boolean) => {
+            this._isAnyChildContentActive = isActive;
+            this._props.whenChildContentsActiveChanged(isActive);
+        });
     }
     return Component;
 }
diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx
index 16586d922..63b485a43 100644
--- a/src/client/views/DocumentButtonBar.tsx
+++ b/src/client/views/DocumentButtonBar.tsx
@@ -1,3 +1,6 @@
+/* eslint-disable jsx-a11y/control-has-associated-label */
+/* eslint-disable jsx-a11y/no-static-element-interactions */
+/* eslint-disable jsx-a11y/click-events-have-key-events */
 import { IconLookup, IconProp } from '@fortawesome/fontawesome-svg-core';
 import { faCalendarDays } from '@fortawesome/free-solid-svg-icons';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
@@ -33,6 +36,7 @@ import { DashFieldView } from './nodes/formattedText/DashFieldView';
 @observer
 export class DocumentButtonBar extends ObservableReactComponent<{ views: () => (DocumentView | undefined)[]; stack?: any }> {
     private _dragRef = React.createRef<HTMLDivElement>();
+    // eslint-disable-next-line no-use-before-define
     @observable public static Instance: DocumentButtonBar;
 
     constructor(props: any) {
@@ -60,8 +64,12 @@ export class DocumentButtonBar extends ObservableReactComponent<{ views: () => (
                             key={icon.toString()}
                             size="sm"
                             icon={icon}
-                            onPointerEnter={action(e => (this.subPin = allDocs ? 'All ' : ''))}
-                            onPointerLeave={action(e => (this.subPin = ''))}
+                            onPointerEnter={action(() => {
+                                this.subPin = allDocs ? 'All ' : '';
+                            })}
+                            onPointerLeave={action(() => {
+                                this.subPin = '';
+                            })}
                             onClick={e => {
                                 this._props.views().forEach(dv => click(dv!.Document));
                                 e.stopPropagation();
@@ -77,12 +85,14 @@ export class DocumentButtonBar extends ObservableReactComponent<{ views: () => (
                 <div
                     className="documentButtonBar-icon documentButtonBar-follow"
                     style={{ backgroundColor: followLink ? Colors.LIGHT_BLUE : Colors.DARK_GRAY, color: followLink ? Colors.BLACK : Colors.WHITE }}
-                    onClick={undoBatch(e => this._props.views().map(view => view?.toggleFollowLink(undefined, false)))}>
+                    onClick={undoBatch(() => this._props.views().map(view => view?.toggleFollowLink(undefined, false)))}>
                     <div className="documentButtonBar-followTypes">
                         {followBtn(
                             true,
-                            (doc: Doc) => (doc.followAllLinks = !doc.followAllLinks),
-                            (doc?: Doc) => (doc?.followAllLinks ? true : false),
+                            (doc: Doc) => {
+                                doc.followAllLinks = !doc.followAllLinks;
+                            },
+                            (doc?: Doc) => !!doc?.followAllLinks,
                             'window-maximize'
                         )}
                     </div>
@@ -100,22 +110,22 @@ export class DocumentButtonBar extends ObservableReactComponent<{ views: () => (
                 <div className="documentButtonBar-linkTypes">
                     <Tooltip title={<div>search for target</div>}>
                         <div className="documentButtonBar-button">
-                            <button style={{ backgroundColor: 'transparent', width: 35, height: 35, display: 'flex', justifyContent: 'center', alignItems: 'center', position: 'relative' }} onPointerDown={this.toggleLinkSearch}>
-                                <FontAwesomeIcon style={{ position: 'absolute', transform: 'scale(1.5)' }} icon={'search'} size="lg" />
-                                <FontAwesomeIcon style={{ position: 'absolute', transform: 'scale(0.5)', transformOrigin: 'center', top: 9, left: 2 }} icon={'link'} size="lg" />
+                            <button type="button" style={{ backgroundColor: 'transparent', width: 35, height: 35, display: 'flex', justifyContent: 'center', alignItems: 'center', position: 'relative' }} onPointerDown={this.toggleLinkSearch}>
+                                <FontAwesomeIcon style={{ position: 'absolute', transform: 'scale(1.5)' }} icon="search" size="lg" />
+                                <FontAwesomeIcon style={{ position: 'absolute', transform: 'scale(0.5)', transformOrigin: 'center', top: 9, left: 2 }} icon="link" size="lg" />
                             </button>
                         </div>
                     </Tooltip>
                     <Tooltip title={<div>open linked trail</div>}>
                         <div className="documentButtonBar-button">
-                            <button style={{ backgroundColor: 'transparent', width: 35, height: 35, display: 'flex', justifyContent: 'center', alignItems: 'center', position: 'relative' }} onPointerDown={this.toggleTrail}>
+                            <button type="button" style={{ backgroundColor: 'transparent', width: 35, height: 35, display: 'flex', justifyContent: 'center', alignItems: 'center', position: 'relative' }} onPointerDown={this.toggleTrail}>
                                 <FontAwesomeIcon icon="taxi" size="lg" />
                             </button>
                         </div>
                     </Tooltip>
                 </div>
                 <div style={{ width: 25, height: 25 }}>
-                    <DocumentLinksButton View={this.view0} AlwaysOn={true} InMenu={true} StartLink={true} />
+                    <DocumentLinksButton View={this.view0} AlwaysOn InMenu StartLink />
                 </div>
             </div>
         );
@@ -134,8 +144,12 @@ export class DocumentButtonBar extends ObservableReactComponent<{ views: () => (
                             key={icon.toString()}
                             size="sm"
                             icon={icon}
-                            onPointerEnter={action(e => (this.subEndLink = (pinLayout ? 'Layout' : '') + (pinLayout && pinContent ? ' &' : '') + (pinContent ? ' Content' : '')))}
-                            onPointerLeave={action(e => (this.subEndLink = ''))}
+                            onPointerEnter={action(() => {
+                                this.subEndLink = (pinLayout ? 'Layout' : '') + (pinLayout && pinContent ? ' &' : '') + (pinContent ? ' Content' : '');
+                            })}
+                            onPointerLeave={action(() => {
+                                this.subEndLink = '';
+                            })}
                             onClick={e => {
                                 this.view0 &&
                                     DocumentLinksButton.finishLinkClick(e.clientX, e.clientY, DocumentLinksButton.StartLink, this.view0.Document, true, this.view0, {
@@ -157,7 +171,7 @@ export class DocumentButtonBar extends ObservableReactComponent<{ views: () => (
                     {linkBtn(false, true, 'address-card')}
                     {linkBtn(true, true, 'id-card')}
                 </div>
-                <DocumentLinksButton View={this.view0} AlwaysOn={true} InMenu={true} StartLink={false} />
+                <DocumentLinksButton View={this.view0} AlwaysOn InMenu StartLink={false} />
             </div>
         );
     }
@@ -177,15 +191,16 @@ export class DocumentButtonBar extends ObservableReactComponent<{ views: () => (
                             key={icon.toString()}
                             size="sm"
                             icon={icon}
-                            onPointerEnter={action(
-                                e =>
-                                    (this.subPin =
-                                        (pinLayoutView ? 'Layout' : '') +
-                                        (pinLayoutView && pinContentView ? ' &' : '') +
-                                        (pinContentView ? ' Content View' : '') +
-                                        (pinLayoutView && pinContentView ? '(shift+alt)' : pinLayoutView ? '(shift)' : pinContentView ? '(alt)' : ''))
-                            )}
-                            onPointerLeave={action(e => (this.subPin = ''))}
+                            onPointerEnter={action(() => {
+                                this.subPin =
+                                    (pinLayoutView ? 'Layout' : '') +
+                                    (pinLayoutView && pinContentView ? ' &' : '') +
+                                    (pinContentView ? ' Content View' : '') +
+                                    (pinLayoutView && pinContentView ? '(shift+alt)' : pinLayoutView ? '(shift)' : pinContentView ? '(alt)' : '');
+                            })}
+                            onPointerLeave={action(() => {
+                                this.subPin = '';
+                            })}
                             onClick={e => {
                                 const docs = this._props
                                     .views()
@@ -232,8 +247,8 @@ export class DocumentButtonBar extends ObservableReactComponent<{ views: () => (
     get shareButton() {
         const targetDoc = this.view0?.Document;
         return !targetDoc ? null : (
-            <Tooltip title={<div className="dash-tooltip">{'Open Sharing Manager'}</div>}>
-                <div className="documentButtonBar-icon" style={{ color: 'white' }} onClick={e => SharingManager.Instance.open(this.view0, targetDoc)}>
+            <Tooltip title={<div className="dash-tooltip">Open Sharing Manager</div>}>
+                <div className="documentButtonBar-icon" style={{ color: 'white' }} onClick={() => SharingManager.Instance.open(this.view0, targetDoc)}>
                     <FontAwesomeIcon className="documentdecorations-icon" icon="users" />
                 </div>
             </Tooltip>
@@ -244,7 +259,7 @@ export class DocumentButtonBar extends ObservableReactComponent<{ views: () => (
     get menuButton() {
         const targetDoc = this.view0?.Document;
         return !targetDoc ? null : (
-            <Tooltip title={<div className="dash-tooltip">{`Open Context Menu`}</div>}>
+            <Tooltip title={<div className="dash-tooltip">Open Context Menu</div>}>
                 <div className="documentButtonBar-icon" style={{ color: 'white', cursor: 'pointer' }} onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, e => this.openContextMenu(e))}>
                     <FontAwesomeIcon className="documentdecorations-icon" icon="bars" />
                 </div>
@@ -260,8 +275,7 @@ export class DocumentButtonBar extends ObservableReactComponent<{ views: () => (
                 <div
                     className="documentButtonBar-icon"
                     style={{ color: 'white' }}
-                    onClick={e => {
-                        console.log('hi: ', CalendarManager.Instance);
+                    onClick={() => {
                         CalendarManager.Instance.open(this.view0, targetDoc);
                     }}>
                     <FontAwesomeIcon className="documentdecorations-icon" icon={faCalendarDays as IconLookup} />
@@ -282,7 +296,18 @@ export class DocumentButtonBar extends ObservableReactComponent<{ views: () => (
                     style={{ backgroundColor: this._isRecording ? Colors.ERROR_RED : Colors.DARK_GRAY, color: Colors.WHITE }}
                     onPointerDown={action((e: React.PointerEvent) => {
                         this._isRecording = true;
-                        this._props.views().map(view => view && DocumentViewInternal.recordAudioAnnotation(view.dataDoc, view.LayoutFieldKey, stopFunc => (this._stopFunc = stopFunc), emptyFunction));
+                        this._props.views().map(
+                            view =>
+                                view &&
+                                DocumentViewInternal.recordAudioAnnotation(
+                                    view.dataDoc,
+                                    view.LayoutFieldKey,
+                                    stopFunc => {
+                                        this._stopFunc = stopFunc;
+                                    },
+                                    emptyFunction
+                                )
+                        );
                         const b = UndoManager.StartBatch('Recording');
                         setupMoveUpEvents(
                             this,
@@ -310,10 +335,10 @@ export class DocumentButtonBar extends ObservableReactComponent<{ views: () => (
         if (this._dragRef.current) {
             const dragDocView = this.view0!;
             const dragData = new DragManager.DocumentDragData([dragDocView.Document]);
-            const [left, top] = dragDocView.screenToContentsTransform().inverse().transformPoint(0, 0);
+            const origin = dragDocView.screenToContentsTransform().inverse().transformPoint(0, 0);
             dragData.defaultDropAction = dropActionType.embed;
             dragData.canEmbed = true;
-            DragManager.StartDocumentDrag([dragDocView.ContentDiv!], dragData, left, top, { hideSource: false });
+            DragManager.StartDocumentDrag([dragDocView.ContentDiv!], dragData, origin[0], origin[1], { hideSource: false });
             return true;
         }
         return false;
@@ -336,8 +361,19 @@ export class DocumentButtonBar extends ObservableReactComponent<{ views: () => (
     @computed
     get templateButton() {
         return !this.view0 ? null : (
-            <Tooltip title={<div className="dash-tooltip">Tap to Customize Layout. Drag an embedding</div>} open={this._tooltipOpen} onClose={action(() => (this._tooltipOpen = false))} placement="bottom">
-                <div className="documentButtonBar-linkFlyout" ref={this._dragRef} onPointerEnter={action(() => !this._ref.current?.getBoundingClientRect().width && (this._tooltipOpen = true))}>
+            <Tooltip
+                title={<div className="dash-tooltip">Tap to Customize Layout. Drag an embedding</div>}
+                open={this._tooltipOpen}
+                onClose={action(() => {
+                    this._tooltipOpen = false;
+                })}
+                placement="bottom">
+                <div
+                    className="documentButtonBar-linkFlyout"
+                    ref={this._dragRef}
+                    onPointerEnter={action(() => {
+                        !this._ref.current?.getBoundingClientRect().width && (this._tooltipOpen = true);
+                    })}>
                     <Popup icon={<FaEdit />} popup={this.templateMenu} popupContainsPt={returnTrue} />
                 </div>
             </Tooltip>
@@ -365,17 +401,17 @@ export class DocumentButtonBar extends ObservableReactComponent<{ views: () => (
 
     @observable _captureEndLinkLayout = false;
     @action
-    captureEndLinkLayout = (e: React.PointerEvent) => {
+    captureEndLinkLayout = () => {
         this._captureEndLinkLayout = !this._captureEndLinkLayout;
     };
     @observable _captureEndLinkContent = false;
     @action
-    captureEndLinkContent = (e: React.PointerEvent) => {
+    captureEndLinkContent = () => {
         this._captureEndLinkContent = !this._captureEndLinkContent;
     };
 
     @action
-    captureEndLinkState = (e: React.PointerEvent) => {
+    captureEndLinkState = () => {
         this._captureEndLinkContent = this._captureEndLinkLayout = !this._captureEndLinkLayout;
     };
 
@@ -402,13 +438,15 @@ export class DocumentButtonBar extends ObservableReactComponent<{ views: () => (
         return (
             <div className="documentButtonBar">
                 <div className="documentButtonBar-button">
-                    <DocumentLinksButton View={this.view0} AlwaysOn={true} InMenu={true} ShowCount={true} />
+                    <DocumentLinksButton View={this.view0} AlwaysOn InMenu ShowCount />
                 </div>
                 {this._showLinkPopup ? (
                     <div style={{ position: 'absolute', zIndex: 1000 }}>
                         <LinkPopup
                             key="popup"
-                            linkCreated={link => (link.link_displayLine = !IsFollowLinkScript(this._props.views().lastElement()?.Document.onClick))}
+                            linkCreated={link => {
+                                link.link_displayLine = !IsFollowLinkScript(this._props.views().lastElement()?.Document.onClick);
+                            }}
                             linkCreateAnchor={() => this._props.views().lastElement()?.ComponentView?.getAnchor?.(true)}
                             linkFrom={() => this._props.views().lastElement()?.Document}
                         />
@@ -423,7 +461,7 @@ export class DocumentButtonBar extends ObservableReactComponent<{ views: () => (
                 <div className="documentButtonBar-button">{this.pinButton}</div>
                 <div className="documentButtonBar-button">{this.recordButton}</div>
                 <div className="documentButtonBar-button">{this.calendarButton}</div>
-                {!Doc.UserDoc()['documentLinksButton-fullMenu'] ? null : <div className="documentButtonBar-button">{this.shareButton}</div>}
+                {!Doc.UserDoc().documentLinksButton_fullMenu ? null : <div className="documentButtonBar-button">{this.shareButton}</div>}
                 <div className="documentButtonBar-button">{this.menuButton}</div>
             </div>
         );
diff --git a/src/client/views/EditableView.tsx b/src/client/views/EditableView.tsx
index 85e893e19..684b948af 100644
--- a/src/client/views/EditableView.tsx
+++ b/src/client/views/EditableView.tsx
@@ -1,3 +1,5 @@
+/* eslint-disable jsx-a11y/no-static-element-interactions */
+/* eslint-disable jsx-a11y/click-events-have-key-events */
 import { action, IReactionDisposer, makeObservable, observable, reaction, runInAction } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
@@ -70,7 +72,7 @@ export class EditableView extends ObservableReactComponent<EditableProps> {
     constructor(props: EditableProps) {
         super(props);
         makeObservable(this);
-        this._editing = this._props.editing ? true : false;
+        this._editing = !!this._props.editing;
     }
 
     componentDidMount(): void {
@@ -166,7 +168,7 @@ export class EditableView extends ObservableReactComponent<EditableProps> {
                     this._props.menuCallback(e.currentTarget.getBoundingClientRect().x, e.currentTarget.getBoundingClientRect().y);
                     break;
                 }
-
+            // eslint-disable-next-line no-fallthrough
             default:
                 if (this._props.textCallback?.(e.key)) {
                     e.stopPropagation();
@@ -186,7 +188,6 @@ export class EditableView extends ObservableReactComponent<EditableProps> {
                 this._editing = true;
                 this._props.isEditingCallback?.(true);
             }
-            // e.stopPropagation();
         }
     };
 
@@ -223,6 +224,7 @@ export class EditableView extends ObservableReactComponent<EditableProps> {
     renderEditor() {
         return this._props.autosuggestProps ? (
             <Autosuggest
+                // eslint-disable-next-line react/jsx-props-no-spreading
                 {...this._props.autosuggestProps.autosuggestProps}
                 inputProps={{
                     className: 'editableView-input',
@@ -241,12 +243,13 @@ export class EditableView extends ObservableReactComponent<EditableProps> {
         ) : this._props.oneLine !== false && this._props.GetValue()?.toString().indexOf('\n') === -1 ? (
             <input
                 className="editableView-input"
-                ref={r => (this._inputref = r)}
+                ref={r => { this._inputref = r; }} // prettier-ignore
                 style={{ display: this._props.display, overflow: 'auto', fontSize: this._props.fontSize, minWidth: 20, background: this._props.background }}
                 placeholder={this._props.placeholder}
                 onBlur={e => this.finalizeEdit(e.currentTarget.value, false, true, false)}
                 defaultValue={this._props.GetValue()}
-                autoFocus={true}
+                // eslint-disable-next-line jsx-a11y/no-autofocus
+                autoFocus
                 onChange={this.onChange}
                 onKeyDown={this.onKeyDown}
                 onPointerDown={this.stopPropagation}
@@ -256,12 +259,13 @@ export class EditableView extends ObservableReactComponent<EditableProps> {
         ) : (
             <textarea
                 className="editableView-input"
-                ref={r => (this._inputref = r)}
+                ref={r => { this._inputref = r; }} // prettier-ignore
                 style={{ display: this._props.display, overflow: 'auto', fontSize: this._props.fontSize, minHeight: `min(100%, ${(this._props.GetValue()?.split('\n').length || 1) * 15})`, minWidth: 20, background: this._props.background }}
                 placeholder={this._props.placeholder}
                 onBlur={e => this.finalizeEdit(e.currentTarget.value, false, true, false)}
                 defaultValue={this._props.GetValue()}
-                autoFocus={true}
+                // eslint-disable-next-line jsx-a11y/no-autofocus
+                autoFocus
                 onChange={this.onChange}
                 onKeyDown={this.onKeyDown}
                 onPointerDown={this.stopPropagation}
@@ -304,7 +308,10 @@ export class EditableView extends ObservableReactComponent<EditableProps> {
                         fontStyle: this._props.fontStyle,
                         fontSize: this._props.fontSize,
                     }}>
-                    {this._props.fieldContents ? <FieldView {...this._props.fieldContents} /> : this.props.contents ? this._props.contents?.valueOf() : ''}
+                    {
+                        // eslint-disable-next-line react/jsx-props-no-spreading
+                        this._props.fieldContents ? <FieldView {...this._props.fieldContents} /> : this.props.contents ? this._props.contents?.valueOf() : ''
+                    }
                 </span>
             </div>
         );
diff --git a/src/client/views/FieldsDropdown.tsx b/src/client/views/FieldsDropdown.tsx
index 6a5c2cb4c..3cb7848c2 100644
--- a/src/client/views/FieldsDropdown.tsx
+++ b/src/client/views/FieldsDropdown.tsx
@@ -64,7 +64,7 @@ export class FieldsDropdown extends ObservableReactComponent<fieldsDropdownProps
         return (
             <Select
                 styles={{
-                    control: (baseStyles, state) => ({
+                    control: (baseStyles /* , state */) => ({
                         ...baseStyles,
                         minHeight: '5px',
                         maxHeight: '30px',
@@ -73,17 +73,17 @@ export class FieldsDropdown extends ObservableReactComponent<fieldsDropdownProps
                         padding: 0,
                         margin: 0,
                     }),
-                    singleValue: (baseStyles, state) => ({
+                    singleValue: (baseStyles /* , state */) => ({
                         ...baseStyles,
                         color: SettingsManager.userColor,
                         background: SettingsManager.userBackgroundColor,
                     }),
-                    placeholder: (baseStyles, state) => ({
+                    placeholder: (baseStyles /* , state */) => ({
                         ...baseStyles,
                         color: SettingsManager.userColor,
                         background: SettingsManager.userBackgroundColor,
                     }),
-                    input: (baseStyles, state) => ({
+                    input: (baseStyles /* , state */) => ({
                         ...baseStyles,
                         padding: 0,
                         margin: 0,
@@ -95,7 +95,7 @@ export class FieldsDropdown extends ObservableReactComponent<fieldsDropdownProps
                         color: SettingsManager.userColor,
                         background: !state.isFocused ? SettingsManager.userBackgroundColor : SettingsManager.userVariantColor,
                     }),
-                    menuList: (baseStyles, state) => ({
+                    menuList: (baseStyles /* , state */) => ({
                         ...baseStyles,
                         backgroundColor: SettingsManager.userBackgroundColor,
                     }),
@@ -106,12 +106,14 @@ export class FieldsDropdown extends ObservableReactComponent<fieldsDropdownProps
                 onChange={val => this._props.selectFunc((val as any as { value: string; label: string }).value)}
                 onKeyDown={e => {
                     if (e.key === 'Enter') {
-                        runInAction(() => this._props.selectFunc((this._newField = (e.nativeEvent.target as any)?.value)));
+                        runInAction(() => {
+                            this._props.selectFunc((this._newField = (e.nativeEvent.target as any)?.value));
+                        });
                     }
                     e.stopPropagation();
                 }}
                 onMenuClose={this._props.menuClose}
-                closeMenuOnSelect={true}
+                closeMenuOnSelect
                 value={this._props.showPlaceholder ? null : undefined}
             />
         );
diff --git a/src/client/views/FilterPanel.tsx b/src/client/views/FilterPanel.tsx
index d3cebd1c3..be307600c 100644
--- a/src/client/views/FilterPanel.tsx
+++ b/src/client/views/FilterPanel.tsx
@@ -1,3 +1,4 @@
+/* eslint-disable react/jsx-props-no-spreading */
 /* eslint-disable jsx-a11y/no-static-element-interactions */
 /* eslint-disable jsx-a11y/click-events-have-key-events */
 import { action, computed, makeObservable, observable, ObservableMap } from 'mobx';
diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts
index 5c923d301..ebd61db7d 100644
--- a/src/client/views/GlobalKeyHandler.ts
+++ b/src/client/views/GlobalKeyHandler.ts
@@ -12,7 +12,7 @@ import { SelectionManager } from '../util/SelectionManager';
 import { SettingsManager } from '../util/SettingsManager';
 import { SharingManager } from '../util/SharingManager';
 import { SnappingManager } from '../util/SnappingManager';
-import { UndoManager } from '../util/UndoManager';
+import { UndoManager, undoable } from '../util/UndoManager';
 import { ContextMenu } from './ContextMenu';
 import { DocumentDecorations } from './DocumentDecorations';
 import { InkStrokeProperties } from './InkStrokeProperties';
@@ -27,15 +27,13 @@ import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox';
 import { AnchorMenu } from './pdf/AnchorMenu';
 
 const modifiers = ['control', 'meta', 'shift', 'alt'];
-type KeyHandler = (keycode: string, e: KeyboardEvent) => KeyControlInfo;
 type KeyControlInfo = {
     preventDefault: boolean;
     stopPropagation: boolean;
 };
-
-export let CtrlKey = false;
+type KeyHandler = (keycode: string, e: KeyboardEvent) => KeyControlInfo;
 export class KeyManager {
-    public static Instance: KeyManager = new KeyManager();
+    public static Instance = new KeyManager();
     private router = new Map<string, KeyHandler>();
 
     constructor() {
@@ -49,8 +47,8 @@ export class KeyManager {
         this.router.set('1000', this.shift);
     }
 
-    public unhandle = action((e: KeyboardEvent) => {
-        e.key === 'Control' && (CtrlKey = false);
+    public unhandle = action((/* e: KeyboardEvent */) => {
+        /* empty */
     });
     public handleModifiers = action((e: KeyboardEvent) => {
         if (e.shiftKey) SnappingManager.SetShiftKey(true);
@@ -65,10 +63,9 @@ export class KeyManager {
 
     public handle = action((e: KeyboardEvent) => {
         // accumulate buffer of characters to insert in a new text note.  once the note is created, it will stop keyboard events from reaching this function.
-        if (FormattedTextBox.SelectOnLoadChar) FormattedTextBox.SelectOnLoadChar = FormattedTextBox.SelectOnLoadChar + (e.key === 'Enter' ? '\n' : e.key);
-        e.key === 'Control' && (CtrlKey = true);
+        if (FormattedTextBox.SelectOnLoadChar) FormattedTextBox.SelectOnLoadChar += e.key === 'Enter' ? '\n' : e.key;
         const keyname = e.key && e.key.toLowerCase();
-        this.handleGreedy(keyname);
+        this.handleGreedy(/* keyname */);
 
         if (modifiers.includes(keyname)) {
             return;
@@ -101,20 +98,15 @@ export class KeyManager {
             case 'u':
                 if (document.activeElement?.tagName !== 'INPUT' && document.activeElement?.tagName !== 'TEXTAREA') {
                     const ungroupings = SelectionManager.Views;
-                    UndoManager.RunInBatch(() => ungroupings.forEach(dv => { dv.layoutDoc.group = undefined; }), 'ungroup');
+                    undoable(() => () => ungroupings.forEach(dv => { dv.layoutDoc.group = undefined; }), 'ungroup');
                     SelectionManager.DeselectAll();
                 }
                 break;
             case 'g':
                 if (document.activeElement?.tagName !== 'INPUT' && document.activeElement?.tagName !== 'TEXTAREA') {
                     const selected = SelectionManager.Views;
-                    const collectionView = selected.reduce((col, dv) => (col === null || dv.CollectionFreeFormView === col ? dv.CollectionFreeFormView : undefined), null as null | undefined | CollectionFreeFormView);
-                    if (collectionView) {
-                        UndoManager.RunInBatch(() =>
-                            collectionView._marqueeViewRef.current?.collection(e, true, SelectionManager.Docs)
-                        , 'grouping');
-                        break;
-                    }
+                    const cv = selected.reduce((col, dv) => (!col || dv.CollectionFreeFormView === col ? dv.CollectionFreeFormView : undefined), undefined as undefined | CollectionFreeFormView);
+                    cv && undoable(() => cv._marqueeViewRef.current?.collection(e, true, SelectionManager.Docs), 'grouping');
                 }
                 break;
             case ' ':
@@ -146,7 +138,7 @@ export class KeyManager {
                     // DictationManager.Controls.stop();
                     GoogleAuthenticationManager.Instance.cancel();
                     SharingManager.Instance.close();
-                    if (!GroupManager.Instance.isOpen) SettingsManager.Instance.close();
+                    if (!GroupManager.Instance.isOpen) SettingsManager.Instance.closeMgr();
                     GroupManager.Instance.close();
                     window.getSelection()?.empty();
                     document.body.focus();
@@ -186,17 +178,18 @@ export class KeyManager {
             case 'arrowdown':  return this.nudge(0, 10, 'nudge down');
             case 'u' : 
                 if (document.activeElement?.tagName !== 'INPUT' && document.activeElement?.tagName !== 'TEXTAREA') {
-                    UndoManager.RunInBatch(() => SelectionManager.Docs.forEach(doc => (doc.group = undefined)), 'unggroup');
+                    UndoManager.RunInBatch(() => SelectionManager.Docs.forEach(doc => {doc.group = undefined}), 'unggroup');
                     SelectionManager.DeselectAll();
                 }
                 break;
             case 'g':
                 if (document.activeElement?.tagName !== 'INPUT' && document.activeElement?.tagName !== 'TEXTAREA') {
                     const randomGroup = random(0, 1000);
-                    UndoManager.RunInBatch(() => SelectionManager.Docs.forEach(doc => (doc.group = randomGroup)), 'group');
+                    UndoManager.RunInBatch(() => SelectionManager.Docs.forEach(doc => {doc.group = randomGroup}), 'group');
                     SelectionManager.DeselectAll();
                 }
                 break;
+            default: 
         } // prettier-ignore
 
         return {
@@ -212,8 +205,9 @@ export class KeyManager {
         switch (keyname) {
             case 'ƒ':
             case 'f':
-                const dv = SelectionManager.Views?.[0];
-                UndoManager.RunInBatch(() => dv.CollectionFreeFormDocumentView?.float(), 'float');
+                UndoManager.RunInBatch(() => SelectionManager.Views?.[0]?.CollectionFreeFormDocumentView?.float(), 'float');
+                break;
+            default:
         }
 
         return {
@@ -248,15 +242,19 @@ export class KeyManager {
                 PromiseValue(Cast(Doc.UserDoc()['tabs-button-tools'], Doc)).then(pv => pv && (pv.onClick as ScriptField).script.run({ this: pv }));
                 break;
             case 'i':
-                const importBtn = DocListCast(Doc.MyLeftSidebarMenu.data).find(d => d.target === Doc.MyImports);
-                if (importBtn) {
-                    MainView.Instance.selectMenu(importBtn);
+                {
+                    const importBtn = DocListCast(Doc.MyLeftSidebarMenu.data).find(d => d.target === Doc.MyImports);
+                    if (importBtn) {
+                        MainView.Instance.selectMenu(importBtn);
+                    }
                 }
                 break;
             case 's':
-                const trailsBtn = DocListCast(Doc.MyLeftSidebarMenu.data).find(d => d.target === Doc.MyTrails);
-                if (trailsBtn) {
-                    MainView.Instance.selectMenu(trailsBtn);
+                {
+                    const trailsBtn = DocListCast(Doc.MyLeftSidebarMenu.data).find(d => d.target === Doc.MyTrails);
+                    if (trailsBtn) {
+                        MainView.Instance.selectMenu(trailsBtn);
+                    }
                 }
                 break;
             case 'f':
@@ -323,6 +321,7 @@ export class KeyManager {
                 }
                 preventDefault = false;
                 break;
+            default:
         }
 
         return {
@@ -357,6 +356,7 @@ export class KeyManager {
             case 'p':
                 Doc.ActiveTool = InkTool.Write;
                 break;
+            default:
         }
 
         return {
diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx
index 9f82fdb18..e04daec3b 100644
--- a/src/client/views/InkingStroke.tsx
+++ b/src/client/views/InkingStroke.tsx
@@ -46,6 +46,7 @@ import { PresBox } from './nodes/trails';
 import { StyleProp } from './StyleProvider';
 
 const { INK_MASK_SIZE } = require('./global/globalCssVariables.module.scss'); // prettier-ignore
+
 @observer
 export class InkingStroke extends ViewBoxAnnotatableComponent<FieldViewProps>() implements ViewBoxInterface {
     static readonly MaskDim = INK_MASK_SIZE; // choose a really big number to make sure mask fits over container (which in theory can be arbitrarily big)
@@ -66,7 +67,9 @@ export class InkingStroke extends ViewBoxAnnotatableComponent<FieldViewProps>()
         this._props.setContentViewBox?.(this);
         this._disposers.selfDisper = reaction(
             () => this._props.isSelected(), // react to stroke being deselected by turning off ink handles
-            selected => !selected && (InkStrokeProperties.Instance._controlButton = false)
+            selected => {
+                !selected && (InkStrokeProperties.Instance._controlButton = false);
+            }
         );
     }
     componentWillUnmount() {
@@ -168,7 +171,9 @@ export class InkingStroke extends ViewBoxAnnotatableComponent<FieldViewProps>()
             }),
             isEditing,
             isEditing,
-            action(() => wasSelected && (InkStrokeProperties.Instance._currentPoint = -1))
+            action(() => {
+                wasSelected && (InkStrokeProperties.Instance._currentPoint = -1);
+            })
         );
     };
 
@@ -332,26 +337,28 @@ export class InkingStroke extends ViewBoxAnnotatableComponent<FieldViewProps>()
     };
 
     _subContentView: ViewBoxInterface | undefined;
-    setSubContentView = (doc: ViewBoxInterface) => (this._subContentView = doc);
-    @computed get fillColor() {
+    setSubContentView = (doc: ViewBoxInterface) => {
+        this._subContentView = doc;
+    };
+    @computed get fillColor(): string {
         const isInkMask = BoolCast(this.layoutDoc.stroke_isInkMask);
         return isInkMask ? DashColor(StrCast(this.layoutDoc.fillColor, 'transparent')).blacken(0).rgb().toString() : this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.FillColor) ?? 'transparent';
     }
     @computed get strokeColor() {
         const { inkData } = this.inkScaledData();
-        const fillColor = this.fillColor;
+        const { fillColor } = this;
         return !InkingStroke.IsClosed(inkData) && fillColor && fillColor !== 'transparent' ? fillColor : this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Color) ?? StrCast(this.layoutDoc.color);
     }
     render() {
         TraceMobx();
-        const { inkData, inkStrokeWidth, inkLeft, inkTop, inkScaleX, inkScaleY, inkWidth, inkHeight } = this.inkScaledData();
+        const { inkData, inkStrokeWidth, inkLeft, inkTop, inkScaleX, inkScaleY } = this.inkScaledData();
 
         const startMarker = StrCast(this.layoutDoc.stroke_startMarker);
         const endMarker = StrCast(this.layoutDoc.stroke_endMarker);
         const markerScale = NumCast(this.layoutDoc.stroke_markerScale, 1);
         const closed = InkingStroke.IsClosed(inkData);
         const isInkMask = BoolCast(this.layoutDoc.stroke_isInkMask);
-        const fillColor = this.fillColor;
+        const { fillColor } = this;
 
         // bcz: Hack!! Not really sure why, but having fractional values for width/height of mask ink strokes causes the dragging clone (see DragManager) to be offset from where it should be.
         if (isInkMask && (this.layoutDoc._width !== Math.round(NumCast(this.layoutDoc._width)) || this.layoutDoc._height !== Math.round(NumCast(this.layoutDoc._height)))) {
@@ -392,7 +399,7 @@ export class InkingStroke extends ViewBoxAnnotatableComponent<FieldViewProps>()
         );
         const higlightMargin = Math.min(12, Math.max(2, 0.3 * inkStrokeWidth));
         // Invisible polygonal line that enables the ink to be selected by the user.
-        const clickableLine = (downHdlr?: (e: React.PointerEvent) => void, mask: boolean = false) =>
+        const clickableLine = (downHdlr?: (e: React.PointerEvent) => void, mask: boolean = false): any =>
             InteractionUtils.CreatePolyline(
                 inkData,
                 inkLeft,
@@ -420,16 +427,24 @@ export class InkingStroke extends ViewBoxAnnotatableComponent<FieldViewProps>()
         // bootsrap 3 style sheet sets line height to be 20px for default 14 point font size.
         // this attempts to figure out the lineHeight ratio by inquiring the body's lineHeight and dividing by the fontsize which should yield 1.428571429
         // see: https://bibwild.wordpress.com/2019/06/10/bootstrap-3-to-4-changes-in-how-font-size-line-height-and-spacing-is-done-or-what-happened-to-line-height-computed/
-        const lineHeightGuess = +getComputedStyle(document.body).lineHeight.replace('px', '') / +getComputedStyle(document.body).fontSize.replace('px', '');
+        //  const lineHeightGuess = +getComputedStyle(document.body).lineHeight.replace('px', '') / +getComputedStyle(document.body).fontSize.replace('px', '');
         const interactions = {
-            onPointerLeave: action(() => (this._nearestScrPt = undefined)),
+            onPointerLeave: action(() => {
+                this._nearestScrPt = undefined;
+            }),
             onPointerMove: this._props.isSelected() ? this.onPointerMove : undefined,
             onClick: (e: React.MouseEvent) => this._handledClick && e.stopPropagation(),
             onContextMenu: () => {
                 const cm = ContextMenu.Instance;
                 !Doc.noviceMode && cm?.addItem({ description: 'Recognize Writing', event: this.analyzeStrokes, icon: 'paint-brush' });
                 cm?.addItem({ description: 'Toggle Mask', event: () => InkingStroke.toggleMask(this.dataDoc), icon: 'paint-brush' });
-                cm?.addItem({ description: 'Edit Points', event: action(() => (InkStrokeProperties.Instance._controlButton = !InkStrokeProperties.Instance._controlButton)), icon: 'paint-brush' });
+                cm?.addItem({
+                    description: 'Edit Points',
+                    event: action(() => {
+                        InkStrokeProperties.Instance._controlButton = !InkStrokeProperties.Instance._controlButton;
+                    }),
+                    icon: 'paint-brush',
+                });
             },
         };
         return (
@@ -441,6 +456,7 @@ export class InkingStroke extends ViewBoxAnnotatableComponent<FieldViewProps>()
                         // mixBlendMode: this.layoutDoc.tool === InkTool.Highlighter ? 'multiply' : 'unset',
                         cursor: this._props.isSelected() ? 'default' : undefined,
                     }}
+                    // eslint-disable-next-line react/jsx-props-no-spreading
                     {...interactions}>
                     {clickableLine(this.onPointerDown, isInkMask)}
                     {isInkMask ? null : inkLine}
@@ -454,18 +470,19 @@ export class InkingStroke extends ViewBoxAnnotatableComponent<FieldViewProps>()
                             width: NumCast(this.layoutDoc._width),
                             transform: `scale(${this._props.NativeDimScaling?.() || 1})`,
                             transformOrigin: 'top left',
-                            //top: (this._props.PanelHeight() - (lineHeightGuess * fsize + 20) * (this._props.NativeDimScaling?.() || 1)) / 2,
+                            // top: (this._props.PanelHeight() - (lineHeightGuess * fsize + 20) * (this._props.NativeDimScaling?.() || 1)) / 2,
                         }}>
                         <FormattedTextBox
+                            // eslint-disable-next-line react/jsx-props-no-spreading
                             {...this._props}
                             setHeight={undefined}
                             setContentViewBox={this.setSubContentView} // this makes the inkingStroke the "dominant" component - ie, it will show the inking UI when selected (not text)
                             yPadding={10}
                             xPadding={10}
                             fieldKey="text"
-                            //dontRegisterView={true}
-                            noSidebar={true}
-                            dontScale={true}
+                            // dontRegisterView={true}
+                            noSidebar
+                            dontScale
                             isContentActive={this._props.isContentActive}
                         />
                     </div>
@@ -474,37 +491,6 @@ export class InkingStroke extends ViewBoxAnnotatableComponent<FieldViewProps>()
         );
     }
 }
-
-export function SetActiveInkWidth(width: string): void {
-    !isNaN(parseInt(width)) && ActiveInkPen() && (ActiveInkPen().activeInkWidth = width);
-}
-export function SetActiveBezierApprox(bezier: string): void {
-    ActiveInkPen() && (ActiveInkPen().activeInkBezier = isNaN(parseInt(bezier)) ? '' : bezier);
-}
-export function SetActiveInkColor(value: string) {
-    ActiveInkPen() && (ActiveInkPen().activeInkColor = value);
-}
-export function SetActiveIsInkMask(value: boolean) {
-    ActiveInkPen() && (ActiveInkPen().activeIsInkMask = value);
-}
-export function SetActiveInkHideTextLabels(value: boolean) {
-    ActiveInkPen() && (ActiveInkPen().activeInkHideTextLabels = value);
-}
-export function SetActiveFillColor(value: string) {
-    ActiveInkPen() && (ActiveInkPen().activeFillColor = value);
-}
-export function SetActiveArrowStart(value: string) {
-    ActiveInkPen() && (ActiveInkPen().activeArrowStart = value);
-}
-export function SetActiveArrowEnd(value: string) {
-    ActiveInkPen() && (ActiveInkPen().activeArrowEnd = value);
-}
-export function SetActiveArrowScale(value: number) {
-    ActiveInkPen() && (ActiveInkPen().activeArrowScale = value);
-}
-export function SetActiveDash(dash: string): void {
-    !isNaN(parseInt(dash)) && ActiveInkPen() && (ActiveInkPen().activeDash = dash);
-}
 export function ActiveInkPen(): Doc {
     return Doc.UserDoc();
 }
@@ -538,3 +524,34 @@ export function ActiveInkWidth(): number {
 export function ActiveInkBezierApprox(): string {
     return StrCast(ActiveInkPen()?.activeInkBezier);
 }
+
+export function SetActiveInkWidth(width: string): void {
+    !isNaN(parseInt(width)) && ActiveInkPen() && (ActiveInkPen().activeInkWidth = width);
+}
+export function SetActiveBezierApprox(bezier: string): void {
+    ActiveInkPen() && (ActiveInkPen().activeInkBezier = isNaN(parseInt(bezier)) ? '' : bezier);
+}
+export function SetActiveInkColor(value: string) {
+    ActiveInkPen() && (ActiveInkPen().activeInkColor = value);
+}
+export function SetActiveIsInkMask(value: boolean) {
+    ActiveInkPen() && (ActiveInkPen().activeIsInkMask = value);
+}
+export function SetActiveInkHideTextLabels(value: boolean) {
+    ActiveInkPen() && (ActiveInkPen().activeInkHideTextLabels = value);
+}
+export function SetActiveFillColor(value: string) {
+    ActiveInkPen() && (ActiveInkPen().activeFillColor = value);
+}
+export function SetActiveArrowStart(value: string) {
+    ActiveInkPen() && (ActiveInkPen().activeArrowStart = value);
+}
+export function SetActiveArrowEnd(value: string) {
+    ActiveInkPen() && (ActiveInkPen().activeArrowEnd = value);
+}
+export function SetActiveArrowScale(value: number) {
+    ActiveInkPen() && (ActiveInkPen().activeArrowScale = value);
+}
+export function SetActiveDash(dash: string): void {
+    !isNaN(parseInt(dash)) && ActiveInkPen() && (ActiveInkPen().activeDash = dash);
+}
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index b0156846f..a13fe1cb7 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -1,3 +1,4 @@
+/* eslint-disable node/no-unpublished-import */
 import { library } from '@fortawesome/fontawesome-svg-core';
 import { faBuffer, faHireAHelper } from '@fortawesome/free-brands-svg-icons';
 import * as far from '@fortawesome/free-regular-svg-icons';
@@ -6,9 +7,10 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { action, computed, configure, makeObservable, observable, reaction, runInAction } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
+// eslint-disable-next-line import/no-relative-packages
 import '../../../node_modules/browndash-components/dist/styles/global.min.css';
 import { ClientUtils, lightOrDark, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue, returnZero, setupMoveUpEvents } from '../../ClientUtils';
-import { Utils, emptyFunction } from '../../Utils';
+import { emptyFunction } from '../../Utils';
 import { Doc, DocListCast, Opt } from '../../fields/Doc';
 import { DocData } from '../../fields/DocSymbols';
 import { DocCast, StrCast } from '../../fields/Types';
@@ -625,8 +627,8 @@ export class MainView extends ObservableReactComponent<{}> {
     waitForDoubleClick = () => (SnappingManager.ExploreMode ? 'never' : undefined);
     headerBarScreenXf = () => new Transform(-this.leftScreenOffsetOfMainDocView - this.leftMenuFlyoutWidth(), -this.headerBarDocHeight(), 1);
     mainScreenToLocalXf = () => new Transform(-this.leftScreenOffsetOfMainDocView - this.leftMenuFlyoutWidth(), -this.topOfMainDocContent, 1);
-    addHeaderDoc = (doc: Doc | Doc[], annotationKey?: string) => (doc instanceof Doc ? [doc] : doc).reduce((done, doc) => Doc.AddDocToList(this.headerBarDoc, 'data', doc), true);
-    removeHeaderDoc = (doc: Doc | Doc[], annotationKey?: string) => (doc instanceof Doc ? [doc] : doc).reduce((done, doc) => Doc.RemoveDocFromList(this.headerBarDoc, 'data', doc), true);
+    addHeaderDoc = (docs: Doc | Doc[]) => (docs instanceof Doc ? [docs] : docs).reduce((done, doc) => Doc.AddDocToList(this.headerBarDoc, 'data', doc), true);
+    removeHeaderDoc = (docs: Doc | Doc[]) => (docs instanceof Doc ? [docs] : docs).reduce((done, doc) => Doc.RemoveDocFromList(this.headerBarDoc, 'data', doc), true);
     @computed get headerBarDocView() {
         return (
             <div className="mainView-headerBar" style={{ height: this.headerBarDocHeight() }}>
@@ -734,8 +736,8 @@ export class MainView extends ObservableReactComponent<{}> {
         setupMoveUpEvents(
             this,
             e,
-            action(e => {
-                this._leftMenuFlyoutWidth = Math.max(e.clientX - 58, 0);
+            action(ev => {
+                this._leftMenuFlyoutWidth = Math.max(ev.clientX - 58, 0);
                 return false;
             }),
             () => this._leftMenuFlyoutWidth < 5 && this.closeFlyout(),
@@ -827,7 +829,7 @@ export class MainView extends ObservableReactComponent<{}> {
         if (willOpen) {
             switch ((this._panelContent = title)) {
                 case 'Settings':
-                    SettingsManager.Instance.open();
+                    SettingsManager.Instance.openMgr();
                     break;
                 case 'Help':
                     break;
@@ -907,19 +909,19 @@ export class MainView extends ObservableReactComponent<{}> {
         // setTimeout(action(() => (this._leftMenuFlyoutWidth += 0.5)));
 
         this._sidebarContent.proto = DocCast(button.target);
-        SettingsManager.Instance.SetLastPressedBtn(button);
+        SettingsManager.Instance.LastPressedBtn = button;
     });
 
     closeFlyout = action(() => {
-        SettingsManager.Instance.SetLastPressedBtn(undefined);
+        SettingsManager.Instance.LastPressedBtn = undefined;
         this._panelContent = 'none';
         this._sidebarContent.proto = undefined;
         this._leftMenuFlyoutWidth = 0;
     });
 
-    remButtonDoc = (doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).reduce((flg: boolean, doc) => flg && !doc.dragOnlyWithinContainer && Doc.RemoveDocFromList(Doc.MyDockedBtns, 'data', doc), true);
-    moveButtonDoc = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (document: Doc | Doc[]) => boolean) => this.remButtonDoc(doc) && addDocument(doc);
-    addButtonDoc = (doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).reduce((flg: boolean, doc) => flg && Doc.AddDocToList(Doc.MyDockedBtns, 'data', doc), true);
+    remButtonDoc = (docs: Doc | Doc[]) => (docs instanceof Doc ? [docs] : docs).reduce((flg: boolean, doc) => flg && !doc.dragOnlyWithinContainer && Doc.RemoveDocFromList(Doc.MyDockedBtns, 'data', doc), true);
+    moveButtonDoc = (docs: Doc | Doc[], targetCol: Doc | undefined, addDocument: (document: Doc | Doc[]) => boolean) => this.remButtonDoc(docs) && addDocument(docs);
+    addButtonDoc = (docs: Doc | Doc[]) => (docs instanceof Doc ? [docs] : docs).reduce((flg: boolean, doc) => flg && Doc.AddDocToList(Doc.MyDockedBtns, 'data', doc), true);
 
     buttonBarXf = () => {
         if (!this._docBtnRef.current) return Transform.Identity();
@@ -965,14 +967,16 @@ export class MainView extends ObservableReactComponent<{}> {
         return !dragPar?.layoutDoc.freeform_snapLines ? null : (
             <div className="mainView-snapLines">
                 <svg style={{ width: '100%', height: '100%' }}>
-                    {SnappingManager.HorizSnapLines.map((l, i) => (
-                        // eslint-disable-next-line react/no-array-index-key
-                        <line key={i} x1="0" y1={l} x2="2000" y2={l} stroke={lightOrDark(dragPar.layoutDoc.backgroundColor ?? 'gray')} opacity={0.3} strokeWidth={1} strokeDasharray={'2 2'} />
-                    ))}
-                    {SnappingManager.VertSnapLines.map((l, i) => (
-                        // eslint-disable-next-line react/no-array-index-key
-                        <line key={i} y1={this.topOfMainDocContent.toString()} x1={l} y2="2000" x2={l} stroke={lightOrDark(dragPar.layoutDoc.backgroundColor ?? 'gray')} opacity={0.3} strokeWidth={1} strokeDasharray={'2 2'} />
-                    ))}
+                    {[
+                        ...SnappingManager.HorizSnapLines.map((l, i) => (
+                            // eslint-disable-next-line react/no-array-index-key
+                            <line key={'horiz' + i} x1="0" y1={l} x2="2000" y2={l} stroke={lightOrDark(dragPar.layoutDoc.backgroundColor ?? 'gray')} opacity={0.3} strokeWidth={1} strokeDasharray="2 2" />
+                        )),
+                        ...SnappingManager.VertSnapLines.map((l, i) => (
+                            // eslint-disable-next-line react/no-array-index-key
+                            <line key={'vert' + i} y1={this.topOfMainDocContent.toString()} x1={l} y2="2000" x2={l} stroke={lightOrDark(dragPar.layoutDoc.backgroundColor ?? 'gray')} opacity={0.3} strokeWidth={1} strokeDasharray="2 2" />
+                        )),
+                    ]}
                 </svg>
             </div>
         );
diff --git a/src/client/views/MetadataEntryMenu.tsx b/src/client/views/MetadataEntryMenu.tsx
deleted file mode 100644
index 9626f1653..000000000
--- a/src/client/views/MetadataEntryMenu.tsx
+++ /dev/null
@@ -1,196 +0,0 @@
-import { IReactionDisposer, action, observable, reaction, runInAction } from 'mobx';
-import { observer } from 'mobx-react';
-import * as React from 'react';
-import * as Autosuggest from 'react-autosuggest';
-import { emptyFunction, emptyPath } from '../../Utils';
-import { Doc, DocListCast, Field, FieldType } from '../../fields/Doc';
-import { undoBatch } from '../util/UndoManager';
-import './MetadataEntryMenu.scss';
-import { KeyValueBox } from './nodes/KeyValueBox';
-
-export type DocLike = Doc | Doc[] | Promise<Doc> | Promise<Doc[]>;
-export interface MetadataEntryProps {
-    docs: Doc[];
-    onError?: () => boolean;
-}
-
-@observer
-export class MetadataEntryMenu extends React.Component<MetadataEntryProps> {
-    @observable private _currentKey: string = '';
-    @observable private _currentValue: string = '';
-    private _addChildren: boolean = false;
-    @observable _allSuggestions: string[] = [];
-    _suggestionDispser: IReactionDisposer | undefined;
-    private userModified = false;
-
-    private autosuggestRef = React.createRef<Autosuggest>();
-
-    @action
-    onKeyChange = (e: React.ChangeEvent, { newValue }: { newValue: string }) => {
-        this._currentKey = newValue;
-        if (!this.userModified) {
-            this.previewValue();
-        }
-    };
-
-    previewValue = async () => {
-        let field: FieldType | undefined | null = null;
-        let onProto: boolean = false;
-        let value: string | undefined;
-        const { docs } = this.props;
-        for (const doc of docs) {
-            const v = await doc[this._currentKey];
-            onProto = onProto || !Object.keys(doc).includes(this._currentKey);
-            if (field === null) {
-                field = v;
-            } else if (v !== field) {
-                value = 'multiple values';
-            }
-        }
-        if (value === undefined) {
-            if (field !== null && field !== undefined) {
-                value = (onProto ? '' : '= ') + Field.toScriptString(field);
-            } else {
-                value = '';
-            }
-        }
-        const s = value;
-        runInAction(() => (this._currentValue = s));
-    };
-
-    @action
-    onValueChange = (e: React.ChangeEvent<HTMLInputElement>) => {
-        this._currentValue = e.target.value;
-        this.userModified = e.target.value.trim() !== '';
-    };
-
-    @undoBatch
-    @action
-    onValueKeyDown = async (e: React.KeyboardEvent) => {
-        if (e.key === 'Enter') {
-            e.stopPropagation();
-            const script = KeyValueBox.CompileKVPScript(this._currentValue);
-            if (!script) return;
-
-            let childSuccess = true;
-            if (this._addChildren) {
-                for (const document of this.props.docs) {
-                    const collectionChildren = DocListCast(document.data);
-                    if (collectionChildren) {
-                        childSuccess = collectionChildren.every(c => KeyValueBox.ApplyKVPScript(c, this._currentKey, script));
-                    }
-                }
-            }
-            const success = this.props.docs.every(d => KeyValueBox.ApplyKVPScript(d, this._currentKey, script)) && childSuccess;
-            if (!success) {
-                if (this.props.onError) {
-                    if (this.props.onError()) {
-                        this.clearInputs();
-                    }
-                } else {
-                    this.clearInputs();
-                }
-            } else {
-                this.clearInputs();
-            }
-        }
-    };
-
-    @action
-    clearInputs = () => {
-        this._currentKey = '';
-        this._currentValue = '';
-        this.userModified = false;
-        if (this.autosuggestRef.current) {
-            const input: HTMLInputElement = (this.autosuggestRef.current as any).input;
-            input && input.focus();
-        }
-    };
-
-    getKeySuggestions = (value: string) => {
-        value = value.toLowerCase();
-        const docs = this.props.docs;
-        const keys = new Set<string>();
-        docs.forEach(doc => Doc.allKeys(doc).forEach(key => keys.add(key)));
-        return Array.from(keys).filter(key => key.toLowerCase().startsWith(value));
-    };
-    getSuggestionValue = (suggestion: string) => suggestion;
-
-    renderSuggestion = (suggestion: string) => {
-        return null;
-    };
-    componentDidMount() {
-        this._suggestionDispser = reaction(
-            () => this._currentKey,
-            () => (this._allSuggestions = this.getKeySuggestions(this._currentKey)),
-            { fireImmediately: true }
-        );
-    }
-    componentWillUnmount() {
-        this._suggestionDispser && this._suggestionDispser();
-    }
-
-    onClick = (e: React.ChangeEvent<HTMLInputElement>) => {
-        this._addChildren = !this._addChildren;
-    };
-
-    private get considerChildOptions() {
-        if (!this.props.docs.every(doc => doc._type_collection !== undefined)) {
-            return null;
-        }
-        return (
-            <div style={{ display: 'flex' }}>
-                Children:
-                <input type="checkbox" onChange={this.onClick}></input>
-            </div>
-        );
-    }
-
-    _ref = React.createRef<HTMLInputElement>();
-    render() {
-        return (
-            <div className="metadataEntry-outerDiv" id="metadataEntry-outer" onPointerDown={e => e.stopPropagation()}>
-                <div className="metadataEntry-inputArea">
-                    <div style={{ display: 'flex', flexDirection: 'row' }}>
-                        <span>Key:</span>
-                        <div className="metadataEntry-autoSuggester" onClick={e => this.autosuggestRef.current!.input?.focus()}>
-                            <Autosuggest
-                                // @ts-ignore
-                                inputProps={{ value: this._currentKey, onChange: this.onKeyChange }}
-                                getSuggestionValue={this.getSuggestionValue}
-                                suggestions={emptyPath}
-                                alwaysRenderSuggestions={false}
-                                renderSuggestion={this.renderSuggestion}
-                                onSuggestionsFetchRequested={emptyFunction}
-                                onSuggestionsClearRequested={emptyFunction}
-                                ref={this.autosuggestRef}
-                            />
-                        </div>
-                    </div>
-                    <div style={{ display: 'flex', flexDirection: 'row' }}>
-                        <span>Value:</span>
-                        <input className="metadataEntry-input" ref={this._ref} value={this._currentValue} onClick={e => this._ref.current!.focus()} onChange={this.onValueChange} onKeyDown={this.onValueKeyDown} />
-                    </div>
-                    {this.considerChildOptions}
-                </div>
-                <div className="metadataEntry-keys">
-                    <ul>
-                        {this._allSuggestions
-                            .slice()
-                            .sort()
-                            .map(s => (
-                                <li
-                                    key={s}
-                                    onClick={action(() => {
-                                        this._currentKey = s;
-                                        this.previewValue();
-                                    })}>
-                                    {s}
-                                </li>
-                            ))}
-                    </ul>
-                </div>
-            </div>
-        );
-    }
-}
diff --git a/src/client/views/OverlayView.tsx b/src/client/views/OverlayView.tsx
index 6539c0834..960282a08 100644
--- a/src/client/views/OverlayView.tsx
+++ b/src/client/views/OverlayView.tsx
@@ -18,6 +18,7 @@ import { ObservableReactComponent } from './ObservableReactComponent';
 import './OverlayView.scss';
 import { DefaultStyleProvider } from './StyleProvider';
 import { DocumentView, DocumentViewInternal } from './nodes/DocumentView';
+
 const _global = (window /* browser */ || global) /* node */ as any;
 
 export type OverlayDisposer = () => void;
@@ -82,12 +83,12 @@ export class OverlayWindow extends ObservableReactComponent<OverlayWindowProps>
         this.height = Math.max(this.height, 30);
     };
 
-    onPointerUp = (e: PointerEvent) => {
+    onPointerUp = () => {
         document.removeEventListener('pointermove', this.onPointerMove);
         document.removeEventListener('pointerup', this.onPointerUp);
     };
 
-    onResizerPointerUp = (e: PointerEvent) => {
+    onResizerPointerUp = () => {
         document.removeEventListener('pointermove', this.onResizerPointerMove);
         document.removeEventListener('pointerup', this.onResizerPointerUp);
     };
@@ -171,7 +172,7 @@ export class OverlayView extends ObservableReactComponent<{}> {
                 {contents}
             </OverlayWindow>
         );
-        this._elements.push(contents);
+        this._elements.push(wincontents);
         return remove;
     }
 
diff --git a/src/client/views/PropertiesButtons.tsx b/src/client/views/PropertiesButtons.tsx
index c7720d834..d83fea2a5 100644
--- a/src/client/views/PropertiesButtons.tsx
+++ b/src/client/views/PropertiesButtons.tsx
@@ -1,3 +1,6 @@
+/* eslint-disable jsx-a11y/no-static-element-interactions */
+/* eslint-disable jsx-a11y/click-events-have-key-events */
+/* eslint-disable react/no-unused-class-component-methods */
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { Dropdown, DropdownType, IListItemProps, Toggle, ToggleType, Type } from 'browndash-components';
 import { action, computed, observable } from 'mobx';
@@ -12,7 +15,7 @@ import { MdClosedCaption, MdClosedCaptionDisabled, MdGridOff, MdGridOn, MdSubtit
 import { RxWidth } from 'react-icons/rx';
 import { TbEditCircle, TbEditCircleOff, TbHandOff, TbHandStop, TbHighlight, TbHighlightOff } from 'react-icons/tb';
 import { TfiBarChart } from 'react-icons/tfi';
-import { Doc, DocListCast, Opt } from '../../fields/Doc';
+import { Doc, Opt } from '../../fields/Doc';
 import { DocData } from '../../fields/DocSymbols';
 import { ScriptField } from '../../fields/ScriptField';
 import { BoolCast, ScriptCast } from '../../fields/Types';
@@ -32,6 +35,7 @@ import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox';
 
 @observer
 export class PropertiesButtons extends React.Component<{}, {}> {
+    // eslint-disable-next-line no-use-before-define
     @observable public static Instance: PropertiesButtons;
 
     @computed get selectedDoc() {
@@ -44,61 +48,11 @@ export class PropertiesButtons extends React.Component<{}, {}> {
         return !SelectionManager.SelectedSchemaDoc && SelectionManager.Views.lastElement()?.topMost;
     }
 
-    propertyToggleBtn = (label: (on?: any) => string, property: string, tooltip: (on?: any) => string, icon: (on?: any) => any, onClick?: (dv: Opt<DocumentView>, doc: Doc, property: string) => void, useUserDoc?: boolean) => {
-        const targetDoc = useUserDoc ? Doc.UserDoc() : this.selectedLayoutDoc;
-        const onPropToggle = (dv: Opt<DocumentView>, doc: Doc, prop: string) => ((dv?.layoutDoc || doc)[prop] = !(dv?.layoutDoc || doc)[prop]);
-        return !targetDoc ? null : (
-            <Toggle
-                toggleStatus={BoolCast(targetDoc[property])}
-                tooltip={tooltip(BoolCast(targetDoc[property]))}
-                text={label(targetDoc?.[property])}
-                color={SettingsManager.userColor}
-                icon={icon(targetDoc?.[property] as any)}
-                iconPlacement="left"
-                align="flex-start"
-                fillWidth={true}
-                toggleType={ToggleType.BUTTON}
-                onClick={undoable(() => {
-                    if (SelectionManager.Views.length > 1) {
-                        SelectionManager.Views.forEach(dv => (onClick ?? onPropToggle)(dv, dv.Document, property));
-                    } else if (targetDoc) (onClick ?? onPropToggle)(undefined, targetDoc, property);
-                }, property)}
-            />
-        );
-    };
-
-    // this implments a container pattern by marking the targetDoc (collection) as a lightbox
-    // that always fits its contents to its container and that hides all other documents when
-    // a link is followed that targets a 'lightbox' destination
-    @computed get isLightboxButton() {
-        return this.propertyToggleBtn(
-            on => 'Lightbox',
-            'isLightbox',
-            on => `${on ? 'Set' : 'Remove'} lightbox flag`,
-            on => 'window-restore',
-            onClick => {
-                SelectionManager.Views.forEach(dv => {
-                    const containerDoc = dv.Document;
-                    //containerDoc.followAllLinks =
-                    // containerDoc.noShadow =
-                    // containerDoc.layout_disableBrushing =
-                    // containerDoc._forceActive =
-                    //containerDoc._freeform_fitContentsToBox =
-                    containerDoc._isLightbox = !containerDoc._isLightbox;
-                    //containerDoc._xPadding = containerDoc._yPadding = containerDoc._isLightbox ? 10 : undefined;
-                    const containerContents = DocListCast(dv.dataDoc[Doc.LayoutFieldKey(containerDoc)]);
-                    //dv.Docuemnt.onClick = ScriptField.MakeScript('{this.data = undefined; documentView.select(false)}', { documentView: 'any' });
-                    containerContents.forEach(doc => LinkManager.Links(doc).forEach(link => (link.link_displayLine = false)));
-                });
-            }
-        );
-    }
-
     @computed get titleButton() {
         return this.propertyToggleBtn(
-            on => (!on ? 'SHOW TITLE' : this.selectedDoc?.['_layout_showTitle'] === 'title:hover' ? 'HIDE TITLE' : 'HOVER TITLE'),
+            on => (!on ? 'SHOW TITLE' : this.selectedDoc?._layout_showTitle === 'title:hover' ? 'HIDE TITLE' : 'HOVER TITLE'),
             '_layout_showTitle',
-            on => 'Switch between title styles',
+            () => 'Switch between title styles',
             on => (on ? <MdSubtitlesOff /> : <MdSubtitles />), // {currentIcon}, //(on ?  <MdSubtitles/> :)  , //,'text-width', on ?  <MdSubtitles/> : <MdSubtitlesOff/>,
             (dv, doc) => {
                 const tdoc = dv?.Document || doc;
@@ -119,7 +73,7 @@ export class PropertiesButtons extends React.Component<{}, {}> {
     }
 
     @computed get maskButton() {
-        //highlight text while going down and reading through
+        // highlight text while going down and reading through
         return this.propertyToggleBtn(
             on => (on ? 'PLAIN INK' : 'HIGHLIGHTER MASK'),
             'stroke_isInkMask',
@@ -132,10 +86,10 @@ export class PropertiesButtons extends React.Component<{}, {}> {
     @computed get hideImageButton() {
         // put in developer -- can trace on top of object and drawing is still there
         return this.propertyToggleBtn(
-            on => (on ? 'SHOW BACKGROUND IMAGE' : 'HIDE BACKGROUND IMAGE'), //'Background',
+            on => (on ? 'SHOW BACKGROUND IMAGE' : 'HIDE BACKGROUND IMAGE'), // 'Background',
             '_hideImage',
             on => (on ? 'Show Image' : 'Show Background'),
-            on => (on ? <BiShow /> : <BiHide />) //'portrait'
+            on => (on ? <BiShow /> : <BiHide />) // 'portrait'
         );
     }
 
@@ -144,35 +98,35 @@ export class PropertiesButtons extends React.Component<{}, {}> {
             on => (on ? 'DISABLE CLUSTERS' : 'HIGHLIGHT CLUSTERS'),
             '_freeform_useClusters',
             on => `${on ? 'Hide' : 'Show'} clusters`,
-            on => <FaBraille />
+            () => <FaBraille />
         );
     }
     @computed get panButton() {
         return this.propertyToggleBtn(
-            on => (on ? 'ENABLE PANNING' : 'DISABLE PANNING'), //'Lock\xA0View',
+            on => (on ? 'ENABLE PANNING' : 'DISABLE PANNING'), // 'Lock\xA0View',
             '_lockedTransform',
             on => `${on ? 'Unlock' : 'Lock'} panning of view`,
-            on => (on ? <TbHandStop /> : <TbHandOff />) //'lock'
+            on => (on ? <TbHandStop /> : <TbHandOff />) // 'lock'
         );
     }
 
     @computed get forceActiveButton() {
-        //select text
+        // select text
         return this.propertyToggleBtn(
             on => (on ? 'SELECT TO INTERACT' : 'ALWAYS INTERACTIVE'),
             '_forceActive',
             on => `${on ? 'Document must be selected to interact with its contents' : 'Contents always active (respond to click/drag events)'} `,
-            on => <MdTouchApp /> // 'eye'
+            () => <MdTouchApp /> // 'eye'
         );
     }
 
     @computed get verticalAlignButton() {
-        //select text
+        // select text
         return this.propertyToggleBtn(
             on => (on ? 'ALIGN TOP' : 'ALIGN CENTER'),
             '_layout_centered',
             on => `${on ? 'Text is aligned with top of document' : 'Text is aligned with center of document'} `,
-            on => <MdTouchApp /> // 'eye'
+            () => <MdTouchApp /> // 'eye'
         );
     }
 
@@ -192,10 +146,10 @@ export class PropertiesButtons extends React.Component<{}, {}> {
 
     @computed get fitContentButton() {
         return this.propertyToggleBtn(
-            on => (on ? 'PREVIOUS VIEW' : 'VIEW ALL'), //'View All',
+            on => (on ? 'PREVIOUS VIEW' : 'VIEW ALL'), // 'View All',
             '_freeform_fitContentsToBox',
             on => `${on ? "Don't" : 'Do'} fit content to container visible area`,
-            on => (on ? <CiGrid31 /> : <BsGrid3X3GapFill />) //'object-group'
+            on => (on ? <CiGrid31 /> : <BsGrid3X3GapFill />) // 'object-group'
         );
     }
 
@@ -228,7 +182,7 @@ export class PropertiesButtons extends React.Component<{}, {}> {
 
     @computed get layout_fitWidthButton() {
         return this.propertyToggleBtn(
-            on => (on ? 'SCALED VIEW' : 'READING VIEW'), //'Fit\xA0Width',
+            on => (on ? 'SCALED VIEW' : 'READING VIEW'), // 'Fit\xA0Width',
             '_layout_fitWidth',
             on =>
                 on
@@ -240,12 +194,14 @@ export class PropertiesButtons extends React.Component<{}, {}> {
 
     @computed get captionButton() {
         return this.propertyToggleBtn(
-            //DEVELOPER
-            on => (on ? 'HIDE CAPTION' : 'SHOW CAPTION'), //'Caption',
+            // DEVELOPER
+            on => (on ? 'HIDE CAPTION' : 'SHOW CAPTION'), // 'Caption',
             '_layout_showCaption',
             on => `${on ? 'Hide' : 'Show'} caption footer`,
-            on => (on ? <MdClosedCaptionDisabled /> : <MdClosedCaption />), //'closed-captioning',
-            (dv, doc) => ((dv?.Document || doc)._layout_showCaption = (dv?.Document || doc)._layout_showCaption === undefined ? 'caption' : undefined)
+            on => (on ? <MdClosedCaptionDisabled /> : <MdClosedCaption />), // 'closed-captioning',
+            (dv, doc) => {
+                (dv?.Document || doc)._layout_showCaption = (dv?.Document || doc)._layout_showCaption === undefined ? 'caption' : undefined;
+            }
         );
     }
 
@@ -256,7 +212,9 @@ export class PropertiesButtons extends React.Component<{}, {}> {
             '_chromeHidden',
             on => `${on ? 'Show' : 'Hide'} editing UI`,
             on => (on ? <TbEditCircle /> : <TbEditCircleOff />), // 'edit',
-            (dv, doc) => ((dv?.Document || doc)._chromeHidden = !(dv?.Document || doc)._chromeHidden)
+            (dv, doc) => {
+                (dv?.Document || doc)._chromeHidden = !(dv?.Document || doc)._chromeHidden;
+            }
         );
     }
 
@@ -265,8 +223,8 @@ export class PropertiesButtons extends React.Component<{}, {}> {
         return this.propertyToggleBtn(
             on => (on ? 'AUTO\xA0SIZE' : 'FIXED SIZE'),
             '_layout_autoHeight',
-            on => `Automatical vertical sizing to show all content`,
-            on => <FontAwesomeIcon icon="arrows-alt-v" size="lg" />
+            () => `Automatical vertical sizing to show all content`,
+            () => <FontAwesomeIcon icon="arrows-alt-v" size="lg" />
         );
     }
 
@@ -274,8 +232,8 @@ export class PropertiesButtons extends React.Component<{}, {}> {
         return this.propertyToggleBtn(
             on => (on ? 'HIDE GRID' : 'DISPLAY GRID'),
             '_freeform_backgroundGrid',
-            on => `Display background grid in collection`,
-            on => (on ? <MdGridOff /> : <MdGridOn />) //'border-all'
+            () => `Display background grid in collection`,
+            on => (on ? <MdGridOff /> : <MdGridOn />) // 'border-all'
         );
     }
 
@@ -317,8 +275,8 @@ export class PropertiesButtons extends React.Component<{}, {}> {
         return this.propertyToggleBtn(
             on => (on ? 'HIDE SNAP LINES' : 'SHOW SNAP LINES'),
             'freeform_snapLines',
-            on => `Display snapping lines when objects are dragged`,
-            on => <TfiBarChart />, //'th',
+            () => `Display snapping lines when objects are dragged`,
+            () => <TfiBarChart />, // 'th',
             undefined
         );
     }
@@ -361,7 +319,9 @@ export class PropertiesButtons extends React.Component<{}, {}> {
     @undoBatch
     handlePerspectiveChange = (e: any) => {
         this.selectedDoc && (this.selectedDoc._type_collection = e.target.value);
-        SelectionManager.Views.forEach(docView => (docView.layoutDoc._type_collection = e.target.value));
+        SelectionManager.Views.forEach(docView => {
+            docView.layoutDoc._type_collection = e.target.value;
+        });
     };
     @computed get onClickVal() {
         const linkButton = IsFollowLinkScript(this.selectedDoc.onClick);
@@ -369,10 +329,10 @@ export class PropertiesButtons extends React.Component<{}, {}> {
         const linkedToLightboxView = () => LinkManager.Links(this.selectedDoc).some(link => LinkManager.getOppositeAnchor(link, this.selectedDoc)?._isLightbox);
 
         if (followLoc === OpenWhere.lightbox && !linkedToLightboxView()) return 'linkInPlace';
-        else if (linkButton && followLoc === OpenWhere.addRight) return 'linkOnRight';
-        else if (linkButton && this.selectedDoc._followLinkLocation === OpenWhere.lightbox && linkedToLightboxView()) return 'enterPortal';
-        else if (ScriptCast(this.selectedDoc.onClick)?.script.originalScript.includes('toggleDetail')) return 'toggleDetail';
-        else return 'nothing';
+        if (linkButton && followLoc === OpenWhere.addRight) return 'linkOnRight';
+        if (linkButton && this.selectedDoc._followLinkLocation === OpenWhere.lightbox && linkedToLightboxView()) return 'enterPortal';
+        if (ScriptCast(this.selectedDoc.onClick)?.script.originalScript.includes('toggleDetail')) return 'toggleDetail';
+        return 'nothing';
     }
 
     @computed
@@ -385,20 +345,18 @@ export class PropertiesButtons extends React.Component<{}, {}> {
             ['linkOnRight', 'Open Link on Right'],
         ];
 
-        const items: IListItemProps[] = buttonList.map(value => {
-            return {
-                text: value[1],
-                val: value[1],
-            };
-        });
+        const items: IListItemProps[] = buttonList.map(value => ({
+            text: value[1],
+            val: value[1],
+        }));
         return !this.selectedDoc ? null : (
             <Dropdown
-                tooltip={'Choose onClick behavior'}
+                tooltip="Choose onClick behavior"
                 items={items}
-                closeOnSelect={true}
+                closeOnSelect
                 selectedVal={this.onClickVal}
                 setSelectedVal={val => this.handleOptionChange(val as string)}
-                title={'Choose onClick behaviour'}
+                title="Choose onClick behaviour"
                 color={SettingsManager.userColor}
                 dropdownType={DropdownType.SELECT}
                 type={Type.SEC}
@@ -440,16 +398,11 @@ export class PropertiesButtons extends React.Component<{}, {}> {
                     docView.toggleFollowLink(false, false);
                     docView.Document.followLinkLocation = linkButton ? OpenWhere.addRight : undefined;
                     break;
+                default:
             }
         });
     };
 
-    @undoBatch
-    editOnClickScript = () => {
-        if (SelectionManager.Views.length) SelectionManager.Views.forEach(dv => DocUtils.makeCustomViewClicked(dv.Document, undefined, 'onClick'));
-        else this.selectedDoc && DocUtils.makeCustomViewClicked(this.selectedDoc, undefined, 'onClick');
-    };
-
     @computed
     get onClickFlyout() {
         const buttonList = [
@@ -473,6 +426,7 @@ export class PropertiesButtons extends React.Component<{}, {}> {
                 case 'enterPortal': active = linkButton && this.selectedDoc._followLinkLocation === OpenWhere.lightbox && linkedToLightboxView(); break;
                 case 'toggleDetail':active = ScriptCast(this.selectedDoc.onClick)?.script.originalScript.includes('toggleDetail'); break;
                 case 'nothing':     active = !linkButton && this.selectedDoc.onClick === undefined;break;
+                default:
             }
             return (
                 <div className="list-item" key={`${value}`} style={{ backgroundColor: active ? Colors.LIGHT_BLUE : undefined }} onClick={click}>
@@ -494,6 +448,36 @@ export class PropertiesButtons extends React.Component<{}, {}> {
             </div>
         );
     }
+    @undoBatch
+    editOnClickScript = () => {
+        if (SelectionManager.Views.length) SelectionManager.Views.forEach(dv => DocUtils.makeCustomViewClicked(dv.Document, undefined, 'onClick'));
+        else this.selectedDoc && DocUtils.makeCustomViewClicked(this.selectedDoc, undefined, 'onClick');
+    };
+
+    propertyToggleBtn = (label: (on?: any) => string, property: string, tooltip: (on?: any) => string, icon: (on?: any) => any, onClick?: (dv: Opt<DocumentView>, doc: Doc, property: string) => void, useUserDoc?: boolean) => {
+        const targetDoc = useUserDoc ? Doc.UserDoc() : this.selectedLayoutDoc;
+        const onPropToggle = (dv: Opt<DocumentView>, doc: Doc, prop: string) => {
+            (dv?.layoutDoc || doc)[prop] = !(dv?.layoutDoc || doc)[prop];
+        };
+        return !targetDoc ? null : (
+            <Toggle
+                toggleStatus={BoolCast(targetDoc[property])}
+                tooltip={tooltip(BoolCast(targetDoc[property]))}
+                text={label(targetDoc?.[property])}
+                color={SettingsManager.userColor}
+                icon={icon(targetDoc?.[property] as any)}
+                iconPlacement="left"
+                align="flex-start"
+                fillWidth
+                toggleType={ToggleType.BUTTON}
+                onClick={undoable(() => {
+                    if (SelectionManager.Views.length > 1) {
+                        SelectionManager.Views.forEach(dv => (onClick ?? onPropToggle)(dv, dv.Document, property));
+                    } else if (targetDoc) (onClick ?? onPropToggle)(undefined, targetDoc, property);
+                }, property)}
+            />
+        );
+    };
 
     render() {
         const layoutField = this.selectedDoc?.[Doc.LayoutFieldKey(this.selectedDoc)];
@@ -505,11 +489,9 @@ export class PropertiesButtons extends React.Component<{}, {}> {
         const isStacking = [CollectionViewType.Stacking, CollectionViewType.Masonry, CollectionViewType.NoteTaking].includes(this.selectedDoc?._type_collection as any);
         const isFreeForm = this.selectedDoc?._type_collection === CollectionViewType.Freeform;
         const isTree = this.selectedDoc?._type_collection === CollectionViewType.Tree;
-        const isTabView = this.selectedTabView;
         const toggle = (ele: JSX.Element | null, style?: React.CSSProperties) => (
             <div className="propertiesButtons-button" style={style}>
-                {' '}
-                {ele}{' '}
+                {ele}
             </div>
         );
         const isNovice = Doc.noviceMode;
diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx
index 2a847e51a..8ac0f7d7b 100644
--- a/src/client/views/PropertiesView.tsx
+++ b/src/client/views/PropertiesView.tsx
@@ -11,8 +11,8 @@ import { IReactionDisposer, action, computed, makeObservable, observable, reacti
 import { observer } from 'mobx-react';
 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 { ClientUtils, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue, setupMoveUpEvents } from '../../ClientUtils';
+import * as Icons from 'react-icons/bs'; // {BsCollectionFill, BsFillFileEarmarkImageFill} from "react-icons/bs"
+import { ClientUtils, returnEmptyDoclist, returnEmptyFilter, returnEmptyString, returnFalse, returnTrue, setupMoveUpEvents } from '../../ClientUtils';
 import { emptyFunction } from '../../Utils';
 import { Doc, Field, FieldType, FieldResult, HierarchyMapping, NumListCast, Opt, ReverseHierarchyMap, StrListCast } from '../../fields/Doc';
 import { AclAdmin, DocAcl, DocData } from '../../fields/DocSymbols';
@@ -45,6 +45,7 @@ import { DocumentView, OpenWhere } from './nodes/DocumentView';
 import { StyleProviderFuncType } from './nodes/FieldView';
 import { KeyValueBox } from './nodes/KeyValueBox';
 import { PresBox, PresEffect, PresEffectDirection } from './nodes/trails';
+
 const _global = (window /* browser */ || global) /* node */ as any;
 
 interface PropertiesViewProps {
@@ -218,7 +219,7 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
 
             rows.push(
                 <div className="propertiesView-field" key="newKeyValue" style={{ marginTop: '3px', backgroundColor: SettingsManager.userBackgroundColor, textAlign: 'center' }}>
-                    <EditableView key="editableView" oneLine contents="add key:value or #tags" height={13} fontSize={10} GetValue={() => ''} SetValue={this.setKeyValue} />
+                    <EditableView key="editableView" oneLine contents="add key:value or #tags" height={13} fontSize={10} GetValue={returnEmptyString} SetValue={this.setKeyValue} />
                 </div>
             );
         }
@@ -264,13 +265,13 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
     @observable transform: Transform = Transform.Identity();
     getTransform = () => this.transform;
     propertiesDocViewRef = (ref: HTMLDivElement) => {
-        const observer = new _global.ResizeObserver(
+        const resizeObserver = new _global.ResizeObserver(
             action(() => {
                 const cliRect = ref.getBoundingClientRect();
                 this.transform = new Transform(-cliRect.x, -cliRect.y, 1);
             })
         );
-        ref && observer.observe(ref);
+        ref && resizeObserver.observe(ref);
     };
 
     @computed get contexts() {
@@ -359,9 +360,9 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
         if (permission === '-multiple-') dropdownValues.unshift(permission);
         return (
             <select className="propertiesView-permissions-select" value={permission} onChange={e => this.changePermissions(e, user)}>
-                {dropdownValues.map(permission => (
-                    <option className="propertiesView-permisssions-select" key={permission} value={permission}>
-                        {concat(ReverseHierarchyMap.get(permission)?.image, ' ', permission)}
+                {dropdownValues.map(permissionVal => (
+                    <option className="propertiesView-permisssions-select" key={permissionVal} value={permissionVal}>
+                        {concat(ReverseHierarchyMap.get(permissionVal)?.image, ' ', permissionVal)}
                     </option>
                 ))}
             </select>
@@ -404,10 +405,8 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
     /**
      * @returns a row of the permissions panel
      */
-    sharingItem(name: string, admin: boolean, permission: string, showExpansionIcon?: boolean) {
-        if (name === ClientUtils.CurrentUserEmail()) {
-            name = 'Me';
-        }
+    sharingItem(nameIn: string, admin: boolean, permission: string, showExpansionIcon?: boolean) {
+        const name = nameIn === ClientUtils.CurrentUserEmail() ? 'Me' : nameIn;
         return (
             <div
                 className="propertiesView-sharingTable-item"
@@ -466,7 +465,7 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
         const docs = SelectionManager.Views.length < 2 && this.selectedDoc ? [this.selectedDoc] : SelectionManager.Views.map(docView => docView.Document);
         const target = docs[0];
 
-        const showAdmin = GetEffectiveAcl(target) == AclAdmin;
+        const showAdmin = GetEffectiveAcl(target) === AclAdmin;
         const individualTableEntries = [];
         const usersAdded: string[] = []; // all shared users being added - organized by denormalized email
 
@@ -841,7 +840,7 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
 
     colorButton(value: string, type: string, setter: () => void) {
         return (
-            <div className="color-button" key="color" onPointerDown={action(e => setter())}>
+            <div className="color-button" key="color" onPointerDown={action(() => setter())}>
                 <div
                     className="color-button-preview"
                     style={{
@@ -858,9 +857,11 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
 
     colorPicker(color: string, setter: (color: string) => void) {
         return (
-            // prettier-ignore
             <SketchPicker
-                onChange={undoable( action((color: ColorResult) => setter(color.hex)), 'set stroke color property'  )}
+                onChange={undoable(
+                    action((col: ColorResult) => setter(col.hex)),
+                    'set stroke color property'
+                )}
                 presetColors={['#D0021B', '#F5A623', '#F8E71C', '#8B572A', '#7ED321', '#417505', '#9013FE', '#4A90E2', '#50E3C2', '#B8E986', '#000000', '#4A4A4A', '#9B9B9B', '#FFFFFF', '#f1efeb', 'transparent']}
                 color={color}
             />
@@ -928,13 +929,6 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
         this.selectedDoc && (this.selectedDoc[DocData].stroke_endMarker = value);
     }
 
-    @computed get stkInput() {
-        return this.regInput('stk', this.widthStk, (val: string) => { this.widthStk = val; }); // prettier-ignore
-    }
-    @computed get markScaleInput() {
-        return this.regInput('scale', this.markScal.toString(), (val: string) => { this.markScal = Number(val); }); // prettier-ignore
-    }
-
     regInput = (key: string, value: any, setter: (val: string) => {}) => (
         <div className="inputBox">
             <input className="inputBox-input" type="text" value={value} style={{ color: SettingsManager.userColor, backgroundColor: SettingsManager.userBackgroundColor }} onChange={e => setter(e.target.value)} />
@@ -1640,7 +1634,7 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
                         <div className="propertiesView-input inline" style={{ display: 'grid', gridTemplateColumns: '78px calc(100% - 108px) 50px' }}>
                             <p>Zoom %</p>
                             <div className="ribbon-property" style={{ display: !targZoom ? 'none' : 'inline-flex' }}>
-                                <input className="presBox-input" style={{ width: '100%', color: SettingsManager.userColor, backgroundColor: SettingsManager.userBackgroundColor }} readOnly={true} type="number" value={zoom} />
+                                <input className="presBox-input" style={{ width: '100%', color: SettingsManager.userColor, backgroundColor: SettingsManager.userBackgroundColor }} readOnly type="number" value={zoom} />
                                 <div className="ribbon-propertyUpDown" style={{ display: 'flex', flexDirection: 'column' }}>
                                     <div className="ribbon-propertyUpDownItem" onClick={undoBatch(() => this.setZoom(String(zoom), 0.1))}>
                                         <FontAwesomeIcon icon="caret-up" />
@@ -1766,7 +1760,7 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
                                     color: SettingsManager.userColor,
                                     backgroundColor: this.openPresTransitions ? SettingsManager.userVariantColor : SettingsManager.userBackgroundColor,
                                 }}>
-                                &nbsp; <FontAwesomeIcon style={{ alignSelf: 'center' }} icon={'rocket'} /> &nbsp; Transitions
+                                &nbsp; <FontAwesomeIcon style={{ alignSelf: 'center' }} icon="rocket" /> &nbsp; Transitions
                                 <div className="propertiesView-presentationTrails-title-icon">
                                     <FontAwesomeIcon icon={this.openPresTransitions ? 'caret-down' : 'caret-right'} size="lg" />
                                 </div>
diff --git a/src/client/views/SidebarAnnos.tsx b/src/client/views/SidebarAnnos.tsx
index db3f29fae..6195dcde8 100644
--- a/src/client/views/SidebarAnnos.tsx
+++ b/src/client/views/SidebarAnnos.tsx
@@ -1,3 +1,5 @@
+/* eslint-disable jsx-a11y/no-static-element-interactions */
+/* eslint-disable jsx-a11y/click-events-have-key-events */
 import { computed, makeObservable } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
@@ -197,7 +199,7 @@ export class SidebarAnnos extends ObservableReactComponent<FieldViewProps & Extr
                 </div>
             );
         };
-        const renderMeta = (tag: string, dflt: FieldResult<FieldType>) => {
+        const renderMeta = (tag: string) => {
             const active = this.childFilters().includes(`${tag}${Doc.FilterSep}${Doc.FilterAny}${Doc.FilterSep}exists`);
             return (
                 <div key={tag} className={`sidebarAnnos-filterTag${active ? '-active' : ''}`} onClick={e => Doc.setDocFilter(this._props.Document, tag, Doc.FilterAny, 'exists', true, undefined, e.shiftKey)}>
@@ -228,13 +230,12 @@ export class SidebarAnnos extends ObservableReactComponent<FieldViewProps & Extr
                 <div className="sidebarAnnos-tagList" style={{ height: this.filtersHeight() }} onWheel={e => e.stopPropagation()}>
                     {this.allUsers.length > 1 ? this.allUsers.map(renderUsers) : null}
                     {this.allHashtags.map(renderTag)}
-                    {Array.from(this.allMetadata.keys())
-                        .sort()
-                        .map(key => renderMeta(key, this.allMetadata.get(key)))}
+                    {Array.from(this.allMetadata.keys()).sort().map(renderMeta)}
                 </div>
 
                 <div style={{ width: '100%', height: `calc(100% - 38px)`, position: 'relative' }}>
                     <CollectionStackingView
+                        // eslint-disable-next-line react/jsx-props-no-spreading
                         {...this._props}
                         setContentViewBox={emptyFunction}
                         NativeWidth={returnZero}
@@ -248,11 +249,11 @@ export class SidebarAnnos extends ObservableReactComponent<FieldViewProps & Extr
                         isAnnotationOverlay={false}
                         select={emptyFunction}
                         NativeDimScaling={returnOne}
-                        //childlayout_showTitle={this.layout_showTitle}
+                        // childlayout_showTitle={this.layout_showTitle}
                         isAnyChildContentActive={returnFalse}
                         childDocumentsActive={this._props.isContentActive}
                         whenChildContentsActiveChanged={this._props.whenChildContentsActiveChanged}
-                        childHideDecorationTitle={true}
+                        childHideDecorationTitle
                         removeDocument={this.removeDocument}
                         moveDocument={this.moveDocument}
                         addDocument={this.addDocument}
diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx
index d8adfa68a..75f1a7d80 100644
--- a/src/client/views/StyleProvider.tsx
+++ b/src/client/views/StyleProvider.tsx
@@ -1,3 +1,6 @@
+/* eslint-disable jsx-a11y/alt-text */
+/* eslint-disable jsx-a11y/no-static-element-interactions */
+/* eslint-disable jsx-a11y/click-events-have-key-events */
 import { IconProp } from '@fortawesome/fontawesome-svg-core';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { Tooltip } from '@mui/material';
@@ -74,11 +77,10 @@ function togglePaintView(e: React.MouseEvent, doc: Opt<Doc>, props: Opt<FieldVie
 export function styleFromLayoutString(doc: Doc, props: FieldViewProps, scale: number) {
     const style: { [key: string]: any } = {};
     const divKeys = ['width', 'height', 'fontSize', 'transform', 'left', 'backgroundColor', 'left', 'right', 'top', 'bottom', 'pointerEvents', 'position'];
-    const replacer = (match: any, expr: string, offset: any, string: any) => {
+    const replacer = (match: any, expr: string) =>
         // bcz: this executes a script to convert a property expression string:  { script }  into a value
-        return ScriptField.MakeFunction(expr, { self: Doc.name, this: Doc.name, scale: 'number' })?.script.run({ this: doc, self: doc, scale }).result?.toString() ?? '';
-    };
-    divKeys.map((prop: string) => {
+        ScriptField.MakeFunction(expr, { self: Doc.name, this: Doc.name, scale: 'number' })?.script.run({ this: doc, self: doc, scale }).result?.toString() ?? '';
+    divKeys.forEach((prop: string) => {
         const p = (props as any)[prop];
         typeof p === 'string' && (style[prop] = p?.replace(/{([^.'][^}']+)}/g, replacer));
     });
@@ -105,20 +107,18 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps &
     const isNonTransparentLevel = isNonTransparent ? Number(property.replace(/.*:nonTransparent([0-9]+).*/, '$1')) : 0; // property.includes(':nonTransparent');
     const isAnnotated = property.includes(':annotated');
     const layoutDoc = doc ? Doc.Layout(doc) : doc;
-    const isInk = () => layoutDoc?._layout_isSvg && !props?.LayoutTemplateString;
     const isOpen = property.includes(':open');
-    const isEmpty = property.includes(':empty');
     const boxBackground = property.includes(':box');
     const fieldKey = props?.fieldKey ? props.fieldKey + '_' : isCaption ? 'caption_' : '';
+    const isInk = () => layoutDoc?._layout_isSvg && !props?.LayoutTemplateString;
     const lockedPosition = () => doc && BoolCast(doc._lockedPosition);
     const titleHeight = () => props?.styleProvider?.(doc, props, StyleProp.TitleHeight);
     const backgroundCol = () => props?.styleProvider?.(doc, props, StyleProp.BackgroundColor + ':nonTransparent' + (isNonTransparentLevel + 1));
-    const color = () => props?.styleProvider?.(doc, props, StyleProp.Color);
     const opacity = () => props?.styleProvider?.(doc, props, StyleProp.Opacity);
-    const layout_showTitle = () => props?.styleProvider?.(doc, props, StyleProp.ShowTitle);
+    const layoutShowTitle = () => props?.styleProvider?.(doc, props, StyleProp.ShowTitle);
     // prettier-ignore
     switch (property.split(':')[0]) {
-        case StyleProp.TreeViewIcon:
+        case StyleProp.TreeViewIcon: {
             const img = ImageCast(doc?.icon, ImageCast(doc?.data));
             if (img) {
                 const ext = extname(img.url.href);
@@ -126,13 +126,15 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps &
                 return <img src={url} width={20} height={15} style={{ margin: 'auto', display: 'block', objectFit: 'contain' }} />;
             }
             return Doc.toIcon(doc, isOpen);
-        case StyleProp.TreeViewSortings:
+        }
+        case StyleProp.TreeViewSortings: {
             const allSorts: { [key: string]: { color: string; icon: JSX.Element | string } | undefined } = {};
             allSorts[TreeSort.AlphaDown] = { color: Colors.MEDIUM_BLUE, icon: <BsArrowDown/> };
             allSorts[TreeSort.AlphaUp] = { color: 'crimson', icon: <BsArrowUp/> };
             if (doc?._type_collection === CollectionViewType.Freeform) allSorts[TreeSort.Zindex] = { color: 'green', icon: 'Z' };
             allSorts[TreeSort.WhenAdded] = { color: 'darkgray', icon: <BsArrowDownUp/> };
             return allSorts;
+        }
         case StyleProp.Highlighting:
             if (doc && (Doc.IsSystem(doc) || doc.type === DocumentType.FONTICON)) return undefined;            
             if (doc && !doc.layout_disableBrushing && !props?.disableBrushing) {
@@ -178,7 +180,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps &
                     )) ||
                 ''
             );
-        case StyleProp.Color:
+        case StyleProp.Color: {
             if (SettingsManager.Instance.LastPressedBtn === doc) return SettingsManager.userBackgroundColor;
             if (Doc.IsSystem(doc!)) return SettingsManager.userColor;
             if (doc?.type === DocumentType.FONTICON) return SettingsManager.userColor;
@@ -186,30 +188,33 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps &
             if (docColor) return docColor;
             const backColor = backgroundCol();
             return backColor ? lightOrDark(backColor) : undefined;
-        case StyleProp.BorderRounding:
+        }
+        case StyleProp.BorderRounding: {
             const rounding = StrCast(doc?.[fieldKey + 'borderRounding'], StrCast(doc?.layout_borderRounding, doc?._type_collection === CollectionViewType.Pile ? '50%' : ''));
             return (doc?.[StrCast(doc?.layout_fieldKey)] instanceof Doc || doc?.isTemplateDoc) ? StrCast(doc._layout_borderRounding,rounding) : rounding;
-        case StyleProp.BorderPath:
+        }
+        case StyleProp.BorderPath: {
             const borderPath = Doc.IsComicStyle(doc) &&
                 props?.renderDepth &&
                 !doc?.layout_isSvg && { path: wavyBorderPath(props?.PanelWidth?.() || 0, props?.PanelHeight?.() || 0), fill: wavyBorderPath(props?.PanelWidth?.() || 0, props?.PanelHeight?.() || 0, 0.08), width: 3 };
-            return !borderPath
-                ? null
-                : {
-                      clipPath: `path('${borderPath.path}')`,
-                      jsx: (
-                          <div key="border2" className="documentView-customBorder" style={{ pointerEvents: 'none' }}>
-                              <svg style={{ overflow: 'visible', height: '100%' }} viewBox={`0 0 ${props.PanelWidth()} ${props.PanelHeight()}`}>
-                                  <path d={borderPath.path} style={{ stroke: 'black', fill: 'transparent', strokeWidth: borderPath.width }} />
-                              </svg>
-                          </div>
-                      ),
-                  };
+                return !borderPath
+                    ? null
+                    : {
+                        clipPath: `path('${borderPath.path}')`,
+                        jsx: (
+                            <div key="border2" className="documentView-customBorder" style={{ pointerEvents: 'none' }}>
+                                <svg style={{ overflow: 'visible', height: '100%' }} viewBox={`0 0 ${props.PanelWidth()} ${props.PanelHeight()}`}>
+                                    <path d={borderPath.path} style={{ stroke: 'black', fill: 'transparent', strokeWidth: borderPath.width }} />
+                                </svg>
+                            </div>
+                        ),
+                    };
+        }
         case StyleProp.HeaderMargin:
             return ([CollectionViewType.Stacking, CollectionViewType.NoteTaking, CollectionViewType.Masonry, CollectionViewType.Tree].includes(doc?._type_collection as any) ||
-                (doc?.type === DocumentType.RTF && !layout_showTitle()?.includes('noMargin')) ||
+                (doc?.type === DocumentType.RTF && !layoutShowTitle()?.includes('noMargin')) ||
                 doc?.type === DocumentType.LABEL) &&
-                layout_showTitle() &&
+                layoutShowTitle() &&
                 !StrCast(doc?.layout_showTitle).includes(':hover')
                 ? titleHeight()
                 : 0;
@@ -248,7 +253,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps &
                             (Colors.DARK_GRAY)
                             : Cast((props?.renderDepth || 0) > 0 ? Doc.UserDoc().activeCollectionNestedBackground : Doc.UserDoc().activeCollectionBackground, 'string') ?? (Colors.MEDIUM_GRAY));
                     break;
-                //if (doc._type_collection !== CollectionViewType.Freeform && doc._type_collection !== CollectionViewType.Time) return "rgb(62,62,62)";
+                // if (doc._type_collection !== CollectionViewType.Freeform && doc._type_collection !== CollectionViewType.Time) return "rgb(62,62,62)";
                 default: docColor = docColor || (Colors.WHITE);
             }
             if (isNonTransparent && isNonTransparentLevel < 9 && (!docColor || docColor === 'transparent') && doc?.embedContainer && props?.styleProvider) {
@@ -273,6 +278,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps &
 
                 case DocumentType.LABEL:
                     if (doc?.annotationOn !== undefined) return 'black 2px 2px 1px';
+                // eslint-disable-next-line no-fallthrough
                 default:
                     return doc.z
                         ? `#9c9396  ${StrCast(doc?.layout_boxShadow, '10px 10px 0.9vw')}` // if it's a floating doc, give it a big shadow
@@ -299,7 +305,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps &
                 isGroup )? undefined: 'all' 
             if (props?.isDocumentActive?.()) return isInk() ? 'visiblePainted' : 'all';
             return undefined; // fixes problem with tree view elements getting pointer events when the tree view is not active
-        case StyleProp.Decorations:
+        case StyleProp.Decorations: {
             const lock = () => doc?.pointerEvents !== 'none' ? null : (
                         <div className="styleProvider-lock" onClick={() => toggleLockedPosition(doc)}>
                             <FontAwesomeIcon icon='lock' size="lg" />
@@ -316,16 +322,17 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps &
                     StrListCast(doc?._childFilters).length || StrListCast(doc?._childFiltersByRanges).length
                         ? 'green' // #18c718bd' //'hasFilter'
                         : props?.childFilters?.().filter(f => ClientUtils.IsRecursiveFilter(f) && f !== ClientUtils.noDragDocsFilter).length || props?.childFiltersByRanges().length
-                        ? 'orange' //'inheritsFilter'
+                        ? 'orange' // 'inheritsFilter'
                         : undefined;
                 return !showFilterIcon ? null : (
                     <div className="styleProvider-filter">
                         <Dropdown
                             type={Type.TERT}
                             dropdownType={DropdownType.CLICK}
-                        fillWidth
-                            iconProvider={(active:boolean) => <div className='styleProvider-filterShift'><FaFilter/></div>}
-                            closeOnSelect={true}
+                            fillWidth
+                            // eslint-disable-next-line react/no-unstable-nested-components
+                            iconProvider={() => <div className='styleProvider-filterShift'><FaFilter/></div>}
+                            closeOnSelect
                             setSelectedVal={
                                 action((dv) => {
                                     (dv as any).select(false);
@@ -378,6 +385,8 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps &
                     {audio()}
                 </>
             );
+        }
+        default:
     }
 }
 
diff --git a/src/client/views/UndoStack.tsx b/src/client/views/UndoStack.tsx
index 068143225..2d461c0ab 100644
--- a/src/client/views/UndoStack.tsx
+++ b/src/client/views/UndoStack.tsx
@@ -1,6 +1,7 @@
+/* eslint-disable jsx-a11y/no-static-element-interactions */
+/* eslint-disable jsx-a11y/click-events-have-key-events */
 import { Tooltip } from '@mui/material';
 import { Popup, Type } from 'browndash-components';
-import { observable } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
 import { StrCast } from '../../fields/Types';
@@ -15,19 +16,19 @@ export class UndoStack extends React.Component<UndoStackProps> {
         const background = UndoManager.batchCounter.get() ? 'yellow' : SettingsManager.userVariantColor;
         const color = UndoManager.batchCounter.get() ? 'black' : SettingsManager.userColor;
         return (
-            <Tooltip title={'undo stack (if it stays yellow, undo is broken - you should reload Dash)'}>
+            <Tooltip title="undo stack (if it stays yellow, undo is broken - you should reload Dash)">
                 <div>
                     <div className="undoStack-outerContainer">
                         <Popup
                             text="stack"
                             color={color}
                             background={background}
-                            placement={`top-start`}
+                            placement="top-start"
                             type={Type.TERT}
                             popup={
                                 <div
                                     className="undoStack-commandsContainer"
-                                    ref={r => r?.scroll({ behavior: 'auto', top: r?.scrollHeight + 20 })}
+                                    ref={r => r?.scroll({ behavior: 'auto', top: (r?.scrollHeight ?? 0) + 20 })}
                                     style={{
                                         background,
                                         color,
@@ -35,12 +36,13 @@ export class UndoStack extends React.Component<UndoStackProps> {
                                     {Array.from(UndoManager.undoStackNames).map((name, i) => (
                                         <div
                                             className="undoStack-resultContainer"
+                                            // eslint-disable-next-line react/no-array-index-key
                                             key={i}
-                                            onClick={e => {
+                                            onClick={() => {
                                                 const size = UndoManager.undoStackNames.length;
                                                 for (let n = 0; n < size - i; n++) UndoManager.Undo();
                                             }}>
-                                            <div className="undoStack-commandString">{StrCast(name).replace(/[^\.]*\./, '')}</div>
+                                            <div className="undoStack-commandString">{StrCast(name).replace(/[^.]*\./, '')}</div>
                                         </div>
                                     ))}
                                     {Array.from(UndoManager.redoStackNames)
@@ -48,12 +50,13 @@ export class UndoStack extends React.Component<UndoStackProps> {
                                         .map((name, i) => (
                                             <div
                                                 className="undoStack-resultContainer"
+                                                // eslint-disable-next-line react/no-array-index-key
                                                 key={i}
-                                                onClick={e => {
+                                                onClick={() => {
                                                     for (let n = 0; n <= i; n++) UndoManager.Redo();
                                                 }}>
                                                 <div className="undoStack-commandString" style={{ fontWeight: 'bold', background: SettingsManager.userBackgroundColor, color: SettingsManager.userColor }}>
-                                                    {StrCast(name).replace(/[^\.]*\./, '')}
+                                                    {StrCast(name).replace(/[^.]*\./, '')}
                                                 </div>
                                             </div>
                                         ))}
diff --git a/src/client/views/animationtimeline/Timeline.tsx b/src/client/views/animationtimeline/Timeline.tsx
index c3e513154..f03e316a2 100644
--- a/src/client/views/animationtimeline/Timeline.tsx
+++ b/src/client/views/animationtimeline/Timeline.tsx
@@ -9,6 +9,7 @@ import { Utils, emptyFunction } from '../../../Utils';
 import { Doc, DocListCast } from '../../../fields/Doc';
 import { BoolCast, NumCast, StrCast } from '../../../fields/Types';
 import { DocumentType } from '../../documents/DocumentTypes';
+// eslint-disable-next-line import/extensions
 import clamp from '../../util/clamp';
 import { ObservableReactComponent } from '../ObservableReactComponent';
 import { FieldViewProps } from '../nodes/FieldView';
diff --git a/src/client/views/animationtimeline/Track.tsx b/src/client/views/animationtimeline/Track.tsx
index 490a14be5..1e4ed74be 100644
--- a/src/client/views/animationtimeline/Track.tsx
+++ b/src/client/views/animationtimeline/Track.tsx
@@ -73,7 +73,7 @@ export class Track extends ObservableReactComponent<IProps> {
         this._timelineVisibleReaction?.();
         this._autoKfReaction?.();
     }
-    ////////////////////////////////
+    // //////////////////////////////
 
     getLastRegionTime = () => {
         let lastTime: number = 0;
diff --git a/src/client/views/collections/CollectionMasonryViewFieldRow.tsx b/src/client/views/collections/CollectionMasonryViewFieldRow.tsx
index 2d906dfe7..8803f6f79 100644
--- a/src/client/views/collections/CollectionMasonryViewFieldRow.tsx
+++ b/src/client/views/collections/CollectionMasonryViewFieldRow.tsx
@@ -1,10 +1,13 @@
+/* eslint-disable jsx-a11y/control-has-associated-label */
+/* eslint-disable jsx-a11y/no-static-element-interactions */
+/* eslint-disable jsx-a11y/click-events-have-key-events */
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { action, computed, makeObservable, observable, runInAction } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
 import { returnEmptyString, returnFalse, setupMoveUpEvents } from '../../../ClientUtils';
 import { emptyFunction, numberRange } from '../../../Utils';
-import { Doc, DocListCast } from '../../../fields/Doc';
+import { Doc } from '../../../fields/Doc';
 import { DocData } from '../../../fields/DocSymbols';
 import { PastelSchemaPalette, SchemaHeaderField } from '../../../fields/SchemaHeaderField';
 import { ScriptField } from '../../../fields/ScriptField';
@@ -52,13 +55,19 @@ export class CollectionMasonryViewFieldRow extends ObservableReactComponent<CMVF
     @observable private collapsed: boolean = false;
     @observable private _paletteOn = false;
     private set _heading(value: string) {
-        runInAction(() => this._props.headingObject && (this._props.headingObject.heading = this.heading = value));
+        runInAction(() => {
+            this._props.headingObject && (this._props.headingObject.heading = this.heading = value);
+        });
     }
     private set _color(value: string) {
-        runInAction(() => this._props.headingObject && (this._props.headingObject.color = this.color = value));
+        runInAction(() => {
+            this._props.headingObject && (this._props.headingObject.color = this.color = value);
+        });
     }
     private set _collapsed(value: boolean) {
-        runInAction(() => this._props.headingObject && (this._props.headingObject.collapsed = this.collapsed = value));
+        runInAction(() => {
+            this._props.headingObject && (this._props.headingObject.collapsed = this.collapsed = value);
+        });
     }
 
     private _dropDisposer?: DragManager.DragDropDisposer;
@@ -88,7 +97,7 @@ export class CollectionMasonryViewFieldRow extends ObservableReactComponent<CMVF
         if (this.collapsed) {
             this._props.setDocHeight(this.heading, 20);
         } else {
-            const rawHeight = this._contRef.current!.getBoundingClientRect().height + 15; //+ 15 accounts for the group header
+            const rawHeight = this._contRef.current!.getBoundingClientRect().height + 15; // +15 accounts for the group header
             const transformScale = this._props.screenToLocalTransform().Scale;
             const trueHeight = rawHeight * transformScale;
             this._props.setDocHeight(this.heading, trueHeight);
@@ -102,7 +111,7 @@ export class CollectionMasonryViewFieldRow extends ObservableReactComponent<CMVF
             const key = this._props.pivotField;
             const castedValue = this.getValue(this.heading);
             if (this._props.parent.onInternalDrop(e, de)) {
-                key && de.complete.docDragData.droppedDocuments.forEach(d => Doc.SetInPlace(d, key, castedValue, !this.onLayoutDoc(key)));
+                key && de.complete.docDragData.droppedDocuments.forEach(d => Doc.SetInPlace(d, key, castedValue, true));
             }
             return true;
         }
@@ -118,7 +127,7 @@ export class CollectionMasonryViewFieldRow extends ObservableReactComponent<CMVF
     };
 
     @action
-    headingChanged = (value: string, shiftDown?: boolean) => {
+    headingChanged = (value: string /* , shiftDown?: boolean */) => {
         this._createEmbeddingSelected = false;
         const key = this._props.pivotField;
         const castedValue = this.getValue(value);
@@ -141,7 +150,9 @@ export class CollectionMasonryViewFieldRow extends ObservableReactComponent<CMVF
         this._color = color;
     };
 
-    pointerEnteredRow = action(() => SnappingManager.IsDragging && (this._background = '#b4b4b4'));
+    pointerEnteredRow = action(() => {
+        SnappingManager.IsDragging && (this._background = '#b4b4b4');
+    });
 
     @action
     pointerLeaveRow = () => {
@@ -153,13 +164,13 @@ export class CollectionMasonryViewFieldRow extends ObservableReactComponent<CMVF
     addDocument = (value: string, shiftDown?: boolean, forceEmptyNote?: boolean) => {
         if (!value && !forceEmptyNote) return false;
         this._createEmbeddingSelected = false;
-        const key = this._props.pivotField;
+        const { pivotField } = this._props;
         const newDoc = Docs.Create.TextDocument('', { _layout_autoHeight: true, _width: 200, _layout_fitWidth: true, title: value });
         FormattedTextBox.SetSelectOnLoad(newDoc);
         FormattedTextBox.SelectOnLoadChar = value;
-        key && ((this.onLayoutDoc(key) ? newDoc : newDoc[DocData])[key] = this.getValue(this._props.heading));
+        pivotField && (newDoc[DocData][pivotField] = this.getValue(this._props.heading));
         const docs = this._props.parent.childDocList;
-        return docs ? (docs.splice(0, 0, newDoc) ? true : false) : this._props.parent._props.addDocument?.(newDoc) || false; // should really extend addDocument to specify insertion point (at beginning of list)
+        return docs ? !!docs.splice(0, 0, newDoc) : this._props.parent._props.addDocument?.(newDoc) || false; // should really extend addDocument to specify insertion point (at beginning of list)
     };
 
     deleteRow = undoBatch(
@@ -198,21 +209,11 @@ export class CollectionMasonryViewFieldRow extends ObservableReactComponent<CMVF
     @action
     headerDown = (e: React.PointerEvent<HTMLDivElement>) => {
         if (e.button === 0 && !e.ctrlKey) {
-            setupMoveUpEvents(this, e, this.headerMove, emptyFunction, e => !this._props.chromeHidden && this.collapseSection(e));
+            setupMoveUpEvents(this, e, this.headerMove, emptyFunction, clickEv => !this._props.chromeHidden && this.collapseSection(clickEv));
             this._createEmbeddingSelected = false;
         }
     };
 
-    /**
-     * Returns true if a key is on the layout doc of the documents in the collection.
-     */
-    onLayoutDoc = (key: string): boolean => {
-        DocListCast(this._props.parent.Document.data).forEach(doc => {
-            if (Doc.Get(doc, key, true)) return true;
-        });
-        return false;
-    };
-
     renderColorPicker = () => {
         const selected = this.color;
 
@@ -229,27 +230,29 @@ export class CollectionMasonryViewFieldRow extends ObservableReactComponent<CMVF
         return (
             <div className="collectionStackingView-colorPicker">
                 <div className="colorOptions">
-                    <div className={'colorPicker' + (selected === pink ? ' active' : '')} style={{ backgroundColor: pink }} onClick={() => this.changeColumnColor(pink!)}></div>
-                    <div className={'colorPicker' + (selected === purple ? ' active' : '')} style={{ backgroundColor: purple }} onClick={() => this.changeColumnColor(purple!)}></div>
-                    <div className={'colorPicker' + (selected === blue ? ' active' : '')} style={{ backgroundColor: blue }} onClick={() => this.changeColumnColor(blue!)}></div>
-                    <div className={'colorPicker' + (selected === yellow ? ' active' : '')} style={{ backgroundColor: yellow }} onClick={() => this.changeColumnColor(yellow!)}></div>
-                    <div className={'colorPicker' + (selected === red ? ' active' : '')} style={{ backgroundColor: red }} onClick={() => this.changeColumnColor(red!)}></div>
-                    <div className={'colorPicker' + (selected === gray ? ' active' : '')} style={{ backgroundColor: gray }} onClick={() => this.changeColumnColor(gray)}></div>
-                    <div className={'colorPicker' + (selected === green ? ' active' : '')} style={{ backgroundColor: green }} onClick={() => this.changeColumnColor(green!)}></div>
-                    <div className={'colorPicker' + (selected === cyan ? ' active' : '')} style={{ backgroundColor: cyan }} onClick={() => this.changeColumnColor(cyan!)}></div>
-                    <div className={'colorPicker' + (selected === orange ? ' active' : '')} style={{ backgroundColor: orange }} onClick={() => this.changeColumnColor(orange!)}></div>
+                    <div className={'colorPicker' + (selected === pink ? ' active' : '')} style={{ backgroundColor: pink }} onClick={() => this.changeColumnColor(pink!)} />
+                    <div className={'colorPicker' + (selected === purple ? ' active' : '')} style={{ backgroundColor: purple }} onClick={() => this.changeColumnColor(purple!)} />
+                    <div className={'colorPicker' + (selected === blue ? ' active' : '')} style={{ backgroundColor: blue }} onClick={() => this.changeColumnColor(blue!)} />
+                    <div className={'colorPicker' + (selected === yellow ? ' active' : '')} style={{ backgroundColor: yellow }} onClick={() => this.changeColumnColor(yellow!)} />
+                    <div className={'colorPicker' + (selected === red ? ' active' : '')} style={{ backgroundColor: red }} onClick={() => this.changeColumnColor(red!)} />
+                    <div className={'colorPicker' + (selected === gray ? ' active' : '')} style={{ backgroundColor: gray }} onClick={() => this.changeColumnColor(gray)} />
+                    <div className={'colorPicker' + (selected === green ? ' active' : '')} style={{ backgroundColor: green }} onClick={() => this.changeColumnColor(green!)} />
+                    <div className={'colorPicker' + (selected === cyan ? ' active' : '')} style={{ backgroundColor: cyan }} onClick={() => this.changeColumnColor(cyan!)} />
+                    <div className={'colorPicker' + (selected === orange ? ' active' : '')} style={{ backgroundColor: orange }} onClick={() => this.changeColumnColor(orange!)} />
                 </div>
             </div>
         );
     };
 
-    toggleEmbedding = action(() => (this._createEmbeddingSelected = true));
-    toggleVisibility = () => (this._collapsed = !this.collapsed);
+    toggleEmbedding = action(() => {
+        this._createEmbeddingSelected = true;
+    });
+    toggleVisibility = () => {
+        this._collapsed = !this.collapsed;
+    };
 
     @action
-    textCallback = (char: string) => {
-        return this.addDocument('', false);
-    };
+    textCallback = (/* char: string */) => this.addDocument('', false);
 
     @computed get contentLayout() {
         const rows = Math.max(1, Math.min(this._props.docList.length, Math.floor((this._props.parent._props.PanelWidth() - 2 * this._props.parent.xMargin) / (this._props.parent.columnWidth + this._props.parent.gridGap))));
@@ -263,22 +266,22 @@ export class CollectionMasonryViewFieldRow extends ObservableReactComponent<CMVF
                         className="collectionStackingView-addDocumentButton"
                         style={
                             {
-                                //width: style.columnWidth / style.numGroupColumns,
-                                //padding: `${NumCast(this._props.parent.layoutDoc._yPadding, this._props.parent.yMargin)}px 0px 0px 0px`,
+                                // width: style.columnWidth / style.numGroupColumns,
+                                // padding: `${NumCast(this._props.parent.layoutDoc._yPadding, this._props.parent.yMargin)}px 0px 0px 0px`,
                             }
                         }>
-                        <EditableView GetValue={returnEmptyString} SetValue={this.addDocument} textCallback={this.textCallback} contents={'+ NEW'} />
+                        <EditableView GetValue={returnEmptyString} SetValue={this.addDocument} textCallback={this.textCallback} contents="+ NEW" />
                     </div>
                 ) : null}
                 <div
-                    className={`collectionStackingView-masonryGrid`}
+                    className="collectionStackingView-masonryGrid"
                     ref={this._contRef}
                     style={{
                         padding: stackPad,
                         minHeight: this._props.showHandle && this._props.parent._props.isContentActive() ? '10px' : undefined,
                         width: this._props.parent.NodeWidth,
                         gridGap: this._props.parent.gridGap,
-                        gridTemplateColumns: numberRange(rows).reduce((list: string, i: any) => list + ` ${this._props.parent.columnWidth}px`, ''),
+                        gridTemplateColumns: numberRange(rows).reduce(list => list + ` ${this._props.parent.columnWidth}px`, ''),
                     }}>
                     {this._props.parent.children(this._props.docList)}
                 </div>
@@ -290,7 +293,7 @@ export class CollectionMasonryViewFieldRow extends ObservableReactComponent<CMVF
         const noChrome = this._props.chromeHidden;
         const key = this._props.pivotField;
         const evContents = this.heading ? this.heading : this._props.type && this._props.type === 'number' ? '0' : `NO ${key.toUpperCase()} VALUE`;
-        const editableHeaderView = <EditableView GetValue={() => evContents} SetValue={this.headingChanged} contents={evContents} oneLine={true} />;
+        const editableHeaderView = <EditableView GetValue={() => evContents} SetValue={this.headingChanged} contents={evContents} oneLine />;
         return this._props.Document.miniHeaders ? (
             <div className="collectionStackingView-miniHeader">{editableHeaderView}</div>
         ) : !this._props.headingObject ? null : (
@@ -304,6 +307,7 @@ export class CollectionMasonryViewFieldRow extends ObservableReactComponent<CMVF
                     {noChrome || evContents === `NO ${key.toUpperCase()} VALUE` ? null : (
                         <div className="collectionStackingView-sectionColor">
                             <button
+                                type="button"
                                 className="collectionStackingView-sectionColorButton"
                                 onPointerDown={e =>
                                     setupMoveUpEvents(
@@ -311,7 +315,9 @@ export class CollectionMasonryViewFieldRow extends ObservableReactComponent<CMVF
                                         e,
                                         returnFalse,
                                         emptyFunction,
-                                        action(e => (this._paletteOn = !this._paletteOn))
+                                        action(() => {
+                                            this._paletteOn = !this._paletteOn;
+                                        })
                                     )
                                 }>
                                 <FontAwesomeIcon icon="palette" size="lg" />
@@ -320,13 +326,13 @@ export class CollectionMasonryViewFieldRow extends ObservableReactComponent<CMVF
                         </div>
                     )}
                     {noChrome ? null : (
-                        <button className="collectionStackingView-sectionDelete" onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, noChrome ? emptyFunction : this.collapseSection)}>
+                        <button type="button" className="collectionStackingView-sectionDelete" onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, noChrome ? emptyFunction : this.collapseSection)}>
                             <FontAwesomeIcon icon={this.collapsed ? 'chevron-down' : 'chevron-up'} size="lg" />
                         </button>
                     )}
                     {noChrome || evContents === `NO  ${key.toUpperCase()} VALUE` ? null : (
                         <div className="collectionStackingView-sectionOptions" onPointerDown={e => e.stopPropagation()}>
-                            <button className="collectionStackingView-sectionOptionButton" onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, this.deleteRow)}>
+                            <button type="button" className="collectionStackingView-sectionOptionButton" onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, this.deleteRow)}>
                                 <FontAwesomeIcon icon="trash" size="lg" />
                             </button>
                         </div>
diff --git a/src/client/views/collections/CollectionMenu.tsx b/src/client/views/collections/CollectionMenu.tsx
index a23725348..5a509128d 100644
--- a/src/client/views/collections/CollectionMenu.tsx
+++ b/src/client/views/collections/CollectionMenu.tsx
@@ -36,6 +36,7 @@ interface CollectionMenuProps {
 
 @observer
 export class CollectionMenu extends AntimodeMenu<CollectionMenuProps> {
+    // eslint-disable-next-line no-use-before-define
     @observable static Instance: CollectionMenu;
     @observable SelectedCollection: DocumentView | undefined = undefined;
 
@@ -63,7 +64,7 @@ export class CollectionMenu extends AntimodeMenu<CollectionMenuProps> {
     }
 
     @action
-    toggleMenuPin = (e: React.MouseEvent) => {
+    toggleMenuPin = () => {
         Doc.UserDoc()['menuCollections-pinned'] = this.Pinned = !this.Pinned;
         if (!this.Pinned && this._left < 0) {
             this.jumpTo(300, 300);
@@ -165,7 +166,9 @@ export class CollectionMenu extends AntimodeMenu<CollectionMenuProps> {
 }
 
 interface CollectionViewMenuProps {
+    // eslint-disable-next-line react/no-unused-prop-types
     type: CollectionViewType;
+    // eslint-disable-next-line react/no-unused-prop-types
     fieldKey: string;
     docView: DocumentView;
 }
@@ -377,16 +380,18 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewMenu
         setupMoveUpEvents(
             this,
             e,
-            (e, down, delta) => {
+            moveEv => {
                 const vtype = this.props.type;
                 const c = {
                     params: ['target'],
                     title: vtype,
                     script: `this.target._type_collection = '${StrCast(this.props.type)}'`,
-                    immediate: (source: Doc[]) => (this.document._type_collection = Doc.getDocTemplate(source?.[0])),
+                    immediate: (source: Doc[]) => {
+                        this.document._type_collection = Doc.getDocTemplate(source?.[0]);
+                    },
                     initialize: emptyFunction,
                 };
-                DragManager.StartButtonDrag([this._viewRef.current!], c.script, StrCast(c.title), { target: this.document }, c.params, c.initialize, e.clientX, e.clientY);
+                DragManager.StartButtonDrag([this._viewRef.current!], c.script, StrCast(c.title), { target: this.document }, c.params, c.initialize, moveEv.clientX, moveEv.clientY);
                 return true;
             },
             emptyFunction,
@@ -397,8 +402,10 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewMenu
         setupMoveUpEvents(
             this,
             e,
-            (e, down, delta) => {
-                this._buttonizableCommands?.filter(c => c.title === this._currentKey).map(c => DragManager.StartButtonDrag([this._commandRef.current!], c.script, c.title, { target: this.document }, c.params, c.initialize, e.clientX, e.clientY));
+            moveEv => {
+                this._buttonizableCommands
+                    ?.filter(c => c.title === this._currentKey)
+                    .map(c => DragManager.StartButtonDrag([this._commandRef.current!], c.script, c.title, { target: this.document }, c.params, c.initialize, moveEv.clientX, moveEv.clientY));
                 return true;
             },
             emptyFunction,
diff --git a/src/client/views/collections/CollectionNoteTakingView.tsx b/src/client/views/collections/CollectionNoteTakingView.tsx
index 4ceeb631d..2ba6f5bf4 100644
--- a/src/client/views/collections/CollectionNoteTakingView.tsx
+++ b/src/client/views/collections/CollectionNoteTakingView.tsx
@@ -59,7 +59,7 @@ export class CollectionNoteTakingView extends CollectionSubView() {
     }
 
     @computed get chromeHidden() {
-        return BoolCast(this.layoutDoc.chromeHidden) || this._props.onBrowseClickScript?.() ? true : false;
+        return !!(BoolCast(this.layoutDoc.chromeHidden) || this._props.onBrowseClickScript?.());
     }
     // columnHeaders returns the list of SchemaHeaderFields currently being used by the layout doc to render the columns
     @computed get colHeaderData() {
@@ -68,7 +68,6 @@ export class CollectionNoteTakingView extends CollectionSubView() {
         if (needsUnsetCategory || colHeaderData === undefined || colHeaderData.length === 0) {
             setTimeout(() => {
                 const columnHeaders = Array.from(Cast(this.dataDoc[this.fieldKey + '_columnHeaders'], listSpec(SchemaHeaderField), null) ?? []);
-                const needsUnsetCategory = this.childDocs.some(d => !d[this.notetakingCategoryField] && !columnHeaders?.find(sh => sh.heading === 'unset'));
                 if (needsUnsetCategory || columnHeaders.length === 0) {
                     columnHeaders.push(new SchemaHeaderField('unset', undefined, undefined, 1));
                     this.resizeColumns(columnHeaders);
@@ -112,12 +111,12 @@ export class CollectionNoteTakingView extends CollectionSubView() {
     // to render the docs you see within an individual column.
     children = (docs: Doc[]) => {
         TraceMobx();
-        return docs.map((d, i) => {
+        return docs.map(d => {
             const height = () => this.getDocHeight(d);
             const width = () => this.getDocWidth(d);
             const style = { width: width(), marginTop: this.gridGap, height: height() };
             return (
-                <div className={`collectionNoteTakingView-columnDoc`} key={d[Id]} style={style}>
+                <div className="collectionNoteTakingView-columnDoc" key={d[Id]} style={style}>
                     {this.getDisplayDoc(d, width)}
                 </div>
             );
@@ -136,7 +135,7 @@ export class CollectionNoteTakingView extends CollectionSubView() {
         const sections = new Map<SchemaHeaderField, Doc[]>(columnHeaders.map(sh => [sh, []] as [SchemaHeaderField, []]));
         const rowCol = this.docsDraggedRowCol;
         // this will sort the docs into the correct columns (minus the ones you're currently dragging)
-        docs.map(d => {
+        docs.forEach(d => {
             const sectionValue = (d[this.notetakingCategoryField] as object) ?? `unset`;
             // look for if header exists already
             const existingHeader = columnHeaders.find(sh => sh.heading === sectionValue.toString());
@@ -154,7 +153,9 @@ export class CollectionNoteTakingView extends CollectionSubView() {
 
     removeDocDragHighlight = () => {
         setTimeout(
-            action(() => (this.docsDraggedRowCol.length = 0)),
+            action(() => {
+                this.docsDraggedRowCol.length = 0;
+            }),
             100
         );
     };
@@ -192,13 +193,11 @@ export class CollectionNoteTakingView extends CollectionSubView() {
         Object.keys(this._disposers).forEach(key => this._disposers[key]());
     }
 
-    moveDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (doc: Doc | Doc[], annotationKey?: string) => boolean, annotationKey?: string): boolean => {
-        return this._props.removeDocument?.(doc) && addDocument?.(doc) ? true : false;
-    };
+    moveDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (doc: Doc | Doc[], annotationKey?: string) => boolean) => !!(this._props.removeDocument?.(doc) && addDocument?.(doc));
 
     createRef = (ele: HTMLDivElement | null) => {
         this._masonryGridRef = ele;
-        this.createDashEventsTarget(ele!); //so the whole grid is the drop target?
+        this.createDashEventsTarget(ele!);
     };
 
     @computed get onChildClickHandler() {
@@ -218,14 +217,15 @@ export class CollectionNoteTakingView extends CollectionSubView() {
         Doc.BrushDoc(doc);
         const found = this._mainCont && Array.from(this._mainCont.getElementsByClassName('documentView-node')).find((node: any) => node.id === doc[Id]);
         if (found) {
-            const top = found.getBoundingClientRect().top;
+            const { top } = found.getBoundingClientRect();
             const localTop = this.ScreenToLocalBoxXf().transformPoint(0, top);
             if (Math.floor(localTop[1]) !== 0 && Math.ceil(this._props.PanelHeight()) < (this._mainCont?.scrollHeight || 0)) {
-                let focusSpeed = options.zoomTime ?? 500;
+                const focusSpeed = options.zoomTime ?? 500;
                 smoothScroll(focusSpeed, this._mainCont!, localTop[1] + this._mainCont!.scrollTop, options.easeFunc);
                 return focusSpeed;
             }
         }
+        return undefined;
     };
 
     styleProvider = (doc: Doc | undefined, props: Opt<FieldViewProps>, property: string) => {
@@ -240,6 +240,7 @@ export class CollectionNoteTakingView extends CollectionSubView() {
                     return this._props.childOpacity();
                 }
                 break;
+            default:
         }
         return this._props.styleProvider?.(doc, props, property);
     };
@@ -255,7 +256,9 @@ export class CollectionNoteTakingView extends CollectionSubView() {
         const noteTakingDocTransform = () => this.getDocTransform(doc, dref);
         return (
             <DocumentView
-                ref={r => (dref = r || undefined)}
+                ref={r => {
+                    dref = r || undefined;
+                }}
                 Document={doc}
                 TemplateDataDocument={dataDoc ?? (!Doc.AreProtosEqual(doc[DocData], doc) ? doc[DocData] : undefined)}
                 pointerEvents={this.blockPointerEventsWhenDragging}
@@ -267,8 +270,8 @@ export class CollectionNoteTakingView extends CollectionSubView() {
                 layout_fitWidth={this._props.childLayoutFitWidth}
                 isContentActive={emptyFunction}
                 onKey={this.onKeyDown}
-                //TODO: change this from a prop to a parameter passed into a function
-                dontHideOnDrag={true}
+                // TODO: change this from a prop to a parameter passed into a function
+                dontHideOnDrag
                 isDocumentActive={this.isContentActive}
                 LayoutTemplate={this._props.childLayoutTemplate}
                 LayoutTemplateString={this._props.childLayoutString}
@@ -302,7 +305,7 @@ export class CollectionNoteTakingView extends CollectionSubView() {
 
     // getDocTransform is used to get the coordinates of a document when we go from a view like freeform to columns
     getDocTransform(doc: Doc, dref?: DocumentView) {
-        const y = this._scroll; // required for document decorations to update when the text box container is scrolled
+        this._scroll; // required for document decorations to update when the text box container is scrolled
         const { translateX, translateY } = ClientUtils.GetScreenTransform(dref?.ContentDiv || undefined);
         // the document view may center its contents and if so, will prepend that onto the screenToLocalTansform.  so we have to subtract that off
         return new Transform(-translateX + (dref?.centeringX || 0), -translateY + (dref?.centeringY || 0), 1).scale(this.ScreenToLocalBoxXf().Scale);
@@ -347,7 +350,6 @@ export class CollectionNoteTakingView extends CollectionSubView() {
     // Adding example: column widths are [0.6, 0.4] --> user adds column at end --> column widths are [0.4, 0.267, 0.33]
     @action
     resizeColumns = (headers: SchemaHeaderField[]) => {
-        const n = headers.length;
         const curWidths = headers.reduce((sum, hdr) => sum + Math.abs(hdr.width), 0);
         const scaleFactor = 1 / curWidths;
         this.dataDoc[this.fieldKey + '_columnHeaders'] = new List<SchemaHeaderField>(
@@ -385,7 +387,11 @@ export class CollectionNoteTakingView extends CollectionSubView() {
             // we alter the pivot fields of the docs in case they are moved to a new column.
             const colIndex = this.getColumnFromXCoord(xCoord);
             const colHeader = colIndex === undefined ? 'unset' : StrCast(this.colHeaderData[colIndex].heading);
-            DragManager.docsBeingDragged.map(doc => doc[DocData]).forEach(d => (d[this.notetakingCategoryField] = colHeader));
+            DragManager.docsBeingDragged
+                .map(doc => doc[DocData])
+                .forEach(d => {
+                    d[this.notetakingCategoryField] = colHeader;
+                });
             // used to notify sections to re-render
             this.docsDraggedRowCol.length = 0;
             const columnFromCoord = this.getColumnFromXCoord(xCoord);
@@ -396,7 +402,7 @@ export class CollectionNoteTakingView extends CollectionSubView() {
     // getColumnFromXCoord returns the column index for a given x-coordinate (currently always the client's mouse coordinate).
     // This function is used to know which document a column SHOULD be in while it is being dragged.
     getColumnFromXCoord = (xCoord: number): number | undefined => {
-        let colIndex: number | undefined = undefined;
+        let colIndex: number | undefined;
         const numColumns = this.colHeaderData.length;
         const coords = [];
         let colStartXCoord = 0;
@@ -419,10 +425,10 @@ export class CollectionNoteTakingView extends CollectionSubView() {
         const docsMatchingHeader: Doc[] = [];
         const colIndex = this.getColumnFromXCoord(xCoord);
         const colHeader = colIndex === undefined ? 'unset' : StrCast(this.colHeaderData[colIndex].heading);
-        this.childDocs?.map(d => {
+        this.childDocs?.forEach(d => {
             if (d instanceof Promise) return;
             const sectionValue = (d[this.notetakingCategoryField] as object) ?? 'unset';
-            if (sectionValue.toString() == colHeader) {
+            if (sectionValue.toString() === colHeader) {
                 docsMatchingHeader.push(d);
             }
         });
@@ -438,6 +444,7 @@ export class CollectionNoteTakingView extends CollectionSubView() {
             FormattedTextBox.SetSelectOnLoad(newDoc);
             return this.addDocument?.(newDoc);
         }
+        return undefined;
     };
 
     // onInternalDrop is used when dragging and dropping a document within the view, such as dragging
@@ -575,6 +582,7 @@ export class CollectionNoteTakingView extends CollectionSubView() {
                 alert('You cannot use an existing column name. Please try a new column name');
                 return value;
             }
+            return undefined;
         });
         const columnHeaders = Array.from(Cast(this.dataDoc[this.fieldKey + '_columnHeaders'], listSpec(SchemaHeaderField), null));
         const newColWidth = 1 / (this.numGroupColumns + 1);
@@ -593,17 +601,33 @@ export class CollectionNoteTakingView extends CollectionSubView() {
             const subItems: ContextMenuProps[] = [];
             subItems.push({
                 description: `${this.layoutDoc._notetaking_columns_autoCreate ? 'Manually' : 'Automatically'} Create columns`,
-                event: () => (this.layoutDoc._notetaking_columns_autoCreate = !this.layoutDoc._notetaking_columns_autoCreate),
+                event: () => {
+                    this.layoutDoc._notetaking_columns_autoCreate = !this.layoutDoc._notetaking_columns_autoCreate;
+                },
                 icon: 'computer',
             });
             subItems.push({ description: 'Remove Empty Columns', event: this.removeEmptyColumns, icon: 'computer' });
             subItems.push({
                 description: `${this.layoutDoc._notetaking_columns_autoSize ? 'Variable Size' : 'Autosize'} Columns`,
-                event: () => (this.layoutDoc._notetaking_columns_autoSize = !this.layoutDoc._notetaking_columns_autoSize),
+                event: () => {
+                    this.layoutDoc._notetaking_columns_autoSize = !this.layoutDoc._notetaking_columns_autoSize;
+                },
                 icon: 'plus',
             });
-            subItems.push({ description: `${this.layoutDoc._layout_autoHeight ? 'Variable Height' : 'Auto Height'}`, event: () => (this.layoutDoc._layout_autoHeight = !this.layoutDoc._layout_autoHeight), icon: 'plus' });
-            subItems.push({ description: 'Clear All', event: () => (this.dataDoc.data = new List([])), icon: 'times' });
+            subItems.push({
+                description: `${this.layoutDoc._layout_autoHeight ? 'Variable Height' : 'Auto Height'}`,
+                event: () => {
+                    this.layoutDoc._layout_autoHeight = !this.layoutDoc._layout_autoHeight;
+                },
+                icon: 'plus',
+            });
+            subItems.push({
+                description: 'Clear All',
+                event: () => {
+                    this.dataDoc.data = new List([]);
+                },
+                icon: 'times',
+            });
             ContextMenu.Instance.addItem({ description: 'Options...', subitems: subItems, icon: 'eye' });
         }
     };
@@ -628,6 +652,7 @@ export class CollectionNoteTakingView extends CollectionSubView() {
         const sections = Array.from(this.Sections.entries());
         return sections.reduce((list, sec, i) => {
             list.push(this.sectionNoteTaking(sec[0], sec[1]));
+            // eslint-disable-next-line react/no-array-index-key
             i !== sections.length - 1 && list.push(<CollectionNoteTakingViewDivider key={`divider${i}`} isContentActive={this.isContentActive} index={i} setColumnStartXCoords={this.setColumnStartXCoords} xMargin={this.xMargin} />);
             return list;
         }, [] as JSX.Element[]);
@@ -661,14 +686,18 @@ export class CollectionNoteTakingView extends CollectionSubView() {
                     background: this.backgroundColor(),
                     pointerEvents: this.backgroundEvents,
                 }}
-                onScroll={action(e => (this._scroll = e.currentTarget.scrollTop))}
-                onPointerLeave={action(e => (this.docsDraggedRowCol.length = 0))}
+                onScroll={action(e => {
+                    this._scroll = e.currentTarget.scrollTop;
+                })}
+                onPointerLeave={action(() => {
+                    this.docsDraggedRowCol.length = 0;
+                })}
                 onPointerMove={e => e.buttons && this.onPointerMove(false, e.clientX, e.clientY)}
                 onDragOver={e => this.onPointerMove(true, e.clientX, e.clientY)}
                 onDrop={this.onExternalDrop.bind(this)}
                 onContextMenu={this.onContextMenu}
                 onWheel={e => this._props.isContentActive() && e.stopPropagation()}>
-                <>{this.renderedSections}</>
+                {this.renderedSections}
                 <div className="collectionNotetaking-pivotField" style={{ right: 0, top: 0, position: 'absolute' }}>
                     <FieldsDropdown
                         Document={this.Document}
diff --git a/src/client/views/collections/CollectionPileView.tsx b/src/client/views/collections/CollectionPileView.tsx
index e39f1c700..6dfc38e2e 100644
--- a/src/client/views/collections/CollectionPileView.tsx
+++ b/src/client/views/collections/CollectionPileView.tsx
@@ -1,3 +1,5 @@
+/* eslint-disable jsx-a11y/no-static-element-interactions */
+/* eslint-disable jsx-a11y/click-events-have-key-events */
 import { action, computed, IReactionDisposer, makeObservable } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
@@ -67,6 +69,7 @@ export class CollectionPileView extends CollectionSubView() {
         return (
             <div className="collectionPileView-innards" style={{ pointerEvents: this.contentEvents }}>
                 <CollectionFreeFormView
+                    // eslint-disable-next-line react/jsx-props-no-spreading
                     {...this._props} //
                     layoutEngine={this.layoutEngine}
                     addDocument={this.addPileDoc}
@@ -117,16 +120,16 @@ export class CollectionPileView extends CollectionSubView() {
         setupMoveUpEvents(
             this,
             e,
-            (e: PointerEvent, down: number[], delta: number[]) => {
-                if (this.layoutEngine() === 'pass' && this.childDocs.length && e.shiftKey) {
+            (moveEv: PointerEvent, down: number[], delta: number[]) => {
+                if (this.layoutEngine() === 'pass' && this.childDocs.length && moveEv.shiftKey) {
                     dist += Math.sqrt(delta[0] * delta[0] + delta[1] * delta[1]);
                     if (dist > 100) {
                         if (!this._undoBatch) {
                             this._undoBatch = UndoManager.StartBatch('layout pile');
                         }
                         const doc = this.childDocs[0];
-                        doc.x = e.clientX;
-                        doc.y = e.clientY;
+                        doc.x = moveEv.clientX;
+                        doc.y = moveEv.clientY;
                         this._props.addDocTab(doc, OpenWhere.inParentFromScreen) && (this._props.removeDocument?.(doc) || false);
                         dist = 0;
                     }
@@ -155,7 +158,7 @@ export class CollectionPileView extends CollectionSubView() {
 
     render() {
         return (
-            <div className={`collectionPileView`} onClick={this.onClick} onPointerDown={this.pointerDown} style={{ width: this._props.PanelWidth(), height: '100%' }}>
+            <div className="collectionPileView" onClick={this.onClick} onPointerDown={this.pointerDown} style={{ width: this._props.PanelWidth(), height: '100%' }}>
                 {this.contents}
             </div>
         );
diff --git a/src/client/views/collections/CollectionStackedTimeline.tsx b/src/client/views/collections/CollectionStackedTimeline.tsx
index 3f6638b25..7adf44a5c 100644
--- a/src/client/views/collections/CollectionStackedTimeline.tsx
+++ b/src/client/views/collections/CollectionStackedTimeline.tsx
@@ -1,3 +1,8 @@
+/* eslint-disable react/jsx-props-no-spreading */
+/* eslint-disable jsx-a11y/alt-text */
+/* eslint-disable no-use-before-define */
+/* eslint-disable jsx-a11y/no-static-element-interactions */
+/* eslint-disable jsx-a11y/click-events-have-key-events */
 import { action, computed, IReactionDisposer, makeObservable, observable, reaction, runInAction } from 'mobx';
 import { observer } from 'mobx-react';
 import { computedFn } from 'mobx-utils';
@@ -22,7 +27,7 @@ import { ScriptingGlobals } from '../../util/ScriptingGlobals';
 import { SnappingManager } from '../../util/SnappingManager';
 import { Transform } from '../../util/Transform';
 import { undoBatch, UndoManager } from '../../util/UndoManager';
-import { CollectionSubView } from '../collections/CollectionSubView';
+import { CollectionSubView } from './CollectionSubView';
 import { LightboxView } from '../LightboxView';
 import { AudioWaveform } from '../nodes/audio/AudioWaveform';
 import { DocumentView, OpenWhere } from '../nodes/DocumentView';
@@ -58,9 +63,14 @@ export enum TrimScope {
 
 @observer
 export class CollectionStackedTimeline extends CollectionSubView<CollectionStackedTimelineProps>() {
+    // eslint-disable-next-line no-use-before-define
     public static SelectingRegions: Set<CollectionStackedTimeline> = new Set();
     public static StopSelecting() {
-        this.SelectingRegions.forEach(action(region => (region._selectingRegion = false)));
+        this.SelectingRegions.forEach(
+            action(region => {
+                region._selectingRegion = false;
+            })
+        );
         this.SelectingRegions.clear();
     }
     constructor(props: any) {
@@ -131,7 +141,9 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack
     componentWillUnmount() {
         document.removeEventListener('keydown', this.keyEvents, true);
         if (this._selectingRegion) {
-            runInAction(() => (this._selectingRegion = false));
+            runInAction(() => {
+                this._selectingRegion = false;
+            });
             CollectionStackedTimeline.SelectingRegions.delete(this);
         }
     }
@@ -175,9 +187,9 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack
     anchorEnd = (anchor: Doc, val: any = null) => NumCast(anchor._timecodeToHide, NumCast(anchor[this._props.endTag], val) ?? null);
 
     // converts screen pixel offset to time
-    toTimeline = (screen_delta: number, width: number) => {
-        return Math.max(this.clipStart, Math.min(this.clipEnd, (screen_delta / width) * this.clipDuration + this.clipStart));
-    };
+    // prettier-ignore
+    toTimeline = (screenDelta: number, width: number) => //
+        Math.max(this.clipStart, Math.min(this.clipEnd, (screenDelta / width) * this.clipDuration + this.clipStart));
 
     @computed get rangeClick() {
         // prettier-ignore
@@ -235,6 +247,7 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack
                     this._props.setTime(Math.min(Math.max(this.clipStart, this.currentTime + jump), this.clipEnd));
                     e.stopPropagation();
                     break;
+                default:
             }
         }
     };
@@ -254,17 +267,15 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack
     @action
     onPointerDownTimeline = (e: React.PointerEvent): void => {
         const rect = this._timeline?.getBoundingClientRect();
-        const clientX = e.clientX;
-        const diff = rect ? clientX - rect?.x : null;
-        const shiftKey = e.shiftKey;
+        const { clientX, shiftKey } = e;
         if (rect && this._props.isContentActive()) {
             const wasPlaying = this._props.playing();
             if (wasPlaying) this._props.Pause();
-            var wasSelecting = this._markerEnd !== undefined;
+            let wasSelecting = this._markerEnd !== undefined;
             setupMoveUpEvents(
                 this,
                 e,
-                action(e => {
+                action(() => {
                     if (!wasSelecting) {
                         this._markerStart = this._markerEnd = this.toTimeline(clientX - rect.x, rect.width);
                         wasSelecting = true;
@@ -273,8 +284,8 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack
                     this._markerEnd = this.toTimeline(e.clientX - rect.x, rect.width);
                     return false;
                 }),
-                action((e, movement, isClick) => {
-                    this._markerEnd = this.toTimeline(e.clientX - rect.x, rect.width);
+                action((upEvent, movement, isClick) => {
+                    this._markerEnd = this.toTimeline(upEvent.clientX - rect.x, rect.width);
                     if (this._markerEnd < this._markerStart) {
                         const tmp = this._markerStart;
                         this._markerStart = this._markerEnd;
@@ -287,8 +298,8 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack
                     (!isClick || !wasSelecting) && (this._markerEnd = undefined);
                     this._timelineWrapper && (this._timelineWrapper.style.cursor = '');
                 }),
-                (e, doubleTap) => {
-                    if (e.button !== 2) {
+                (clickEv, doubleTap) => {
+                    if (clickEv.button !== 2) {
                         this._props.select(false);
                         !wasPlaying && doubleTap && this._props.Play();
                     }
@@ -310,7 +321,7 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack
     onHover = (e: React.MouseEvent): void => {
         e.stopPropagation();
         const rect = this._timeline?.getBoundingClientRect();
-        const clientX = e.clientX;
+        const { clientX } = e;
         if (rect) {
             this._hoverTime = this.toTimeline(clientX - rect.x, rect.width);
             if (this.thumbnails) {
@@ -328,14 +339,14 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack
         setupMoveUpEvents(
             this,
             e,
-            action((e, [], []) => {
+            action(moveEv => {
                 if (rect && this._props.isContentActive()) {
-                    this._trimStart = Math.min(Math.max(this.trimStart + (e.movementX / rect.width) * this.clipDuration, this.clipStart), this.trimEnd - this.minTrimLength);
+                    this._trimStart = Math.min(Math.max(this.trimStart + (moveEv.movementX / rect.width) * this.clipDuration, this.clipStart), this.trimEnd - this.minTrimLength);
                 }
                 return false;
             }),
             emptyFunction,
-            action((e, doubleTap) => {
+            action((clickEv, doubleTap) => {
                 doubleTap && (this._trimStart = this.clipStart);
             })
         );
@@ -348,14 +359,14 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack
         setupMoveUpEvents(
             this,
             e,
-            action((e, [], []) => {
+            action(moveEv => {
                 if (rect && this._props.isContentActive()) {
-                    this._trimEnd = Math.max(Math.min(this.trimEnd + (e.movementX / rect.width) * this.clipDuration, this.clipEnd), this.trimStart + this.minTrimLength);
+                    this._trimEnd = Math.max(Math.min(this.trimEnd + (moveEv.movementX / rect.width) * this.clipDuration, this.clipEnd), this.trimStart + this.minTrimLength);
                 }
                 return false;
             }),
             emptyFunction,
-            action((e, doubleTap) => {
+            action((clickEv, doubleTap) => {
                 doubleTap && (this._trimEnd = this.clipEnd);
             })
         );
@@ -384,7 +395,7 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack
 
     // handles dragging and dropping markers in timeline
     @action
-    internalDocDrop(e: Event, de: DragManager.DropEvent, docDragData: DragManager.DocumentDragData, xp: number) {
+    internalDocDrop(e: Event, de: DragManager.DropEvent, docDragData: DragManager.DocumentDragData) {
         if (super.onInternalDrop(e, de)) {
             // determine x coordinate of drop and assign it to the documents being dragged  --- see internalDocDrop of collectionFreeFormView.tsx for how it's done when dropping onto a 2D freeform view
             const localPt = this.ScreenToLocalBoxXf().transformPoint(de.x, de.y);
@@ -404,7 +415,7 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack
     }
 
     onInternalDrop = (e: Event, de: DragManager.DropEvent) => {
-        if (de.complete.docDragData?.droppedDocuments.length) return this.internalDocDrop(e, de, de.complete.docDragData, 0);
+        if (de.complete.docDragData?.droppedDocuments.length) return this.internalDocDrop(e, de, de.complete.docDragData);
         return false;
     };
 
@@ -442,7 +453,7 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack
     }
 
     @action
-    playOnClick = (anchorDoc: Doc, clientX: number) => {
+    playOnClick = (anchorDoc: Doc /* , clientX: number */) => {
         const seekTimeInSeconds = this.anchorStart(anchorDoc) - 0.05;
         const endTime = this.anchorEnd(anchorDoc);
         if (this.layoutDoc.autoPlayAnchors) {
@@ -451,17 +462,15 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack
                 this._props.playFrom(seekTimeInSeconds, endTime);
                 this.scrollToTime(seekTimeInSeconds);
             }
-        } else {
-            if (seekTimeInSeconds < NumCast(this.layoutDoc._layout_currentTimecode) && endTime > NumCast(this.layoutDoc._layout_currentTimecode)) {
-                if (!this.layoutDoc.autoPlayAnchors && this._props.playing()) {
-                    this._props.Pause();
-                } else {
-                    this._props.Play();
-                }
+        } else if (seekTimeInSeconds < NumCast(this.layoutDoc._layout_currentTimecode) && endTime > NumCast(this.layoutDoc._layout_currentTimecode)) {
+            if (!this.layoutDoc.autoPlayAnchors && this._props.playing()) {
+                this._props.Pause();
             } else {
-                this._props.playFrom(seekTimeInSeconds, endTime);
-                this.scrollToTime(seekTimeInSeconds);
+                this._props.Play();
             }
+        } else {
+            this._props.playFrom(seekTimeInSeconds, endTime);
+            this.scrollToTime(seekTimeInSeconds);
         }
         return { select: true };
     };
@@ -480,19 +489,17 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack
                 const rect = this._timeline?.getBoundingClientRect();
                 rect && this._props.setTime(this.toTimeline(clientX - rect.x, rect.width));
             }
+        } else if (this.layoutDoc.autoPlayAnchors) {
+            this._props.playFrom(seekTimeInSeconds, endTime);
         } else {
-            if (this.layoutDoc.autoPlayAnchors) {
-                this._props.playFrom(seekTimeInSeconds, endTime);
-            } else {
-                this._props.setTime(seekTimeInSeconds);
-            }
+            this._props.setTime(seekTimeInSeconds);
         }
         return { select: true };
     };
 
     // makes sure no anchors overlaps each other by setting the correct position and width
     getLevel = (m: Doc, placed: { anchorStartTime: number; anchorEndTime: number; level: number }[]) => {
-        const timelineContentWidth = this.timelineContentWidth;
+        const { timelineContentWidth } = this;
         const x1 = this.anchorStart(m);
         const x2 = this.anchorEnd(m, x1 + (10 / timelineContentWidth) * this.clipDuration);
         let max = 0;
@@ -504,6 +511,7 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack
                     max = Math.max(max, p.level);
                     return p.level;
                 }
+                return undefined;
             })
         );
         let level = max + 1;
@@ -565,10 +573,14 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack
                     onWheel={e => this.isContentActive() && e.stopPropagation()}
                     onScroll={this.setScroll}
                     onMouseMove={e => this.isContentActive() && this.onHover(e)}
-                    ref={wrapper => (this._timelineWrapper = wrapper)}>
+                    ref={wrapper => {
+                        this._timelineWrapper = wrapper;
+                    }}>
                     <div
                         className="collectionStackedTimeline"
-                        ref={(timeline: HTMLDivElement | null) => (this._timeline = timeline)}
+                        ref={(timeline: HTMLDivElement | null) => {
+                            this._timeline = timeline;
+                        }}
                         onClick={e => this.isContentActive() && StopEvent(e)}
                         onPointerDown={e => this.isContentActive() && this.onPointerDownTimeline(e)}
                         style={{ width: this.timelineContentWidth }}>
@@ -583,7 +595,7 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack
                             const height = this._props.PanelHeight() / maxLevel;
                             return this.Document.hideAnchors ? null : (
                                 <div
-                                    className={'collectionStackedTimeline-marker-timeline'}
+                                    className="collectionStackedTimeline-marker-timeline"
                                     key={d.anchor[Id]}
                                     style={{
                                         left,
@@ -593,6 +605,7 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack
                                         pointerEvents: 'none',
                                     }}>
                                     <StackedTimelineAnchor
+                                        // eslint-disable-next-line react/jsx-props-no-spreading
                                         {...this._props}
                                         mark={d.anchor}
                                         containerViewPath={this._props.containerViewPath}
@@ -647,7 +660,7 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack
 
                         {this.IsTrimming !== TrimScope.None && (
                             <>
-                                <div className="collectionStackedTimeline-trim-shade" style={{ width: `${((this.trimStart - this.clipStart) / this.clipDuration) * 100}%` }}></div>
+                                <div className="collectionStackedTimeline-trim-shade" style={{ width: `${((this.trimStart - this.clipStart) / this.clipDuration) * 100}%` }} />
 
                                 <div
                                     className="collectionStackedTimeline-trim-controls"
@@ -655,8 +668,8 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack
                                         left: `${((this.trimStart - this.clipStart) / this.clipDuration) * 100}%`,
                                         width: `${((this.trimEnd - this.trimStart) / this.clipDuration) * 100}%`,
                                     }}>
-                                    <div className="collectionStackedTimeline-trim-handle" onPointerDown={this.trimLeft}></div>
-                                    <div className="collectionStackedTimeline-trim-handle" onPointerDown={this.trimRight}></div>
+                                    <div className="collectionStackedTimeline-trim-handle" onPointerDown={this.trimLeft} />
+                                    <div className="collectionStackedTimeline-trim-handle" onPointerDown={this.trimRight} />
                                 </div>
 
                                 <div
@@ -664,7 +677,8 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack
                                     style={{
                                         left: `${((this.trimEnd - this.clipStart) / this.clipDuration) * 100}%`,
                                         width: `${((this.clipEnd - this.trimEnd) / this.clipDuration) * 100}%`,
-                                    }}></div>
+                                    }}
+                                />
                             </>
                         )}
                     </div>
@@ -686,7 +700,7 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack
 interface StackedTimelineAnchorProps {
     mark: Doc;
     whenChildContentsActiveChanged: (isActive: boolean) => void;
-    addDocTab: (doc: Doc, where: OpenWhere) => boolean;
+    addDocTab: (doc: Doc | Doc[], where: OpenWhere) => boolean;
     rangeClickScript: () => ScriptField;
     rangePlayScript: () => ScriptField;
     left: number;
@@ -736,13 +750,13 @@ class StackedTimelineAnchor extends ObservableReactComponent<StackedTimelineAnch
         this._disposer = reaction(
             () => this._props.currentTimecode(),
             time => {
-                const dictationDoc = Cast(this._props.layoutDoc.data_dictation, Doc, null);
-                const isDictation = dictationDoc && LinkManager.Links(this._props.mark).some(link => Cast(link.link_anchor_1, Doc, null)?.annotationOn === dictationDoc);
+                // const dictationDoc = Cast(this._props.layoutDoc.data_dictation, Doc, null);
+                // const isDictation = dictationDoc && LinkManager.Links(this._props.mark).some(link => Cast(link.link_anchor_1, Doc, null)?.annotationOn === dictationDoc);
                 if (
                     !LightboxView.LightboxDoc &&
                     // bcz: when should links be followed?  we don't want to move away from the video to follow a link but we can open it in a sidebar/etc.  But we don't know that upfront.
                     // for now, we won't follow any links when the lightbox is oepn to avoid "losing" the video.
-                    /*(isDictation || !Doc.AreProtosEqual(LightboxView.LightboxDoc, this._props.layoutDoc))*/
+                    /* (isDictation || !Doc.AreProtosEqual(LightboxView.LightboxDoc, this._props.layoutDoc)) */
                     !this._props.layoutDoc.dontAutoFollowLinks &&
                     LinkManager.Links(this._props.mark).length &&
                     time > NumCast(this._props.mark[this._props.startTag]) &&
@@ -764,34 +778,33 @@ class StackedTimelineAnchor extends ObservableReactComponent<StackedTimelineAnch
     // starting the drag event for anchor resizing
     @action
     onAnchorDown = (e: React.PointerEvent, anchor: Doc, left: boolean): void => {
-        //this._props._timeline?.setPointerCapture(e.pointerId);
-        const newTime = (e: PointerEvent) => {
-            const rect = (e.target as any).getBoundingClientRect();
-            return this._props.toTimeline(e.clientX - rect.x, rect.width);
+        const newTime = (timeDownEv: PointerEvent) => {
+            const rect = (timeDownEv.target as any).getBoundingClientRect();
+            return this._props.toTimeline(timeDownEv.clientX - rect.x, rect.width);
         };
-        const changeAnchor = (anchor: Doc, left: boolean, time: number | undefined) => {
+        const changeAnchor = (time: number | undefined) => {
             const timelineOnly = Cast(anchor[this._props.startTag], 'number', null) !== undefined;
             if (timelineOnly) {
-                if (!left && time !== undefined && time <= NumCast(anchor[this._props.startTag])) time = undefined;
-                Doc.SetInPlace(anchor, left ? this._props.startTag : this._props.endTag, time, true);
-                if (!left) Doc.SetInPlace(anchor, 'layout_borderRounding', time !== undefined ? undefined : '100%', true);
+                const timeMod = !left && time !== undefined && time <= NumCast(anchor[this._props.startTag]) ? undefined : time;
+                Doc.SetInPlace(anchor, left ? this._props.startTag : this._props.endTag, timeMod, true);
+                if (!left) Doc.SetInPlace(anchor, 'layout_borderRounding', timeMod !== undefined ? undefined : '100%', true);
             } else {
                 anchor[left ? '_timecodeToShow' : '_timecodeToHide'] = time;
             }
             return false;
         };
         this.noEvents = true;
-        var undo: UndoManager.Batch | undefined;
+        let undo: UndoManager.Batch | undefined;
         setupMoveUpEvents(
             this,
             e,
-            e => {
+            moveEv => {
                 if (!undo) undo = UndoManager.StartBatch('drag anchor');
-                this._props.setTime(newTime(e));
-                return changeAnchor(anchor, left, newTime(e));
+                this._props.setTime(newTime(moveEv));
+                return changeAnchor(newTime(moveEv));
             },
-            action(e => {
-                this._props.setTime(newTime(e));
+            action(upEv => {
+                this._props.setTime(newTime(upEv));
                 undo?.end();
                 this.noEvents = false;
             }),
@@ -829,7 +842,9 @@ class StackedTimelineAnchor extends ObservableReactComponent<StackedTimelineAnch
                     {...this._props}
                     NativeWidth={returnZero}
                     NativeHeight={returnZero}
-                    ref={action((r: DocumentView | null) => (anchor.view = r))}
+                    ref={action((r: DocumentView | null) => {
+                        anchor.view = r;
+                    })}
                     Document={mark}
                     TemplateDataDocument={undefined}
                     containerViewPath={this._props.containerViewPath}
@@ -852,7 +867,7 @@ class StackedTimelineAnchor extends ObservableReactComponent<StackedTimelineAnch
                     onClickScript={script}
                     onDoubleClickScript={this._props.layoutDoc.autoPlayAnchors ? undefined : doublescript}
                     ignoreAutoHeight={false}
-                    hideResizeHandles={true}
+                    hideResizeHandles
                     contextMenuItems={this.contextMenuItems}
                 />
             ),
@@ -878,12 +893,15 @@ class StackedTimelineAnchor extends ObservableReactComponent<StackedTimelineAnch
         );
     }
 }
+// eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function formatToTime(time: number): any {
     return formatTime(time);
 });
+// eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function min(num1: number, num2: number): number {
     return Math.min(num1, num2);
 });
+// eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function max(num1: number, num2: number): number {
     return Math.max(num1, num2);
 });
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index 7d93f4074..f3dedaedf 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -3,11 +3,12 @@ import * as React from 'react';
 import * as rp from 'request-promise';
 import { ClientUtils, returnFalse } from '../../../ClientUtils';
 import CursorField from '../../../fields/CursorField';
-import { Doc, DocListCast, Field, FieldType, Opt, StrListCast } from '../../../fields/Doc';
+import { Doc, DocListCast, Opt, StrListCast } from '../../../fields/Doc';
 import { AclPrivate } from '../../../fields/DocSymbols';
 import { Id } from '../../../fields/FieldSymbols';
 import { List } from '../../../fields/List';
 import { listSpec } from '../../../fields/Schema';
+import { ScriptField } from '../../../fields/ScriptField';
 import { BoolCast, Cast, ScriptCast, StrCast } from '../../../fields/Types';
 import { WebField } from '../../../fields/URLField';
 import { GetEffectiveAcl, TraceMobx } from '../../../fields/util';
@@ -61,8 +62,8 @@ export interface SubCollectionViewProps extends CollectionViewProps {
     isAnyChildContentActive: () => boolean;
 }
 
-export function CollectionSubView<X>(moreProps?: X) {
-    class CollectionSubView extends ViewBoxBaseComponent<X & SubCollectionViewProps>() {
+export function CollectionSubView<X>() {
+    class CollectionSubViewInternal extends ViewBoxBaseComponent<X & SubCollectionViewProps>() {
         private dropDisposer?: DragManager.DragDropDisposer;
         private gestureDisposer?: GestureUtils.GestureEventDisposer;
         protected _mainCont?: HTMLDivElement;
@@ -167,23 +168,24 @@ export function CollectionSubView<X>(moreProps?: X) {
                 if (notFiltered) {
                     notFiltered = (!searchDocs.length || searchDocs.includes(d)) && DocUtils.FilterDocs([d], childDocFilters, childFiltersByRanges, this.Document).length > 0;
                     const fieldKey = Doc.LayoutFieldKey(d);
-                    const annos = !Field.toString(Doc.LayoutField(d) as FieldType).includes(CollectionView.name);
-                    const data = d[annos ? fieldKey + '_annotations' : fieldKey];
-                    const side = annos && d[fieldKey + '_sidebar'];
-                    if (data !== undefined || side !== undefined) {
-                        let subDocs = [...DocListCast(data), ...DocListCast(side)];
+                    const isAnnotatableDoc = d[fieldKey] instanceof List && !(d[fieldKey] as List<Doc>)?.some(ele => !(ele instanceof Doc));
+                    const docChildDocs = d[isAnnotatableDoc ? fieldKey + '_annotations' : fieldKey];
+                    const sidebarDocs = isAnnotatableDoc && d[fieldKey + '_sidebar'];
+                    if (docChildDocs !== undefined || sidebarDocs !== undefined) {
+                        let subDocs = [...DocListCast(docChildDocs), ...DocListCast(sidebarDocs)];
                         if (subDocs.length > 0) {
                             let newarray: Doc[] = [];
                             notFiltered = notFiltered || (!searchDocs.length && DocUtils.FilterDocs(subDocs, childDocFilters, childFiltersByRanges, d).length);
                             while (subDocs.length > 0 && !notFiltered) {
                                 newarray = [];
+                                // eslint-disable-next-line no-loop-func
                                 subDocs.forEach(t => {
-                                    const fieldKey = Doc.LayoutFieldKey(t);
-                                    const annos = !Field.toString(Doc.LayoutField(t) as FieldType).includes(CollectionView.name);
+                                    const docFieldKey = Doc.LayoutFieldKey(t);
+                                    const isSubDocAnnotatable = t[docFieldKey] instanceof List && !(t[docFieldKey] as List<Doc>)?.some(ele => !(ele instanceof Doc));
                                     notFiltered =
                                         notFiltered || ((!searchDocs.length || searchDocs.includes(t)) && ((!childDocFilters.length && !childFiltersByRanges.length) || DocUtils.FilterDocs([t], childDocFilters, childFiltersByRanges, d).length));
-                                    DocListCast(t[annos ? fieldKey + '_annotations' : fieldKey]).forEach(newdoc => newarray.push(newdoc));
-                                    annos && DocListCast(t[fieldKey + '_sidebar']).forEach(newdoc => newarray.push(newdoc));
+                                    DocListCast(t[isSubDocAnnotatable ? docFieldKey + '_annotations' : docFieldKey]).forEach(newdoc => newarray.push(newdoc));
+                                    isSubDocAnnotatable && DocListCast(t[docFieldKey + '_sidebar']).forEach(newdoc => newarray.push(newdoc));
                                 });
                                 subDocs = newarray;
                             }
@@ -227,6 +229,7 @@ export function CollectionSubView<X>(moreProps?: X) {
         }
 
         @undoBatch
+        // eslint-disable-next-line @typescript-eslint/no-unused-vars
         protected onGesture(e: Event, ge: GestureUtils.GestureEvent) {}
 
         protected onInternalPreDrop(e: Event, de: DragManager.DropEvent, targetDropAction: dropActionType) {
@@ -245,7 +248,7 @@ export function CollectionSubView<X>(moreProps?: X) {
 
         addDocument = (doc: Doc | Doc[], annotationKey?: string) => this._props.addDocument?.(doc, annotationKey) || false;
         removeDocument = (doc: Doc | Doc[], annotationKey?: string) => this._props.removeDocument?.(doc, annotationKey) || false;
-        moveDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (doc: Doc | Doc[], annotationKey?: string) => boolean, annotationKey?: string) => this._props.moveDocument?.(doc, targetCollection, addDocument) || false;
+        moveDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (doc: Doc | Doc[], annotationKey?: string) => boolean) => this._props.moveDocument?.(doc, targetCollection, addDocument) || false;
 
         protected onInternalDrop(e: Event, de: DragManager.DropEvent): boolean {
             const { docDragData } = de.complete;
@@ -531,5 +534,5 @@ export function CollectionSubView<X>(moreProps?: X) {
         };
     }
 
-    return CollectionSubView;
+    return CollectionSubViewInternal;
 }
diff --git a/src/client/views/collections/CollectionTimeView.tsx b/src/client/views/collections/CollectionTimeView.tsx
index 7f234ffe9..e1d2e3c40 100644
--- a/src/client/views/collections/CollectionTimeView.tsx
+++ b/src/client/views/collections/CollectionTimeView.tsx
@@ -1,3 +1,5 @@
+/* eslint-disable jsx-a11y/no-static-element-interactions */
+/* eslint-disable jsx-a11y/click-events-have-key-events */
 import { action, computed, makeObservable, observable, runInAction } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
@@ -16,7 +18,6 @@ import { ContextMenu } from '../ContextMenu';
 import { ContextMenuProps } from '../ContextMenuItem';
 import { FieldsDropdown } from '../FieldsDropdown';
 import { DocumentView } from '../nodes/DocumentView';
-import { FocusViewOptions } from '../nodes/FieldView';
 import { PresBox } from '../nodes/trails';
 import { CollectionSubView } from './CollectionSubView';
 import './CollectionTimeView.scss';
@@ -68,7 +69,7 @@ export class CollectionTimeView extends CollectionSubView() {
     };
 
     @action
-    scrollPreview = (docView: DocumentView, anchor: Doc, focusSpeed: number, options: FocusViewOptions) => {
+    scrollPreview = (docView: DocumentView, anchor: Doc /* , focusSpeed: number, options: FocusViewOptions */) => {
         // if in preview, then override document's fields with view spec
         this._focusFilters = StrListCast(anchor.config_docFilters);
         this._focusRangeFilters = StrListCast(anchor.config_docRangeFilters);
@@ -77,13 +78,15 @@ export class CollectionTimeView extends CollectionSubView() {
     };
 
     layoutEngine = () => this._layoutEngine;
-    toggleVisibility = action(() => (this._collapsed = !this._collapsed));
+    toggleVisibility = action(() => {
+        this._collapsed = !this._collapsed;
+    });
 
     onMinDown = (e: React.PointerEvent) => {
         setupMoveUpEvents(
             this,
             e,
-            action((e: PointerEvent, down: number[], delta: number[]) => {
+            action((moveEv: PointerEvent, down: number[], delta: number[]) => {
                 const minReq = NumCast(this.Document[this._props.fieldKey + '-timelineMinReq'], NumCast(this.Document[this._props.fieldKey + '-timelineMin'], 0));
                 const maxReq = NumCast(this.Document[this._props.fieldKey + '-timelineMaxReq'], NumCast(this.Document[this._props.fieldKey + '-timelineMax'], 10));
                 this.Document[this._props.fieldKey + '-timelineMinReq'] = minReq + ((maxReq - minReq) * delta[0]) / this._props.PanelWidth();
@@ -99,7 +102,7 @@ export class CollectionTimeView extends CollectionSubView() {
         setupMoveUpEvents(
             this,
             e,
-            action((e: PointerEvent, down: number[], delta: number[]) => {
+            action((moveEv, down: number[], delta: number[]) => {
                 const minReq = NumCast(this.Document[this._props.fieldKey + '-timelineMinReq'], NumCast(this.Document[this._props.fieldKey + '-timelineMin'], 0));
                 const maxReq = NumCast(this.Document[this._props.fieldKey + '-timelineMaxReq'], NumCast(this.Document[this._props.fieldKey + '-timelineMax'], 10));
                 this.Document[this._props.fieldKey + '-timelineMaxReq'] = maxReq + ((maxReq - minReq) * delta[0]) / this._props.PanelWidth();
@@ -114,7 +117,7 @@ export class CollectionTimeView extends CollectionSubView() {
         setupMoveUpEvents(
             this,
             e,
-            action((e: PointerEvent, down: number[], delta: number[]) => {
+            action((moveEv: PointerEvent, down: number[], delta: number[]) => {
                 const minReq = NumCast(this.Document[this._props.fieldKey + '-timelineMinReq'], NumCast(this.Document[this._props.fieldKey + '-timelineMin'], 0));
                 const maxReq = NumCast(this.Document[this._props.fieldKey + '-timelineMaxReq'], NumCast(this.Document[this._props.fieldKey + '-timelineMax'], 10));
                 this.Document[this._props.fieldKey + '-timelineMinReq'] = minReq - ((maxReq - minReq) * delta[0]) / this._props.PanelWidth();
@@ -134,7 +137,7 @@ export class CollectionTimeView extends CollectionSubView() {
     };
 
     @action
-    contentsDown = (e: React.MouseEvent) => {
+    contentsDown = () => {
         const prevFilterIndex = NumCast(this.layoutDoc._prevFilterIndex);
         if (prevFilterIndex > 0) {
             this.goTo(prevFilterIndex - 1);
@@ -147,6 +150,7 @@ export class CollectionTimeView extends CollectionSubView() {
         return (
             <div className="collectionTimeView-innards" key="timeline" style={{ pointerEvents: this._props.isContentActive() ? undefined : 'none' }} onClick={this.contentsDown}>
                 <CollectionFreeFormView
+                    // eslint-disable-next-line react/jsx-props-no-spreading
                     {...this._props}
                     engineProps={{ pivotField: this.pivotField, childFilters: this.childDocFilters, childFiltersByRanges: this.childDocRangeFilters }}
                     fitContentsToBox={returnTrue}
@@ -162,7 +166,7 @@ export class CollectionTimeView extends CollectionSubView() {
         const fieldKey = Doc.LayoutFieldKey(doc);
         doc[fieldKey + '-timelineCur'] = ComputedField.MakeFunction("(activePresentationItem()[this._pivotField || 'year'] || 0)");
     }
-    specificMenu = (e: React.MouseEvent) => {
+    specificMenu = () => {
         const layoutItems: ContextMenuProps[] = [];
         const doc = this.layoutDoc;
 
@@ -194,7 +198,7 @@ export class CollectionTimeView extends CollectionSubView() {
 
     render() {
         let nonNumbers = 0;
-        this.childDocs.map(doc => {
+        this.childDocs.forEach(doc => {
             const num = NumCast(doc[this.pivotField], Number(StrCast(doc[this.pivotField])));
             if (isNaN(num)) {
                 nonNumbers++;
@@ -226,13 +230,20 @@ export class CollectionTimeView extends CollectionSubView() {
                     </>
                 )}
                 <div style={{ right: 0, top: 0, position: 'absolute' }}>
-                    <FieldsDropdown Document={this.Document} selectFunc={fieldKey => (this.layoutDoc._pivotField = fieldKey)} placeholder={StrCast(this.layoutDoc._pivotField)} />
+                    <FieldsDropdown
+                        Document={this.Document}
+                        selectFunc={fieldKey => {
+                            this.layoutDoc._pivotField = fieldKey;
+                        }}
+                        placeholder={StrCast(this.layoutDoc._pivotField)}
+                    />
                 </div>
             </div>
         );
     }
 }
 
+// eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function pivotColumnClick(pivotDoc: Doc, bounds: ViewDefBounds) {
     const pivotField = StrCast(pivotDoc._pivotField, 'author');
     let prevFilterIndex = NumCast(pivotDoc._prevFilterIndex);
@@ -249,6 +260,7 @@ ScriptingGlobals.add(function pivotColumnClick(pivotDoc: Doc, bounds: ViewDefBou
             const pivotView = DocumentManager.Instance.getDocumentView(pivotDoc);
             if (pivotDoc && pivotView?.ComponentView instanceof CollectionTimeView && filterVals.length === 1) {
                 if (pivotView?.ComponentView.childDocs.length && pivotView.ComponentView.childDocs[0][filterVals[0]]) {
+                    // eslint-disable-next-line prefer-destructuring
                     pivotDoc._pivotField = filterVals[0];
                 }
             }
diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx
index 32473f20b..538a6fd5e 100644
--- a/src/client/views/collections/CollectionTreeView.tsx
+++ b/src/client/views/collections/CollectionTreeView.tsx
@@ -1,3 +1,5 @@
+/* eslint-disable jsx-a11y/no-static-element-interactions */
+/* eslint-disable jsx-a11y/click-events-have-key-events */
 import { action, computed, IReactionDisposer, makeObservable, observable, reaction } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
@@ -23,13 +25,13 @@ import { ContextMenu } from '../ContextMenu';
 import { ContextMenuProps } from '../ContextMenuItem';
 import { EditableView } from '../EditableView';
 import { DocumentView } from '../nodes/DocumentView';
-import { FieldViewProps } from '../nodes/FieldView';
 import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox';
 import { StyleProp } from '../StyleProvider';
 import { CollectionFreeFormView } from './collectionFreeForm';
 import { CollectionSubView } from './CollectionSubView';
 import './CollectionTreeView.scss';
 import { TreeView } from './TreeView';
+
 const _global = (window /* browser */ || global) /* node */ as any;
 
 export type collectionTreeViewProps = {
@@ -94,8 +96,10 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree
 
     // these should stay in synch with counterparts in DocComponent.ts ViewBoxAnnotatableComponent
     @observable _isAnyChildContentActive = false;
-    whenChildContentsActiveChanged = action((isActive: boolean) => this._props.whenChildContentsActiveChanged((this._isAnyChildContentActive = isActive)));
-    isContentActive = (outsideReaction?: boolean) => (this._isAnyChildContentActive ? true : this._props.isContentActive() ? true : false);
+    whenChildContentsActiveChanged = action((isActive: boolean) => {
+        this._props.whenChildContentsActiveChanged((this._isAnyChildContentActive = isActive));
+    });
+    isContentActive = () => (this._isAnyChildContentActive ? true : !!this._props.isContentActive());
 
     componentWillUnmount() {
         this._isDisposing = true;
@@ -105,7 +109,7 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree
     }
 
     componentDidMount() {
-        //this._props.setContentView?.(this);
+        // this._props.setContentView?.(this);
         this._disposers.autoheight = reaction(
             () => this.layoutDoc.layout_autoHeight,
             auto => auto && this.computeHeight(),
@@ -128,20 +132,19 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree
     observeHeight = (ref: any) => {
         if (ref) {
             this.refList.add(ref);
-            this.observer = new _global.ResizeObserver(
-                action((entries: any) => {
-                    if (this.layoutDoc.layout_autoHeight && ref && this.refList.size && !SnappingManager.IsDragging) {
-                        this.computeHeight();
-                    }
-                })
-            );
+            this.observer = new _global.ResizeObserver(() => {
+                if (this.layoutDoc.layout_autoHeight && ref && this.refList.size && !SnappingManager.IsDragging) {
+                    this.computeHeight();
+                }
+            });
             this.layoutDoc.layout_autoHeight && this.computeHeight();
             this.observer.observe(ref);
         }
     };
     protected createTreeDropTarget = (ele: HTMLDivElement) => {
         this._treedropDisposer?.();
-        if ((this._mainEle = ele)) this._treedropDisposer = DragManager.MakeDropTarget(ele, this.onInternalDrop.bind(this), this.Document, this.onInternalPreDrop.bind(this));
+        this._mainEle = ele;
+        if (ele) this._treedropDisposer = DragManager.MakeDropTarget(ele, this.onInternalDrop.bind(this), this.Document, this.onInternalPreDrop.bind(this));
     };
 
     protected onInternalDrop(e: Event, de: DragManager.DropEvent) {
@@ -169,7 +172,7 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree
         }
     };
 
-    dragConfig = (dragData: DragManager.DocumentDragData) => (dragData.treeViewDoc = this.Document);
+    dragConfig = (dragData: DragManager.DocumentDragData) => { dragData.treeViewDoc = this.Document; }; // prettier-ignore
 
     screenToLocalTransform = () => this.ScreenToLocalBoxXf().translate(0, -this._headerHeight);
 
@@ -198,10 +201,10 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree
 
     @action
     addDoc = (docs: Doc | Doc[], relativeTo: Opt<Doc>, before?: boolean): boolean => {
-        const doclist = docs instanceof Doc ? [docs] : docs;
-        const addDocRelativeTo = (doc: Doc | Doc[]) => doclist.reduce((flg, doc) => flg && Doc.AddDocToList(this.Document[DocData], this._props.fieldKey, doc, relativeTo, before), true);
+        const addDocRelativeTo = (adocs: Doc | Doc[]) => (adocs as Doc[]).reduce((flg, doc) => flg && Doc.AddDocToList(this.Document[DocData], this._props.fieldKey, doc, relativeTo, before), true);
         if (this.Document.resolvedDataDoc instanceof Promise) return false;
-        const res = relativeTo === undefined ? this._props.addDocument?.(docs) || false : addDocRelativeTo(docs);
+        const doclist = docs instanceof Doc ? [docs] : docs;
+        const res = relativeTo === undefined ? this._props.addDocument?.(doclist) || false : addDocRelativeTo(doclist);
         res &&
             doclist.forEach(doc => {
                 Doc.SetContainer(doc, this.Document);
@@ -209,7 +212,7 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree
             });
         return res;
     };
-    onContextMenu = (e: React.MouseEvent): void => {
+    onContextMenu = (): void => {
         // need to test if propagation has stopped because GoldenLayout forces a parallel react hierarchy to be created for its top-level layout
         const layoutItems: ContextMenuProps[] = [];
         const menuDoc = ScriptCast(Cast(this.layoutDoc.layout_headerButton, Doc, null)?.onClick).script.originalScript === CollectionTreeView.AddTreeFunc;
@@ -217,11 +220,11 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree
         if (!Doc.noviceMode) {
             layoutItems.push({
                 description: 'Make tree state ' + (this.Document.treeView_OpenIsTransient ? 'persistent' : 'transient'),
-                event: () => (this.Document.treeView_OpenIsTransient = !this.Document.treeView_OpenIsTransient),
+                event: () => { this.Document.treeView_OpenIsTransient = !this.Document.treeView_OpenIsTransient; }, // prettier-ignore
                 icon: 'paint-brush',
             });
-            layoutItems.push({ description: (this.Document.treeView_HideHeaderFields ? 'Show' : 'Hide') + ' Header Fields', event: () => (this.Document.treeView_HideHeaderFields = !this.Document.treeView_HideHeaderFields), icon: 'paint-brush' });
-            layoutItems.push({ description: (this.Document.treeView_HideTitle ? 'Show' : 'Hide') + ' Title', event: () => (this.Document.treeView_HideTitle = !this.Document.treeView_HideTitle), icon: 'paint-brush' });
+            layoutItems.push({ description: (this.Document.treeView_HideHeaderFields ? 'Show' : 'Hide') + ' Header Fields', event: () => { this.Document.treeView_HideHeaderFields = !this.Document.treeView_HideHeaderFields; }, icon: 'paint-brush' }); // prettier-ignore
+            layoutItems.push({ description: (this.Document.treeView_HideTitle ? 'Show' : 'Hide') + ' Title', event: () => { this.Document.treeView_HideTitle = !this.Document.treeView_HideTitle; }, icon: 'paint-brush' }); // prettier-ignore
         }
         ContextMenu.Instance.addItem({ description: 'Options...', subitems: layoutItems, icon: 'eye' });
         if (!Doc.noviceMode) {
@@ -240,9 +243,9 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree
         return (
             <EditableView
                 contents={this.dataDoc.title}
-                display={'block'}
+                display="block"
                 maxHeight={72}
-                height={'auto'}
+                height="auto"
                 GetValue={() => StrCast(this.dataDoc.title)}
                 SetValue={undoBatch((value: string, shift: boolean, enter: boolean) => {
                     if (enter && this.Document.treeView_Type === TreeViewType.outline) this.makeTextCollection(this.treeChildren);
@@ -253,22 +256,24 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree
         );
     }
 
-    onKey = (e: React.KeyboardEvent, fieldProps: FieldViewProps) => {
+    onKey = (e: React.KeyboardEvent /* , fieldProps: FieldViewProps */) => {
         if (this.outlineMode && e.key === 'Enter') {
             e.stopPropagation();
             this.makeTextCollection(this.treeChildren);
             return true;
         }
+        return undefined;
     };
     get documentTitle() {
         return (
             <FormattedTextBox
+                // eslint-disable-next-line react/jsx-props-no-spreading
                 {...this._props}
                 fieldKey="text"
                 renderDepth={this._props.renderDepth + 1}
                 isContentActive={this.isContentActive}
                 isDocumentActive={this.isContentActive}
-                forceAutoHeight={true} // needed to make the title resize even if the rest of the tree view is not layout_autoHeight
+                forceAutoHeight // needed to make the title resize even if the rest of the tree view is not layout_autoHeight
                 PanelWidth={this.documentTitleWidth}
                 PanelHeight={this.documentTitleHeight}
                 NativeDimScaling={returnOne}
@@ -296,7 +301,12 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree
         const dragAction = StrCast(this.Document.childDragAction) as any as dropActionType;
         const addDoc = (doc: Doc | Doc[], relativeTo?: Doc, before?: boolean) => this.addDoc(doc, relativeTo, before);
         const moveDoc = (d: Doc | Doc[], target: Doc | undefined, addDoc: (doc: Doc | Doc[]) => boolean) => this._props.moveDocument?.(d, target, addDoc) || false;
-        if (this._renderCount < this.treeChildren.length) setTimeout(action(() => (this._renderCount = Math.min(this.treeChildren.length, this._renderCount + 20))));
+        if (this._renderCount < this.treeChildren.length)
+            setTimeout(
+                action(() => {
+                    this._renderCount = Math.min(this.treeChildren.length, this._renderCount + 20);
+                })
+            );
         return TreeView.GetChildElements(
             this.treeChildren,
             this,
@@ -326,7 +336,6 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree
             this.observeHeight,
             this.unobserveHeight,
             this.childContextMenuItems(),
-            //TODO: [AL] add these
             this._props.AddToMap,
             this._props.RemFromMap,
             this._props.hierarchyIndex,
@@ -337,7 +346,9 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree
         return this.dataDoc === null ? null : (
             <div
                 className="collectionTreeView-titleBar"
-                ref={action((r: any) => (this._titleRef = r) && (this._titleHeight = r.getBoundingClientRect().height * this.ScreenToLocalBoxXf().Scale))}
+                ref={action((r: any) => {
+                    (this._titleRef = r) && (this._titleHeight = r.getBoundingClientRect().height * this.ScreenToLocalBoxXf().Scale);
+                })}
                 key={this.Document[Id]}
                 style={!this.outlineMode ? { marginLeft: this.marginX(), paddingTop: this.marginTop() } : {}}>
                 {this.outlineMode ? this.documentTitle : this.editableTitle}
@@ -420,7 +431,11 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree
         return (
             <div style={{ display: 'flex', flexDirection: 'column', height: '100%', pointerEvents: 'all' }}>
                 {!this.buttonMenu && !this.noviceExplainer ? null : (
-                    <div className="documentButtonMenu" ref={action((r: HTMLDivElement | null) => r && (this._headerHeight = DivHeight(r)))}>
+                    <div
+                        className="documentButtonMenu"
+                        ref={action((r: HTMLDivElement | null) => {
+                            r && (this._headerHeight = DivHeight(r));
+                        })}>
                         {this.buttonMenu}
                         {this.noviceExplainer}
                     </div>
@@ -453,7 +468,7 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree
                                 minHeight: '100%',
                             }}
                             onWheel={e => e.stopPropagation()}
-                            onClick={e => (!this.layoutDoc.forceActive ? this._props.select(false) : SelectionManager.DeselectAll())}
+                            onClick={() => (!this.layoutDoc.forceActive ? this._props.select(false) : SelectionManager.DeselectAll())}
                             onDrop={this.onTreeDrop}>
                             <ul className={`no-indent${this.outlineMode ? '-outline' : ''}`}>{this.treeViewElements}</ul>
                         </div>
@@ -470,13 +485,14 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree
             <div style={{ transform: `scale(${scale})`, transformOrigin: 'top left', width: `${100 / scale}%`, height: `${100 / scale}%` }}>
                 {!(this.Document instanceof Doc) || !this.treeChildren ? null : this.Document.treeView_HasOverlay ? (
                     <CollectionFreeFormView
+                        // eslint-disable-next-line react/jsx-props-no-spreading
                         {...this._props}
                         setContentViewBox={emptyFunction}
                         NativeWidth={returnZero}
                         NativeHeight={returnZero}
                         pointerEvents={this._props.isContentActive() && SnappingManager.IsDragging ? returnAll : returnNone}
-                        isAnnotationOverlay={true}
-                        isAnnotationOverlayScrollable={true}
+                        isAnnotationOverlay
+                        isAnnotationOverlayScrollable
                         childDocumentsActive={this._props.isContentActive}
                         fieldKey={this._props.fieldKey + '_annotations'}
                         dropAction={dropActionType.move}
@@ -500,6 +516,7 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree
     }
 }
 
+// eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function addTreeFolder(doc: Doc) {
     CollectionTreeView.addTreeFolder(doc);
 });
diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx
index 8a2e83ed9..a661cf6a2 100644
--- a/src/client/views/collections/TabDocView.tsx
+++ b/src/client/views/collections/TabDocView.tsx
@@ -32,7 +32,7 @@ import { ObservableReactComponent } from '../ObservableReactComponent';
 import { DefaultStyleProvider, StyleProp } from '../StyleProvider';
 import { Colors } from '../global/globalEnums';
 import { DocumentView, OpenWhere, OpenWhereMod, returnEmptyDocViewList } from '../nodes/DocumentView';
-import { FieldViewProps, FocusViewOptions } from '../nodes/FieldView';
+import { FieldViewProps } from '../nodes/FieldView';
 import { KeyValueBox } from '../nodes/KeyValueBox';
 import { PresBox, PresMovement } from '../nodes/trails';
 import { CollectionDockingView } from './CollectionDockingView';
@@ -42,6 +42,146 @@ import { CollectionFreeFormView } from './collectionFreeForm/CollectionFreeFormV
 
 const _global = (window /* browser */ || global) /* node */ as any;
 
+interface TabMinimapViewProps {
+    document: Doc;
+    tabView: () => DocumentView | undefined;
+    addDocTab: (doc: Doc | Doc[], where: OpenWhere) => boolean;
+    PanelWidth: () => number;
+    PanelHeight: () => number;
+    background: () => string;
+}
+interface TabMiniThumbProps {
+    miniWidth: () => number;
+    miniHeight: () => number;
+    miniTop: () => number;
+    miniLeft: () => number;
+}
+
+@observer
+class TabMiniThumb extends React.Component<TabMiniThumbProps> {
+    render() {
+        const { miniWidth, miniHeight, miniLeft, miniTop } = this.props;
+        return <div className="miniThumb" style={{ width: `${miniWidth()}%`, height: `${miniHeight()}%`, left: `${miniLeft()}%`, top: `${miniTop()}%` }} />;
+    }
+}
+@observer
+export class TabMinimapView extends ObservableReactComponent<TabMinimapViewProps> {
+    static miniStyleProvider = (doc: Opt<Doc>, props: Opt<FieldViewProps>, property: string): any => {
+        if (doc) {
+            switch (property.split(':')[0]) {
+                case StyleProp.PointerEvents: return 'none';
+                case StyleProp.DocContents: {
+                    const background = (() => {
+                        switch (doc.type as DocumentType) {
+                            case DocumentType.PDF:    return 'pink';
+                            case DocumentType.AUDIO:  return 'lightgreen';
+                            case DocumentType.WEB:    return 'brown';
+                            case DocumentType.IMG:    return 'blue';
+                            case DocumentType.MAP:    return 'orange';
+                            case DocumentType.VID:    return 'purple';
+                            case DocumentType.RTF:    return 'yellow';
+                            case DocumentType.COL:    return undefined;
+                            default:                  return 'gray';
+                        } // prettier-ignore
+                    })();
+                    return !background ? undefined : <div style={{ width: NumCast(doc._width), height: NumCast(doc._height), position: 'absolute', display: 'block', background }} />;
+                }
+                default: return DefaultStyleProvider(doc, props, property);
+            } // prettier-ignore
+        }
+        return undefined;
+    };
+
+    @computed get renderBounds() {
+        const compView = this._props.tabView()?.ComponentView as CollectionFreeFormView;
+        const bounds = compView?.freeformData?.(true)?.bounds;
+        if (!bounds) return undefined;
+        const xbounds = bounds.r - bounds.x;
+        const ybounds = bounds.b - bounds.y;
+        const dim = Math.max(xbounds, ybounds);
+        return { l: bounds.x + xbounds / 2 - dim / 2, t: bounds.y + ybounds / 2 - dim / 2, cx: bounds.x + xbounds / 2, cy: bounds.y + ybounds / 2, dim };
+    }
+    @computed get xPadding() {
+        return !this.renderBounds ? 0 : Math.max(0, this._props.PanelWidth() / NumCast(this._props.document._freeform_scale, 1) - 2 * (this.renderBounds.cx - this.renderBounds.l));
+    }
+    @computed get yPadding() {
+        return !this.renderBounds ? 0 : Math.max(0, this._props.PanelHeight() / NumCast(this._props.document._freeform_scale, 1) - 2 * (this.renderBounds.cy - this.renderBounds.l));
+    }
+    childLayoutTemplate = () => Cast(this._props.document.childLayoutTemplate, Doc, null);
+    returnMiniSize = () => NumCast(this._props.document._miniMapSize, 150);
+    miniDown = (e: React.PointerEvent) => {
+        const doc = this._props.document;
+        const miniSize = this.returnMiniSize();
+        doc &&
+            setupMoveUpEvents(
+                this,
+                e,
+                action((moveEv, down: number[], delta: number[]) => {
+                    const renderBounds = this.renderBounds ?? { l: 0, r: 0, t: 0, b: 0, dim: 1 };
+                    doc._freeform_panX = clamp(NumCast(doc._freeform_panX) + (delta[0] / miniSize) * renderBounds.dim, renderBounds.l, renderBounds.l + renderBounds.dim);
+                    doc._freeform_panY = clamp(NumCast(doc._freeform_panY) + (delta[1] / miniSize) * renderBounds.dim, renderBounds.t, renderBounds.t + renderBounds.dim);
+                    return false;
+                }),
+                emptyFunction,
+                emptyFunction
+            );
+    };
+    popup = () => {
+        if (!this.renderBounds) return <div />;
+        const { renderBounds } = this;
+        const miniWidth = () => (this._props.PanelWidth() / NumCast(this._props.document._freeform_scale, 1) / renderBounds.dim) * 100;
+        const miniHeight = () => (this._props.PanelHeight() / NumCast(this._props.document._freeform_scale, 1) / renderBounds.dim) * 100;
+        const miniLeft = () => 50 + ((NumCast(this._props.document._freeform_panX) - renderBounds.cx) / renderBounds.dim) * 100 - miniWidth() / 2;
+        const miniTop = () => 50 + ((NumCast(this._props.document._freeform_panY) - renderBounds.cy) / renderBounds.dim) * 100 - miniHeight() / 2;
+        const miniSize = this.returnMiniSize();
+        return (
+            <div className="miniMap" style={{ width: miniSize, height: miniSize, background: this._props.background() }}>
+                <CollectionFreeFormView
+                    Document={this._props.document}
+                    docViewPath={returnEmptyDocViewList}
+                    childLayoutTemplate={this.childLayoutTemplate} // bcz: Ugh .. should probably be rendering a CollectionView or the minimap should be part of the collectionFreeFormView to avoid having to set stuff like this.
+                    noOverlay // don't render overlay Docs since they won't scale
+                    isContentActive={emptyFunction}
+                    isAnyChildContentActive={returnFalse}
+                    select={emptyFunction}
+                    isSelected={returnFalse}
+                    dontRegisterView
+                    fieldKey={Doc.LayoutFieldKey(this._props.document)}
+                    addDocument={returnFalse}
+                    moveDocument={returnFalse}
+                    removeDocument={returnFalse}
+                    PanelWidth={this.returnMiniSize}
+                    PanelHeight={this.returnMiniSize}
+                    ScreenToLocalTransform={Transform.Identity}
+                    renderDepth={0}
+                    whenChildContentsActiveChanged={emptyFunction}
+                    focus={emptyFunction}
+                    styleProvider={TabMinimapView.miniStyleProvider}
+                    addDocTab={this._props.addDocTab}
+                    // eslint-disable-next-line no-use-before-define
+                    pinToPres={TabDocView.PinDoc}
+                    childFilters={CollectionDockingView.Instance?.childDocFilters ?? returnEmptyDoclist}
+                    childFiltersByRanges={CollectionDockingView.Instance?.childDocRangeFilters ?? returnEmptyDoclist}
+                    searchFilterDocs={CollectionDockingView.Instance?.searchFilterDocs ?? returnEmptyDoclist}
+                    fitContentsToBox={returnTrue}
+                    xPadding={this.xPadding}
+                    yPadding={this.yPadding}
+                />
+                <div className="miniOverlay" onPointerDown={this.miniDown}>
+                    <TabMiniThumb miniLeft={miniLeft} miniTop={miniTop} miniWidth={miniWidth} miniHeight={miniHeight} />
+                </div>
+            </div>
+        );
+    };
+    render() {
+        return this._props.document.layout !== CollectionView.LayoutString(Doc.LayoutFieldKey(this._props.document)) || this._props.document?._type_collection !== CollectionViewType.Freeform ? null : (
+            <div className="miniMap-hidden">
+                <Popup icon={<FontAwesomeIcon icon="globe-asia" size="lg" />} color={SnappingManager.userVariantColor} type={Type.TERT} onPointerDown={e => e.stopPropagation()} placement="top-end" popup={this.popup} />
+            </div>
+        );
+    }
+}
+
 interface TabDocViewProps {
     documentId: FieldId;
     keyValue?: boolean;
@@ -92,7 +232,7 @@ export class TabDocView extends ObservableReactComponent<TabDocViewProps> {
     @action
     init = (tab: any, doc: Opt<Doc>) => {
         if (tab.contentItem === tab.header.parent.getActiveContentItem()) this._activated = true;
-        if (tab.DashDoc !== doc && doc && tab.hasOwnProperty('contentItem') && tab.contentItem.config.type !== 'stack') {
+        if (tab.DashDoc !== doc && doc && tab.contentItem?.config.type !== 'stack') {
             tab._disposers = {} as { [name: string]: IReactionDisposer };
             tab.contentItem.config.fixed && (tab.contentItem.parent.config.fixed = true);
             tab.DashDoc = doc;
@@ -130,17 +270,17 @@ export class TabDocView extends ObservableReactComponent<TabDocViewProps> {
                     setupMoveUpEvents(
                         this,
                         e,
-                        e =>
-                            !e.defaultPrevented &&
-                            DragManager.StartDocumentDrag([iconWrap], new DragManager.DocumentDragData([doc], doc.dropAction as dropActionType), e.clientX, e.clientY, undefined, () => {
+                        moveEv =>
+                            !moveEv.defaultPrevented &&
+                            DragManager.StartDocumentDrag([iconWrap], new DragManager.DocumentDragData([doc], doc.dropAction as dropActionType), moveEv.clientX, moveEv.clientY, undefined, () => {
                                 CollectionDockingView.CloseSplit(doc);
                             }),
                         returnFalse,
-                        action(e => {
+                        action(clickEv => {
                             if (this.view) {
                                 SelectionManager.SelectView(this.view, false);
                                 const child = getChild();
-                                simulateMouseClick(child, e.clientX, e.clientY + 30, e.screenX, e.screenY + 30);
+                                simulateMouseClick(child, clickEv.clientX, clickEv.clientY + 30, clickEv.screenX, clickEv.screenY + 30);
                             } else {
                                 this._activated = true;
                                 setTimeout(() => this.view && SelectionManager.SelectView(this.view, false));
@@ -160,7 +300,7 @@ export class TabDocView extends ObservableReactComponent<TabDocViewProps> {
                     ({ variant, degree, highlight }) => {
                         const color = highlight?.highlightIndex === Doc.DocBrushStatus.highlighted ? highlight.highlightColor : degree ? ['transparent', variant, variant, 'orange'][degree] : variant;
 
-                        const textColor = color === variant ? SnappingManager.userColor : lightOrDark(color);
+                        const textColor = color === variant ? SnappingManager.userColor ?? '' : lightOrDark(color);
                         titleEle.style.color = textColor;
                         iconWrap.style.color = textColor;
                         closeWrap.style.color = textColor;
@@ -186,19 +326,19 @@ export class TabDocView extends ObservableReactComponent<TabDocViewProps> {
                 );
             }
             // shifts the focus to this tab when another tab is dragged over it
-            tab.element[0].onmouseenter = (e: MouseEvent) => {
+            tab.element[0].onmouseenter = () => {
                 if (SnappingManager.IsDragging && tab.contentItem !== tab.header.parent.getActiveContentItem()) {
                     tab.header.parent.setActiveContentItem(tab.contentItem);
                     tab.setActive(true);
                 }
                 this._document && Doc.BrushDoc(this._document);
             };
-            tab.element[0].onmouseleave = (e: MouseEvent) => {
+            tab.element[0].onmouseleave = () => {
                 this._document && Doc.UnBrushDoc(this._document);
             };
 
             tab.element[0].oncontextmenu = (e: MouseEvent) => {
-                let child = getChild();
+                const child = getChild();
                 if (child) {
                     simulateMouseClick(child, e.clientX, e.clientY + 30, e.screenX, e.screenY + 30);
                     e.stopPropagation();
@@ -220,12 +360,9 @@ export class TabDocView extends ObservableReactComponent<TabDocViewProps> {
                 () => SelectionManager.IsSelected(this._document),
                 action(selected => {
                     if (selected) this._activated = true;
-                    const toggle = tab.element[0].children[2].children[0] as HTMLInputElement;
                     if (selected && tab.contentItem !== tab.header.parent.getActiveContentItem()) {
                         undoable(() => tab.header.parent.setActiveContentItem(tab.contentItem), 'tab switch')();
                     }
-                    //toggle.style.fontWeight = selected ? 'bold' : '';
-                    // toggle.style.textTransform = selected ? "uppercase" : "";
                 }),
                 { fireImmediately: true }
             );
@@ -233,14 +370,16 @@ export class TabDocView extends ObservableReactComponent<TabDocViewProps> {
             // highlight the tab when the tab document is brushed in any part of the UI
             tab._disposers.reactionDisposer = reaction(
                 () => doc?.title,
-                title => (titleEle.value = title),
+                title => {
+                    titleEle.value = title;
+                },
                 { fireImmediately: true }
             );
 
             // clean up the tab when it is closed
             tab.closeElement
-                .off('click') //unbind the current click handler
-                .click(function () {
+                .off('click') // unbind the current click handler
+                .click(() => {
                     Object.values(tab._disposers).forEach((disposer: any) => disposer?.());
                     SelectionManager.DeselectAll();
                     UndoManager.RunInBatch(() => tab.contentItem.remove(), 'delete tab');
@@ -250,7 +389,7 @@ export class TabDocView extends ObservableReactComponent<TabDocViewProps> {
 
     /**
      * Adds a document to the presentation view
-     **/
+     * */
     @action
     public static PinDoc(docs: Doc | Doc[], pinProps: PinProps) {
         const docList = docs instanceof Doc ? [docs] : docs;
@@ -310,7 +449,7 @@ export class TabDocView extends ObservableReactComponent<TabDocViewProps> {
             if (curPres.expandBoolean) pinDoc.presentation_expandInlineButton = true;
             Doc.AddDocToList(curPres, 'data', pinDoc, PresBox.Instance?.sortArray()?.lastElement());
             PresBox.Instance?.clearSelectedArray();
-            pinDoc && PresBox.Instance?.addToSelectedArray(pinDoc); //Update selected array
+            pinDoc && PresBox.Instance?.addToSelectedArray(pinDoc); // Update selected array
         });
         if (
             // open the presentation trail if it's not already opened
@@ -328,6 +467,7 @@ export class TabDocView extends ObservableReactComponent<TabDocViewProps> {
     componentDidMount() {
         new _global.ResizeObserver(
             action((entries: any) => {
+                // eslint-disable-next-line no-restricted-syntax
                 for (const entry of entries) {
                     this._panelWidth = entry.contentRect.width;
                     this._panelHeight = entry.contentRect.height;
@@ -374,29 +514,30 @@ export class TabDocView extends ObservableReactComponent<TabDocViewProps> {
     //                                  "replace:right" - will replace the stack on the right named "right" if it exists, or create a stack on the right with that name,
     //                                   "replace:monkeys" - will replace any tab that has the label 'monkeys', or a tab with that label will be created by default on the right
     //  lightbox - will add the document to any collection along the path from the document to the docking view that has a field isLightbox. if none is found, it adds to the full screen lightbox
-    addDocTab = (doc: Doc, location: OpenWhere) => {
+    addDocTab = (docsIn: Doc | Doc[], location: OpenWhere) => {
+        const docs = docsIn instanceof Doc ? [docsIn] : docsIn;
         SelectionManager.DeselectAll();
         const whereFields = location.split(':');
         const keyValue = whereFields.includes(OpenWhereMod.keyvalue);
         const whereMods = whereFields.length > 1 ? (whereFields[1] as OpenWhereMod) : OpenWhereMod.none;
         const panelName = whereFields.length > 1 ? whereFields.lastElement() : '';
-        if (doc.dockingConfig && !keyValue) return DashboardView.openDashboard(doc);
+        if (docs[0]?.dockingConfig && !keyValue) return DashboardView.openDashboard(docs[0]);
         // prettier-ignore
         switch (whereFields[0]) {
             case undefined:
             case OpenWhere.lightbox:   if (this.layoutDoc?._isLightbox) {
-                                            const lightboxView = !doc.annotationOn && DocCast(doc.embedContainer) ? DocumentManager.Instance.getFirstDocumentView(DocCast(doc.embedContainer)) : undefined;
+                                            const lightboxView = !docs[0].annotationOn && DocCast(docs[0].embedContainer) ? DocumentManager.Instance.getFirstDocumentView(DocCast(docs[0].embedContainer)) : undefined;
                                             const data = lightboxView?.dataDoc[Doc.LayoutFieldKey(lightboxView.Document)];
                                             if (lightboxView && (!data || data instanceof List)) {
-                                                lightboxView.layoutDoc[Doc.LayoutFieldKey(lightboxView.Document)] = new List<Doc>([doc]);
+                                                lightboxView.layoutDoc[Doc.LayoutFieldKey(lightboxView.Document)] = new List<Doc>(docs);
                                                 return true;
                                             }
                                        }
-                                       return LightboxView.Instance.AddDocTab(doc, OpenWhere.lightbox);
-            case OpenWhere.close:      return CollectionDockingView.CloseSplit(doc, whereMods);
-            case OpenWhere.replace:    return CollectionDockingView.ReplaceTab(doc, whereMods, this.stack, panelName, undefined, keyValue);
-            case OpenWhere.toggle:     return CollectionDockingView.ToggleSplit(doc, whereMods, this.stack, TabDocView.DontSelectOnActivate, keyValue);
-            case OpenWhere.add:default:return CollectionDockingView.AddSplit(doc, whereMods, this.stack, undefined, keyValue);
+                                       return LightboxView.Instance.AddDocTab(docs[0], OpenWhere.lightbox);
+            case OpenWhere.close:      return CollectionDockingView.CloseSplit(docs[0], whereMods);
+            case OpenWhere.replace:    return CollectionDockingView.ReplaceTab(docs[0], whereMods, this.stack, panelName, undefined, keyValue);
+            case OpenWhere.toggle:     return CollectionDockingView.ToggleSplit(docs[0], whereMods, this.stack, TabDocView.DontSelectOnActivate, keyValue);
+            case OpenWhere.add:default:return CollectionDockingView.AddSplit(docs[0], whereMods, this.stack, undefined, keyValue);
         }
     };
     remDocTab = (doc: Doc | Doc[]) => {
@@ -410,12 +551,12 @@ export class TabDocView extends ObservableReactComponent<TabDocViewProps> {
 
     getCurrentFrame = () => NumCast(Cast(PresBox.Instance.activeItem.presentation_targetDoc, Doc, null)._currentFrame);
     static Activate = (tabDoc: Doc) => {
-        const tab = Array.from(CollectionDockingView.Instance?.tabMap!).find(tab => tab.DashDoc === tabDoc && !tab.contentItem.config.props.keyValue);
+        const tab = Array.from(CollectionDockingView.Instance?.tabMap!).find(findTab => findTab.DashDoc === tabDoc && !findTab.contentItem.config.props.keyValue);
         tab?.header.parent.setActiveContentItem(tab.contentItem); // glr: Panning does not work when this is set - (this line is for trying to make a tab that is not topmost become topmost)
         return tab !== undefined;
     };
     @action
-    focusFunc = (doc: Doc, options: FocusViewOptions) => {
+    focusFunc = () => {
         if (!this.tab.header.parent._activeContentItem || this.tab.header.parent._activeContentItem !== this.tab.contentItem) {
             this.tab.header.parent.setActiveContentItem(this.tab.contentItem); // glr: Panning does not work when this is set - (this line is for trying to make a tab that is not topmost become topmost)
         }
@@ -498,8 +639,12 @@ export class TabDocView extends ObservableReactComponent<TabDocViewProps> {
                         }
                         this._lastTab = this.tab;
                         (this._mainCont as any).InitTab = (tab: any) => this.init(tab, this._document);
-                        DocServer.GetRefField(this._props.documentId).then(action(doc => doc instanceof Doc && (this._document = doc) && this.tab && this.init(this.tab, this._document)));
-                        new _global.ResizeObserver(action((entries: any) => this._forceInvalidateScreenToLocal++)).observe(ref);
+                        DocServer.GetRefField(this._props.documentId).then(
+                            action(doc => {
+                                doc instanceof Doc && (this._document = doc) && this.tab && this.init(this.tab, this._document);
+                            })
+                        );
+                        new _global.ResizeObserver(action(() => this._forceInvalidateScreenToLocal++)).observe(ref);
                     }
                 }}>
                 {this.docView}
@@ -507,141 +652,3 @@ export class TabDocView extends ObservableReactComponent<TabDocViewProps> {
         );
     }
 }
-
-interface TabMinimapViewProps {
-    document: Doc;
-    tabView: () => DocumentView | undefined;
-    addDocTab: (doc: Doc, where: OpenWhere) => boolean;
-    PanelWidth: () => number;
-    PanelHeight: () => number;
-    background: () => string;
-}
-interface TabMiniThumbProps {
-    miniWidth: () => number;
-    miniHeight: () => number;
-    miniTop: () => number;
-    miniLeft: () => number;
-}
-
-@observer
-class TabMiniThumb extends React.Component<TabMiniThumbProps> {
-    render() {
-        const { miniWidth, miniHeight, miniLeft, miniTop } = this.props;
-        return <div className="miniThumb" style={{ width: `${miniWidth()}%`, height: `${miniHeight()}%`, left: `${miniLeft()}%`, top: `${miniTop()}%` }} />;
-    }
-}
-@observer
-export class TabMinimapView extends ObservableReactComponent<TabMinimapViewProps> {
-    static miniStyleProvider = (doc: Opt<Doc>, props: Opt<FieldViewProps>, property: string): any => {
-        if (doc) {
-            switch (property.split(':')[0]) {
-                case StyleProp.PointerEvents: return 'none';
-                case StyleProp.DocContents: {
-                    const background = (() => {
-                        switch (doc.type as DocumentType) {
-                            case DocumentType.PDF:    return 'pink';
-                            case DocumentType.AUDIO:  return 'lightgreen';
-                            case DocumentType.WEB:    return 'brown';
-                            case DocumentType.IMG:    return 'blue';
-                            case DocumentType.MAP:    return 'orange';
-                            case DocumentType.VID:    return 'purple';
-                            case DocumentType.RTF:    return 'yellow';
-                            case DocumentType.COL:    return undefined;
-                            default:                  return 'gray';
-                        } // prettier-ignore
-                    })();
-                    return !background ? undefined : <div style={{ width: NumCast(doc._width), height: NumCast(doc._height), position: 'absolute', display: 'block', background }} />;
-                }
-                default: return DefaultStyleProvider(doc, props, property);
-            } // prettier-ignore
-        }
-    };
-
-    @computed get renderBounds() {
-        const compView = this._props.tabView()?.ComponentView as CollectionFreeFormView;
-        const bounds = compView?.freeformData?.(true)?.bounds;
-        if (!bounds) return undefined;
-        const xbounds = bounds.r - bounds.x;
-        const ybounds = bounds.b - bounds.y;
-        const dim = Math.max(xbounds, ybounds);
-        return { l: bounds.x + xbounds / 2 - dim / 2, t: bounds.y + ybounds / 2 - dim / 2, cx: bounds.x + xbounds / 2, cy: bounds.y + ybounds / 2, dim };
-    }
-    @computed get xPadding() {
-        return !this.renderBounds ? 0 : Math.max(0, this._props.PanelWidth() / NumCast(this._props.document._freeform_scale, 1) - 2 * (this.renderBounds.cx - this.renderBounds.l));
-    }
-    @computed get yPadding() {
-        return !this.renderBounds ? 0 : Math.max(0, this._props.PanelHeight() / NumCast(this._props.document._freeform_scale, 1) - 2 * (this.renderBounds.cy - this.renderBounds.l));
-    }
-    childLayoutTemplate = () => Cast(this._props.document.childLayoutTemplate, Doc, null);
-    returnMiniSize = () => NumCast(this._props.document._miniMapSize, 150);
-    miniDown = (e: React.PointerEvent) => {
-        const doc = this._props.document;
-        const miniSize = this.returnMiniSize();
-        doc &&
-            setupMoveUpEvents(
-                this,
-                e,
-                action((e: PointerEvent, down: number[], delta: number[]) => {
-                    const renderBounds = this.renderBounds ?? { l: 0, r: 0, t: 0, b: 0, dim: 1 };
-                    doc._freeform_panX = clamp(NumCast(doc._freeform_panX) + (delta[0] / miniSize) * renderBounds.dim, renderBounds.l, renderBounds.l + renderBounds.dim);
-                    doc._freeform_panY = clamp(NumCast(doc._freeform_panY) + (delta[1] / miniSize) * renderBounds.dim, renderBounds.t, renderBounds.t + renderBounds.dim);
-                    return false;
-                }),
-                emptyFunction,
-                emptyFunction
-            );
-    };
-    popup = () => {
-        if (!this.renderBounds) return <></>;
-        const renderBounds = this.renderBounds;
-        const miniWidth = () => (this._props.PanelWidth() / NumCast(this._props.document._freeform_scale, 1) / renderBounds.dim) * 100;
-        const miniHeight = () => (this._props.PanelHeight() / NumCast(this._props.document._freeform_scale, 1) / renderBounds.dim) * 100;
-        const miniLeft = () => 50 + ((NumCast(this._props.document._freeform_panX) - renderBounds.cx) / renderBounds.dim) * 100 - miniWidth() / 2;
-        const miniTop = () => 50 + ((NumCast(this._props.document._freeform_panY) - renderBounds.cy) / renderBounds.dim) * 100 - miniHeight() / 2;
-        const miniSize = this.returnMiniSize();
-        return (
-            <div className="miniMap" style={{ width: miniSize, height: miniSize, background: this._props.background() }}>
-                <CollectionFreeFormView
-                    Document={this._props.document}
-                    docViewPath={returnEmptyDocViewList}
-                    childLayoutTemplate={this.childLayoutTemplate} // bcz: Ugh .. should probably be rendering a CollectionView or the minimap should be part of the collectionFreeFormView to avoid having to set stuff like this.
-                    noOverlay={true} // don't render overlay Docs since they won't scale
-                    isContentActive={emptyFunction}
-                    isAnyChildContentActive={returnFalse}
-                    select={emptyFunction}
-                    isSelected={returnFalse}
-                    dontRegisterView={true}
-                    fieldKey={Doc.LayoutFieldKey(this._props.document)}
-                    addDocument={returnFalse}
-                    moveDocument={returnFalse}
-                    removeDocument={returnFalse}
-                    PanelWidth={this.returnMiniSize}
-                    PanelHeight={this.returnMiniSize}
-                    ScreenToLocalTransform={Transform.Identity}
-                    renderDepth={0}
-                    whenChildContentsActiveChanged={emptyFunction}
-                    focus={emptyFunction}
-                    styleProvider={TabMinimapView.miniStyleProvider}
-                    addDocTab={this._props.addDocTab}
-                    pinToPres={TabDocView.PinDoc}
-                    childFilters={CollectionDockingView.Instance?.childDocFilters ?? returnEmptyDoclist}
-                    childFiltersByRanges={CollectionDockingView.Instance?.childDocRangeFilters ?? returnEmptyDoclist}
-                    searchFilterDocs={CollectionDockingView.Instance?.searchFilterDocs ?? returnEmptyDoclist}
-                    fitContentsToBox={returnTrue}
-                    xPadding={this.xPadding}
-                    yPadding={this.yPadding}
-                />
-                <div className="miniOverlay" onPointerDown={this.miniDown}>
-                    <TabMiniThumb miniLeft={miniLeft} miniTop={miniTop} miniWidth={miniWidth} miniHeight={miniHeight} />
-                </div>
-            </div>
-        );
-    };
-    render() {
-        return this._props.document.layout !== CollectionView.LayoutString(Doc.LayoutFieldKey(this._props.document)) || this._props.document?._type_collection !== CollectionViewType.Freeform ? null : (
-            <div className="miniMap-hidden">
-                <Popup icon={<FontAwesomeIcon icon="globe-asia" size="lg" />} color={SnappingManager.userVariantColor} type={Type.TERT} onPointerDown={e => e.stopPropagation()} placement="top-end" popup={this.popup} />
-            </div>
-        );
-    }
-}
diff --git a/src/client/views/collections/TreeView.tsx b/src/client/views/collections/TreeView.tsx
index 20323a521..fab8e3892 100644
--- a/src/client/views/collections/TreeView.tsx
+++ b/src/client/views/collections/TreeView.tsx
@@ -55,7 +55,7 @@ export interface TreeViewProps {
     treeViewParent: Doc;
     renderDepth: number;
     dragAction: dropActionType;
-    addDocTab: (doc: Doc, where: OpenWhere) => boolean;
+    addDocTab: (doc: Doc | Doc[], where: OpenWhere) => boolean;
     panelWidth: () => number;
     panelHeight: () => number;
     addDocument: (doc: Doc | Doc[], annotationKey?: string, relativeTo?: Doc, before?: boolean) => boolean;
@@ -200,11 +200,11 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> {
         }
         return false;
     };
-    @undoBatch remove = (doc: Doc | Doc[], key: string) => {
+    @undoBatch remove = (docs: Doc | Doc[], key: string) => {
         this.treeView._props.select(false);
-        const ind = DocListCast(this.dataDoc[key]).indexOf(doc instanceof Doc ? doc : doc.lastElement());
+        const ind = DocListCast(this.dataDoc[key]).indexOf(docs instanceof Doc ? docs : docs.lastElement());
 
-        const res = (doc instanceof Doc ? [doc] : doc).reduce((flg, doc) => flg && Doc.RemoveDocFromList(this.dataDoc, key, doc), true);
+        const res = (docs instanceof Doc ? [docs] : docs).reduce((flg, doc) => flg && Doc.RemoveDocFromList(this.dataDoc, key, doc), true);
         res && ind > 0 && DocumentManager.Instance.getDocumentView(DocListCast(this.dataDoc[key])[ind - 1], this.treeView.DocumentView?.())?.select(false);
         return res;
     };
@@ -318,7 +318,7 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> {
         this._props.hierarchyIndex !== undefined && this._props.AddToMap?.(this.Document, this._props.hierarchyIndex);
     }
 
-    onDragUp = (e: PointerEvent) => {
+    onDragUp = () => {
         document.removeEventListener('pointerup', this.onDragUp, true);
         document.removeEventListener('pointermove', this.onDragMove, true);
     };
@@ -403,7 +403,7 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> {
         if (!this._header.current) return false;
         const rect = this._header.current.getBoundingClientRect();
         const before = pt[1] < rect.top + rect.height / 2;
-        const inside = this.treeView.fileSysMode && !this.Document.isFolder ? false : pt[0] > rect.left + rect.width * 0.33 || (!before && this.treeViewOpen && this.childDocs?.length ? true : false);
+        const inside = this.treeView.fileSysMode && !this.Document.isFolder ? false : pt[0] > rect.left + rect.width * 0.33 || !!(!before && this.treeViewOpen && this.childDocs?.length);
         if (de.complete.linkDragData) {
             const sourceDoc = de.complete.linkDragData.linkSourceGetAnchor();
             const destDoc = this.Document;
@@ -431,14 +431,14 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> {
         return false;
     };
 
-    localAdd = (doc: Doc | Doc[]) => {
-        const innerAdd = (doc: Doc) => {
+    localAdd = (docs: Doc | Doc[]): boolean => {
+        const innerAdd = (doc: Doc): boolean => {
             const dataIsComputed = ComputedField.WithoutComputed(() => FieldValue(this.dataDoc[this.fieldKey])) instanceof ComputedField;
             const added = (!dataIsComputed || (this.dropping && this.moving)) && Doc.AddDocToList(this.dataDoc, this.fieldKey, doc);
             dataIsComputed && Doc.SetContainer(doc, DocCast(this.Document.embedContainer));
             return added;
         };
-        return (doc instanceof Doc ? [doc] : doc).reduce((flg, doc) => flg && innerAdd(doc), true as boolean);
+        return (docs instanceof Doc ? [docs] : docs).reduce((flg, doc) => flg && innerAdd(doc), true as boolean);
     };
 
     dropping: boolean = false;
@@ -513,16 +513,16 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> {
             const leftOffset = observable({ width: 0 });
             const expandedWidth = () => this._props.panelWidth() - leftOffset.width;
             if (contents instanceof Doc || (Cast(contents, listSpec(Doc)) && Cast(contents, listSpec(Doc))!.length && Cast(contents, listSpec(Doc))![0] instanceof Doc)) {
-                const remDoc = (doc: Doc | Doc[]) => this.remove(doc, key);
-                const moveDoc = (doc: Doc | Doc[], target: Doc | undefined, addDoc: (doc: Doc | Doc[]) => boolean) => this.move(doc, target, addDoc);
-                const addDoc = (doc: Doc | Doc[], addBefore?: Doc, before?: boolean) => {
-                    const innerAdd = (doc: Doc) => {
+                const remDoc = (docs: Doc | Doc[]) => this.remove(docs, key);
+                const moveDoc = (docs: Doc | Doc[], target: Doc | undefined, addDoc: (doc: Doc | Doc[]) => boolean) => this.move(docs, target, addDoc);
+                const addDoc = (docs: Doc | Doc[], addBefore?: Doc, before?: boolean) => {
+                    const innerAdd = (iDoc: Doc) => {
                         const dataIsComputed = ComputedField.WithoutComputed(() => FieldValue(this.dataDoc[key])) instanceof ComputedField;
-                        const added = (!dataIsComputed || (this.dropping && this.moving)) && Doc.AddDocToList(this.dataDoc, key, doc, addBefore, before, false, true);
-                        dataIsComputed && Doc.SetContainer(doc, DocCast(this.Document.embedContainer));
+                        const added = (!dataIsComputed || (this.dropping && this.moving)) && Doc.AddDocToList(this.dataDoc, key, iDoc, addBefore, before, false, true);
+                        dataIsComputed && Doc.SetContainer(iDoc, DocCast(this.Document.embedContainer));
                         return added;
                     };
-                    return (doc instanceof Doc ? [doc] : doc).reduce((flg, doc) => flg && innerAdd(doc), true as boolean);
+                    return (docs instanceof Doc ? [docs] : docs).reduce((flg, iDoc) => flg && innerAdd(iDoc), true as boolean);
                 };
                 contentElement = TreeView.GetChildElements(
                     contents instanceof Doc ? [contents] : DocListCast(contents),
@@ -643,7 +643,7 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> {
 
                 return added;
             };
-            const addDoc = (doc: Doc | Doc[], addBefore?: Doc, before?: boolean) => (doc instanceof Doc ? [doc] : doc).reduce((flg, doc) => flg && localAdd(doc, addBefore, before), true);
+            const addDoc = (docs: Doc | Doc[], addBefore?: Doc, before?: boolean) => (docs instanceof Doc ? [docs] : docs).reduce((flg, doc) => flg && localAdd(doc, addBefore, before), true);
             const docs = expandKey === 'embeddings' ? this.childEmbeddings : expandKey === 'links' ? this.childLinks : expandKey === 'annotations' ? this.childAnnos : this.childDocs;
             let downX = 0;
             let downY = 0;
@@ -911,7 +911,7 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> {
         // prettier-ignore
         switch (property.split(':')[0]) {
             case StyleProp.Opacity:         return this.treeView.outlineMode ? undefined : 1;
-            case StyleProp.BackgroundColor: return this.selected ? '#7089bb' : undefined;//StrCast(doc._backgroundColor, StrCast(doc.backgroundColor));
+            case StyleProp.BackgroundColor: return this.selected ? '#7089bb' : undefined; // StrCast(doc._backgroundColor, StrCast(doc.backgroundColor));
             case StyleProp.Highlighting:    if (this.treeView.outlineMode) return undefined;
                                             break;
             case StyleProp.BoxShadow:       return undefined;
@@ -1192,7 +1192,7 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> {
         const pt = [de.clientX, de.clientY];
         const rect = this._header.current!.getBoundingClientRect();
         const before = pt[1] < rect.top + rect.height / 2;
-        const inside = this.treeView.fileSysMode && !this.Document.isFolder ? false : pt[0] > rect.left + rect.width * 0.33 || (!before && this.treeViewOpen && this.childDocs?.length ? true : false);
+        const inside = this.treeView.fileSysMode && !this.Document.isFolder ? false : pt[0] > rect.left + rect.width * 0.33 || !!(!before && this.treeViewOpen && this.childDocs?.length);
 
         this.treeView.onTreeDrop(de, (docs: Doc[]) => this.dropDocuments(docs, before, inside, dropActionType.copy, undefined, undefined, false, false));
     };
@@ -1259,7 +1259,7 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> {
         remove: undefined | ((doc: Doc | Doc[]) => boolean),
         move: DragManager.MoveFunction,
         dragAction: dropActionType,
-        addDocTab: (doc: Doc, where: OpenWhere) => boolean,
+        addDocTab: (doc: Doc | Doc[], where: OpenWhere) => boolean,
         styleProvider: undefined | StyleProviderFuncType,
         screenToLocalXf: () => Transform,
         isContentActive: (outsideReaction?: boolean) => boolean,
@@ -1282,11 +1282,6 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> {
         hierarchyIndex?: number[],
         renderCount?: number
     ) {
-        const viewSpecScript = Cast(treeViewParent.viewSpecScript, ScriptField);
-        if (viewSpecScript) {
-            childDocs = childDocs.filter(d => viewSpecScript.script.run({ doc: d }, console.log).result);
-        }
-
         const docs = TreeView.sortDocs(childDocs, StrCast(treeViewParent.treeView_SortCriterion, TreeSort.WhenAdded));
         const rowWidth = () => panelWidth() - treeBulletWidth() * (treeView._props.NativeDimScaling?.() || 1);
         const treeViewRefs = new Map<Doc, TreeView | undefined>();
@@ -1327,7 +1322,6 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> {
                         dataDoc={pair.data}
                         treeViewParent={treeViewParent}
                         prevSibling={docs[i]}
-                        // TODO: [AL] add these
                         hierarchyIndex={hierarchyIndex ? [...hierarchyIndex, i + 1] : undefined}
                         AddToMap={AddToMap}
                         RemFromMap={RemFromMap}
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormInfoUI.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormInfoUI.tsx
index 29d835b28..65a529d62 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormInfoUI.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormInfoUI.tsx
@@ -4,7 +4,6 @@ import * as React from 'react';
 import { Doc, DocListCast, FieldType, FieldResult } from '../../../../fields/Doc';
 import { InkTool } from '../../../../fields/InkField';
 import { StrCast } from '../../../../fields/Types';
-import { DocumentManager } from '../../../util/DocumentManager';
 import { LinkManager } from '../../../util/LinkManager';
 import { ObservableReactComponent } from '../../ObservableReactComponent';
 import { DocButtonState, DocumentLinksButton } from '../../nodes/DocumentLinksButton';
@@ -49,8 +48,8 @@ export class CollectionFreeFormInfoUI extends ObservableReactComponent<Collectio
     setupStates = () => {
         this._originalbackground = StrCast(this._props.Document[DocData].backgroundColor);
         //  state entry functions
-        const setBackground = (colour: string) => () => {this._props.Document[DocData].backgroundColor = colour;} // prettier-ignore
-        const setOpacity = (opacity: number) => () => {this._props.LayoutDoc.opacity = opacity;} // prettier-ignore
+        // const setBackground = (colour: string) => () => {this._props.Document[DocData].backgroundColor = colour;} // prettier-ignore
+        // const setOpacity = (opacity: number) => () => {this._props.LayoutDoc.opacity = opacity;} // prettier-ignore
         //  arc transition trigger conditions
         const firstDoc = () => (this._props.childDocs().length ? this._props.childDocs()[0] : undefined);
         const numDocs = () => this._props.childDocs().length;
@@ -73,7 +72,6 @@ export class CollectionFreeFormInfoUI extends ObservableReactComponent<Collectio
 
         let trail: number;
 
-        const trailView = () => DocumentManager.Instance.DocumentViews.find(view => view.Document === Doc.MyTrails);
         const presentationMode = () => Doc.ActivePresentation?.presentation_status;
 
         // set of states
@@ -83,6 +81,7 @@ export class CollectionFreeFormInfoUI extends ObservableReactComponent<Collectio
                 docCreated: [() => numDocs(), () => {
                     docX = firstDoc()?.x;
                     docY = firstDoc()?.y;
+                    // eslint-disable-next-line no-use-before-define
                     return oneDoc;
                 }],
             }
@@ -93,18 +92,20 @@ export class CollectionFreeFormInfoUI extends ObservableReactComponent<Collectio
             {
                 // docCreated: [() => numDocs() > 1, () => multipleDocs],
                 docDeleted: [() => numDocs() < 1, () => start],
-                docMoved: [() => (docX && docX != docNewX()) || (docY && docY != docNewY()), () => {
+                docMoved: [() => (docX && docX !== docNewX()) || (docY && docY !== docNewY()), () => {
                     docX = firstDoc()?.x;
                     docY = firstDoc()?.y;
+                    // eslint-disable-next-line no-use-before-define
                     return movedDoc;
                 }],
             }
         ); // prettier-ignore
 
         const movedDoc = InfoState(
-            'Great moves. Try creating a second document. You can see the list of supported document types by typing a colon (\":\")',
+            'Great moves. Try creating a second document. You can see the list of supported document types by typing a colon (":")',
             {
-                docCreated: [() => numDocs() == 2, () => multipleDocs],
+                // eslint-disable-next-line no-use-before-define
+                docCreated: [() => numDocs() === 2, () => multipleDocs],
                 docDeleted: [() => numDocs() < 1, () => start],
             },
             'dash-colon-menu.gif',
@@ -114,6 +115,7 @@ export class CollectionFreeFormInfoUI extends ObservableReactComponent<Collectio
         const multipleDocs = InfoState(
             'Let\'s create a new link. Click the link icon on one of your documents.',
             {
+                // eslint-disable-next-line no-use-before-define
                 linkStarted: [() => linkStart(), () => startedLink],
                 docRemoved: [() => numDocs() < 2, () => oneDoc],
             },
@@ -124,6 +126,7 @@ export class CollectionFreeFormInfoUI extends ObservableReactComponent<Collectio
             'Now click the highlighted link icon on your other document.',
             {
                 linkUnstart: [() => linkUnstart(), () => multipleDocs],
+                // eslint-disable-next-line no-use-before-define
                 linkCreated: [() => numDocLinks(), () => madeLink],
                 docRemoved: [() => numDocs() < 2, () => oneDoc],
             },
@@ -136,6 +139,7 @@ export class CollectionFreeFormInfoUI extends ObservableReactComponent<Collectio
                 linkCreated: [() => !numDocLinks(), () => multipleDocs],
                 linkViewed: [() => linkMenuOpen(), () => {
                     alert(numDocLinks() + " cheer for " + numDocLinks() + " link!");
+                    // eslint-disable-next-line no-use-before-define
                     return viewedLink;
                 }],
             },
@@ -147,10 +151,12 @@ export class CollectionFreeFormInfoUI extends ObservableReactComponent<Collectio
             {
                 linkDeleted: [() => !numDocLinks(), () => multipleDocs],
                 docRemoved: [() => numDocs() < 2, () => oneDoc],
-                docCreated: [() => numDocs() == 3, () => {
+                docCreated: [() => numDocs() === 3, () => {
                     trail = pin().length;
+                    // eslint-disable-next-line no-use-before-define
                     return presentDocs;
                 }],
+                // eslint-disable-next-line no-use-before-define
                 activePen: [() => activeTool() === InkTool.Pen, () => penMode],
             },
             'documentation.png',
@@ -164,6 +170,7 @@ export class CollectionFreeFormInfoUI extends ObservableReactComponent<Collectio
                     () => pin().length > trail,
                     () => {
                         trail = pin().length;
+                        // eslint-disable-next-line no-use-before-define
                         return pinnedDoc1;
                     },
                 ],
@@ -186,11 +193,13 @@ export class CollectionFreeFormInfoUI extends ObservableReactComponent<Collectio
                 () => pin().length > trail,
                 () => {
                     trail = pin().length;
+                    // eslint-disable-next-line no-use-before-define
                     return pinnedDoc2;
                 },
             ],
             // editPresentation: [() => presentationMode() === 'edit', () => editPresentationMode],
             // manualPresentation: [() => presentationMode() === 'manual', () => manualPresentationMode],
+            // eslint-disable-next-line no-use-before-define
             autoPresentation: [() => presentationMode() === 'auto', () => autoPresentationMode],
             docRemoved: [() => numDocs() < 3, () => viewedLink],
         });
@@ -200,11 +209,13 @@ export class CollectionFreeFormInfoUI extends ObservableReactComponent<Collectio
                 () => pin().length > trail,
                 () => {
                     trail = pin().length;
+                    // eslint-disable-next-line no-use-before-define
                     return pinnedDoc3;
                 },
             ],
             // editPresentation: [() => presentationMode() === 'edit', () => editPresentationMode],
             // manualPresentation: [() => presentationMode() === 'manual', () => manualPresentationMode],
+            // eslint-disable-next-line no-use-before-define
             autoPresentation: [() => presentationMode() === 'auto', () => autoPresentationMode],
             docRemoved: [() => numDocs() < 3, () => viewedLink],
         });
@@ -219,6 +230,7 @@ export class CollectionFreeFormInfoUI extends ObservableReactComponent<Collectio
             ],
             // editPresentation: [() => presentationMode() === 'edit', () => editPresentationMode],
             // manualPresentation: [() => presentationMode() === 'manual', () => manualPresentationMode],
+            // eslint-disable-next-line no-use-before-define
             autoPresentation: [() => presentationMode() === 'auto', () => autoPresentationMode],
             docRemoved: [() => numDocs() < 3, () => viewedLink],
         });
@@ -236,15 +248,18 @@ export class CollectionFreeFormInfoUI extends ObservableReactComponent<Collectio
 
         const manualPresentationMode = InfoState("You're in manual presentation mode.", {
             // editPresentation: [() => presentationMode() === 'edit', () => editPresentationMode],
+            // eslint-disable-next-line no-use-before-define
             autoPresentation: [() => presentationMode() === 'auto', () => autoPresentationMode],
             docRemoved: [() => numDocs() < 3, () => viewedLink],
-            docCreated: [() => numDocs() == 4, () => completed],
+            // eslint-disable-next-line no-use-before-define
+            docCreated: [() => numDocs() === 4, () => completed],
         });
 
         const autoPresentationMode = InfoState("You're in auto presentation mode.", {
             // editPresentation: [() => presentationMode() === 'edit', () => editPresentationMode],
             manualPresentation: [() => presentationMode() === 'manual', () => manualPresentationMode],
             docRemoved: [() => numDocs() < 3, () => viewedLink],
+            // eslint-disable-next-line no-use-before-define
             docCreated: [() => numDocs() === 4, () => completed],
         });
 
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
index a0b96c75a..3970c6807 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
@@ -1,3 +1,4 @@
+/* eslint-disable no-use-before-define */
 import { Doc, Field, FieldType, FieldResult } from '../../../../fields/Doc';
 import { Id, ToString } from '../../../../fields/FieldSymbols';
 import { ObjectField } from '../../../../fields/ObjectField';
@@ -48,7 +49,7 @@ export interface PoolData {
 export interface ViewDefResult {
     ele: JSX.Element;
     bounds?: ViewDefBounds;
-    inkMask?: number; //sort elements into either the mask layer (which has a mixedBlendMode appropriate for transparent masks), or the regular documents layer;   -1 = no mask, 0 = mask layer but stroke is transparent (hidden, as in during a presentation when you want to smoothly animate it into being a mask), >0 = mask layer and not hidden
+    inkMask?: number; // sort elements into either the mask layer (which has a mixedBlendMode appropriate for transparent masks), or the regular documents layer;   -1 = no mask, 0 = mask layer but stroke is transparent (hidden, as in during a presentation when you want to smoothly animate it into being a mask), >0 = mask layer and not hidden
 }
 function toLabel(target: FieldResult<FieldType>) {
     if (typeof target === 'number' || Number(target)) {
@@ -84,9 +85,9 @@ interface PivotColumn {
     filters: string[];
 }
 
-export function computePassLayout(poolData: Map<string, PoolData>, pivotDoc: Doc, childPairs: { layout: Doc; data?: Doc }[], panelDim: number[], viewDefsToJSX: (views: ViewDefBounds[]) => ViewDefResult[], engineProps: any) {
+export function computePassLayout(poolData: Map<string, PoolData>, pivotDoc: Doc, childPairs: { layout: Doc; data?: Doc }[], panelDim: number[], viewDefsToJSX: (views: ViewDefBounds[]) => ViewDefResult[] /* , engineProps: any */) {
     const docMap = new Map<string, PoolData>();
-    childPairs.forEach(({ layout, data }, i) => {
+    childPairs.forEach(({ layout, data }) => {
         docMap.set(layout[Id], {
             x: NumCast(layout.x),
             y: NumCast(layout.y),
@@ -97,10 +98,15 @@ export function computePassLayout(poolData: Map<string, PoolData>, pivotDoc: Doc
             replica: '',
         });
     });
+    // eslint-disable-next-line no-use-before-define
     return normalizeResults(panelDim, 12, docMap, poolData, viewDefsToJSX, [], 0, []);
 }
 
-export function computeStarburstLayout(poolData: Map<string, PoolData>, pivotDoc: Doc, childPairs: { layout: Doc; data?: Doc }[], panelDim: number[], viewDefsToJSX: (views: ViewDefBounds[]) => ViewDefResult[], engineProps: any) {
+function toNumber(val: FieldResult<FieldType>) {
+    return val === undefined ? undefined : NumCast(val, Number(StrCast(val)));
+}
+
+export function computeStarburstLayout(poolData: Map<string, PoolData>, pivotDoc: Doc, childPairs: { layout: Doc; data?: Doc }[], panelDim: number[], viewDefsToJSX: (views: ViewDefBounds[]) => ViewDefResult[] /* , engineProps: any */) {
     const docMap = new Map<string, PoolData>();
     const burstDiam = [NumCast(pivotDoc._width), NumCast(pivotDoc._height)];
     const burstScale = NumCast(pivotDoc._starburstDocScale, 1);
@@ -132,7 +138,7 @@ export function computePivotLayout(poolData: Map<string, PoolData>, pivotDoc: Do
 
     let nonNumbers = 0;
     const pivotFieldKey = toLabel(engineProps?.pivotField ?? pivotDoc._pivotField) || 'author';
-    childPairs.map(pair => {
+    childPairs.forEach(pair => {
         const listValue = Cast(pair.layout[pivotFieldKey], listSpec('string'), null);
 
         const num = toNumber(pair.layout[pivotFieldKey]);
@@ -184,11 +190,11 @@ export function computePivotLayout(poolData: Map<string, PoolData>, pivotDoc: Do
     const textlen = Array.from(pivotColumnGroups.keys())
         .map(c => getTextWidth(toLabel(c), desc))
         .reduce((p, c) => Math.max(p, c), 0 as number);
-    const max_text = Math.min(Math.ceil(textlen / 120) * 28, panelDim[1] / 2);
+    const maxText = Math.min(Math.ceil(textlen / 120) * 28, panelDim[1] / 2);
     const maxInColumn = Array.from(pivotColumnGroups.values()).reduce((p, s) => Math.max(p, s.docs.length), 1);
 
     const colWidth = panelDim[0] / pivotColumnGroups.size;
-    const colHeight = panelDim[1] - max_text;
+    const colHeight = panelDim[1] - maxText;
     let numCols = 0;
     let bestArea = 0;
     let pivotAxisWidth = 0;
@@ -212,7 +218,7 @@ export function computePivotLayout(poolData: Map<string, PoolData>, pivotDoc: Do
     let x = 0;
     const sortedPivotKeys = pivotNumbers ? Array.from(pivotColumnGroups.keys()).sort((n1: FieldResult, n2: FieldResult) => toNumber(n1)! - toNumber(n2)!) : Array.from(pivotColumnGroups.keys()).sort();
     sortedPivotKeys.forEach(key => {
-        const val = pivotColumnGroups.get(key)!;
+        const val = pivotColumnGroups.get(key);
         let y = 0;
         let xCount = 0;
         const text = toLabel(key);
@@ -222,11 +228,11 @@ export function computePivotLayout(poolData: Map<string, PoolData>, pivotDoc: Do
             x,
             y: pivotAxisWidth,
             width: pivotAxisWidth * expander * numCols,
-            height: max_text,
+            height: maxText,
             fontSize,
             payload: val,
         });
-        val.docs.forEach((doc, i) => {
+        val?.docs.forEach((doc, i) => {
             const layoutDoc = Doc.Layout(doc);
             let wid = pivotAxisWidth;
             let hgt = pivotAxisWidth / (Doc.NativeAspect(layoutDoc) || 1);
@@ -262,14 +268,11 @@ export function computePivotLayout(poolData: Map<string, PoolData>, pivotDoc: Do
         payload: pivotColumnGroups.get(key)!.filters,
     }));
     groupNames.push(...dividers);
-    return normalizeResults(panelDim, max_text, docMap, poolData, viewDefsToJSX, groupNames, 0, []);
-}
-
-function toNumber(val: FieldResult<FieldType>) {
-    return val === undefined ? undefined : NumCast(val, Number(StrCast(val)));
+    // eslint-disable-next-line no-use-before-define
+    return normalizeResults(panelDim, maxText, docMap, poolData, viewDefsToJSX, groupNames, 0, []);
 }
 
-export function computeTimelineLayout(poolData: Map<string, PoolData>, pivotDoc: Doc, childPairs: { layout: Doc; data?: Doc }[], panelDim: number[], viewDefsToJSX: (views: ViewDefBounds[]) => ViewDefResult[], engineProps?: any) {
+export function computeTimelineLayout(poolData: Map<string, PoolData>, pivotDoc: Doc, childPairs: { layout: Doc; data?: Doc }[], panelDim: number[], viewDefsToJSX: (views: ViewDefBounds[]) => ViewDefResult[] /* , engineProps?: any */) {
     const fieldKey = 'data';
     const pivotDateGroups = new Map<number, Doc[]>();
     const docMap = new Map<string, PoolData>();
@@ -340,6 +343,7 @@ export function computeTimelineLayout(poolData: Map<string, PoolData>, pivotDoc:
         if (!stack && (curTime === undefined || Math.abs(x - (curTime - minTime) * scaling) > pivotAxisWidth)) {
             groupNames.push({ type: 'text', text: toLabel(key), x: x, y: stack * 25, height: fontHeight, fontSize, payload: undefined });
         }
+        // eslint-disable-next-line no-use-before-define
         layoutDocsAtTime(keyDocs, key);
     });
     if (sortedKeys.length && curTime !== undefined && curTime > sortedKeys[sortedKeys.length - 1]) {
@@ -404,7 +408,7 @@ function normalizeResults(
 
     Array.from(docMap.entries())
         .filter(ele => ele[1].pair)
-        .map(ele => {
+        .forEach(ele => {
             const newPosRaw = ele[1];
             if (newPosRaw) {
                 const newPos: PoolData = {
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index a70713429..d9c988d54 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -1,3 +1,4 @@
+/* eslint-disable react/jsx-props-no-spreading */
 /* eslint-disable jsx-a11y/click-events-have-key-events */
 /* eslint-disable jsx-a11y/no-static-element-interactions */
 import { Bezier } from 'bezier-js';
@@ -22,7 +23,6 @@ import { TraceMobx } from '../../../../fields/util';
 import { Gestures, PointData } from '../../../../pen-gestures/GestureTypes';
 import { GestureUtils } from '../../../../pen-gestures/GestureUtils';
 import { aggregateBounds, emptyFunction, intersectRect, Utils } from '../../../../Utils';
-import { CognitiveServices } from '../../../cognitive_services/CognitiveServices';
 import { Docs, DocUtils } from '../../../documents/Documents';
 import { CollectionViewType, DocumentType } from '../../../documents/DocumentTypes';
 import { DocumentManager } from '../../../util/DocumentManager';
@@ -48,6 +48,7 @@ import { DocumentView, OpenWhere } from '../../nodes/DocumentView';
 import { FieldViewProps, FocusViewOptions } from '../../nodes/FieldView';
 import { FormattedTextBox } from '../../nodes/formattedText/FormattedTextBox';
 import { PresBox } from '../../nodes/trails/PresBox';
+// eslint-disable-next-line import/extensions
 import { CreateImage } from '../../nodes/WebBoxRenderer';
 import { StyleProp } from '../../StyleProvider';
 import { CollectionSubView } from '../CollectionSubView';
@@ -76,7 +77,7 @@ export interface collectionFreeformViewProps {
 @observer
 export class CollectionFreeFormView extends CollectionSubView<Partial<collectionFreeformViewProps>>() {
     public get displayName() {
-        return 'CollectionFreeFormView(' + this.Document.title?.toString() + ')';
+        return 'CollectionFreeFormView(' + (this.Document.title?.toString() ?? '') + ')';
     } // this makes mobx trace() statements more descriptive
 
     @observable _paintedId = 'id' + Utils.GenerateGuid().replace(/-/g, '');
@@ -94,8 +95,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         super(props);
         makeObservable(this);
     }
-    @observable
-    public static ShowPresPaths = false;
 
     private _panZoomTransitionTimer: any;
     private _lastX: number = 0;
@@ -238,7 +237,9 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         }
     };
     @observable _keyframeEditing = false;
-    @action setKeyFrameEditing = (set: boolean) => (this._keyframeEditing = set);
+    @action setKeyFrameEditing = (set: boolean) => {
+        this._keyframeEditing = set;
+    };
     getKeyFrameEditing = () => this._keyframeEditing;
     onBrowseClickHandler = () => this._props.onBrowseClickScript?.() || ScriptCast(this.layoutDoc.onBrowseClick);
     onChildClickHandler = () => this._props.childClickScript || ScriptCast(this.Document.onChildClick);
@@ -256,7 +257,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     // this search order, for example, allows icons of cropped images to find the panx/pany/zoom on the cropped image's data doc instead of the usual layout doc because the zoom/panX/panY define the cropped image
     panX = () => this.freeformData()?.bounds.cx ?? NumCast(this.Document[this.panXFieldKey], NumCast(Cast(this.Document.resolvedDataDoc, Doc, null)?.freeform_panX, 1));
     panY = () => this.freeformData()?.bounds.cy ?? NumCast(this.Document[this.panYFieldKey], NumCast(Cast(this.Document.resolvedDataDoc, Doc, null)?.freeform_panY, 1));
-    zoomScaling = () => this.freeformData()?.scale ?? NumCast(Doc.Layout(this.Document)[this.scaleFieldKey], 1); //, NumCast(DocCast(this.Document.resolvedDataDoc)?.[this.scaleFieldKey], 1));
+    zoomScaling = () => this.freeformData()?.scale ?? NumCast(Doc.Layout(this.Document)[this.scaleFieldKey], 1); // , NumCast(DocCast(this.Document.resolvedDataDoc)?.[this.scaleFieldKey], 1));
     PanZoomCenterXf = () => (this._props.isAnnotationOverlay && this.zoomScaling() === 1 ? `` : `translate(${this.centeringShiftX}px, ${this.centeringShiftY}px) scale(${this.zoomScaling()}) translate(${-this.panX()}px, ${-this.panY()}px)`);
     ScreenToContentsXf = () => this.screenToFreeformContentsXf.copy();
     getActiveDocuments = () => this.childLayoutPairs.filter(pair => this.isCurrent(pair.layout)).map(pair => pair.layout);
@@ -272,7 +273,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     addDocument = (newBox: Doc | Doc[]) => {
         let retVal = false;
         if (newBox instanceof Doc) {
-            if ((retVal = this._props.addDocument?.(newBox) || false)) {
+            retVal = this._props.addDocument?.(newBox) || false;
+            if (retVal) {
                 this.bringToFront(newBox);
                 this.updateCluster(newBox);
             }
@@ -282,15 +284,17 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         }
         if (retVal) {
             const newBoxes = newBox instanceof Doc ? [newBox] : newBox;
-            for (const newBox of newBoxes) {
-                if (newBox.activeFrame !== undefined) {
-                    const vals = CollectionFreeFormDocumentView.animFields.map(field => newBox[field.key]);
-                    CollectionFreeFormDocumentView.animFields.forEach(field => delete newBox[`${field.key}_indexed`]);
-                    CollectionFreeFormDocumentView.animFields.forEach(field => delete newBox[field.key]);
-                    delete newBox.activeFrame;
-                    CollectionFreeFormDocumentView.animFields.forEach((field, i) => field.key !== 'opacity' && (newBox[field.key] = vals[i]));
+            newBoxes.forEach(box => {
+                if (box.activeFrame !== undefined) {
+                    const vals = CollectionFreeFormDocumentView.animFields.map(field => box[field.key]);
+                    CollectionFreeFormDocumentView.animFields.forEach(field => delete box[`${field.key}_indexed`]);
+                    CollectionFreeFormDocumentView.animFields.forEach(field => delete box[field.key]);
+                    delete box.activeFrame;
+                    CollectionFreeFormDocumentView.animFields.forEach((field, i) => {
+                        field.key !== 'opacity' && (box[field.key] = vals[i]);
+                    });
                 }
-            }
+            });
             if (this.Document._currentFrame !== undefined && !this._props.isAnnotationOverlay) {
                 CollectionFreeFormDocumentView.setupKeyframes(newBoxes, NumCast(this.Document._currentFrame), true);
             }
@@ -313,14 +317,19 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     };
 
     focus = (anchor: Doc, options: FocusViewOptions) => {
-        if (this._lightboxDoc) return;
+        if (this._lightboxDoc) return undefined;
         if (anchor === this.Document) {
             // if (options.willZoomCentered && options.zoomScale) {
             //     this.fitContentOnce();
             //     options.didMove = true;
             // }
         }
-        if (anchor.type !== DocumentType.CONFIG && !DocListCast(this.Document[this.fieldKey ?? Doc.LayoutFieldKey(this.Document)]).includes(anchor) && !this.childLayoutPairs.map(pair => pair.layout).includes(anchor)) return;
+        // prettier-ignore
+        if (anchor.type !== DocumentType.CONFIG && 
+            !DocListCast(this.Document[this.fieldKey ?? Doc.LayoutFieldKey(this.Document)]).includes(anchor) && //
+            !this.childLayoutPairs.map(pair => pair.layout).includes(anchor)) { 
+            return undefined;
+        }
         const xfToCollection = options?.docTransform ?? Transform.Identity();
         const savedState = { panX: NumCast(this.Document[this.panXFieldKey]), panY: NumCast(this.Document[this.panYFieldKey]), scale: options?.willZoomCentered ? this.Document[this.scaleFieldKey] : undefined };
         const cantTransform = this.fitContentsToBox || ((this.Document.isGroup || this.layoutDoc._lockedTransform) && !LightboxView.LightboxDoc);
@@ -336,12 +345,16 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
             this.setPan(panX, panY, focusTime, true); // docs that are floating in their collection can't be panned to from their collection -- need to propagate the pan to a parent freeform somehow
             return focusTime;
         }
+        return undefined;
     };
 
     getView = async (doc: Doc, options: FocusViewOptions): Promise<Opt<DocumentView>> =>
         new Promise<Opt<DocumentView>>(res => {
             if (doc.hidden && this._lightboxDoc !== doc) options.didMove = !(doc.hidden = false);
-            if (doc === this.Document) return res(this.DocumentView?.());
+            if (doc === this.Document) {
+                res(this.DocumentView?.());
+                return;
+            }
             const findDoc = (finish: (dv: DocumentView) => void) => DocumentManager.Instance.AddViewRenderedCb(doc, dv => finish(dv));
             findDoc(dv => res(dv));
         });
@@ -357,7 +370,9 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
             .map(pair => pair.layout)
             .slice()
             .sort((doc1, doc2) => NumCast(doc1.zIndex) - NumCast(doc2.zIndex));
-        zsorted.forEach((doc, index) => (doc.zIndex = doc.stroke_isInkMask ? 5000 : index + 1));
+        zsorted.forEach((doc, index) => {
+            doc.zIndex = doc.stroke_isInkMask ? 5000 : index + 1;
+        });
         const dvals = CollectionFreeFormDocumentView.getValues(refDoc, NumCast(refDoc.activeFrame, 1000));
         const dropPos = this.Document._currentFrame !== undefined ? [NumCast(dvals.x), NumCast(dvals.y)] : [NumCast(refDoc.x), NumCast(refDoc.y)];
 
@@ -410,7 +425,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
             let added = false;
             //  do nothing if link is dropped into any freeform view parent of dragged document
             const source = Docs.Create.TextDocument('', { _width: 200, _height: 75, x, y, title: 'dropped annotation' });
-            added = this._props.addDocument?.(source) ? true : false;
+            added = !!this._props.addDocument?.(source);
             de.complete.linkDocument = DocUtils.MakeLink(linkDragData.linkSourceGetAnchor(), source, { link_relationship: 'annotated by:annotation of' }); // TODODO this is where in text links get passed
             if (de.complete.linkDocument) {
                 de.complete.linkDocument.layout_isSvg = true;
@@ -425,8 +440,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
 
     onInternalDrop = (e: Event, de: DragManager.DropEvent) => {
         if (de.complete.annoDragData?.dragDocument && super.onInternalDrop(e, de)) return this.internalAnchorAnnoDrop(e, de, de.complete.annoDragData);
-        else if (de.complete.linkDragData) return this.internalLinkDrop(e, de, de.complete.linkDragData);
-        else if (de.complete.docDragData?.droppedDocuments.length) return this.internalDocDrop(e, de, de.complete.docDragData);
+        if (de.complete.linkDragData) return this.internalLinkDrop(e, de, de.complete.linkDragData);
+        if (de.complete.docDragData?.droppedDocuments.length) return this.internalDocDrop(e, de, de.complete.docDragData);
         return false;
     };
 
@@ -487,8 +502,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     }
 
     @action
-    updateClusters(_freeform_useClusters: boolean) {
-        this.Document._freeform_useClusters = _freeform_useClusters;
+    updateClusters(useClusters: boolean) {
+        this.Document._freeform_useClusters = useClusters;
         this._clusterSets.length = 0;
         this.childLayoutPairs.map(pair => pair.layout).map(c => this.updateCluster(c));
     }
@@ -498,12 +513,14 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         const childLayouts = this.childLayoutPairs.map(pair => pair.layout);
         if (this.Document._freeform_useClusters) {
             const docFirst = docs[0];
-            docs.map(doc => this._clusterSets.map(set => Doc.IndexOf(doc, set) !== -1 && set.splice(Doc.IndexOf(doc, set), 1)));
+            docs.forEach(doc => this._clusterSets.map(set => Doc.IndexOf(doc, set) !== -1 && set.splice(Doc.IndexOf(doc, set), 1)));
             const preferredInd = NumCast(docFirst.layout_cluster);
-            docs.map(doc => (doc.layout_cluster = -1));
+            docs.forEach(doc => {
+                doc.layout_cluster = -1;
+            });
             docs.map(doc =>
                 this._clusterSets.map((set, i) =>
-                    set.map(member => {
+                    set.forEach(member => {
                         if (docFirst.layout_cluster === -1 && Doc.IndexOf(member, childLayouts) !== -1 && CollectionFreeFormView.overlapping(doc, member, this._clusterDistance)) {
                             docFirst.layout_cluster = i;
                         }
@@ -518,19 +535,21 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
             ) {
                 docFirst.layout_cluster = preferredInd;
             }
-            this._clusterSets.map((set, i) => {
+            this._clusterSets.forEach((set, i) => {
                 if (docFirst.layout_cluster === -1 && !set.filter(member => Doc.IndexOf(member, childLayouts) !== -1).length) {
                     docFirst.layout_cluster = i;
                 }
             });
             if (docFirst.layout_cluster === -1) {
-                docs.map(doc => {
+                docs.forEach(doc => {
                     doc.layout_cluster = this._clusterSets.length;
                     this._clusterSets.push([doc]);
                 });
             } else if (this._clusterSets.length) {
                 for (let i = this._clusterSets.length; i <= NumCast(docFirst.layout_cluster); i++) !this._clusterSets[i] && this._clusterSets.push([]);
-                docs.map(doc => this._clusterSets[(doc.layout_cluster = NumCast(docFirst.layout_cluster))].push(doc));
+                docs.forEach(doc => {
+                    this._clusterSets[(doc.layout_cluster = NumCast(docFirst.layout_cluster))].push(doc);
+                });
             }
             childLayouts.map(child => !this._clusterSets.some((set, i) => Doc.IndexOf(child, set) !== -1 && child.layout_cluster === i) && this.updateCluster(child));
         }
@@ -573,17 +592,21 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         if (doc && this.childDocList?.includes(doc))
             switch (property.split(':')[0]) {
                 case StyleProp.BackgroundColor:
-                    const cluster = NumCast(doc?.layout_cluster);
-                    if (this.Document._freeform_useClusters && doc?.type !== DocumentType.IMG) {
-                        if (this._clusterSets.length <= cluster) {
-                            setTimeout(() => doc && this.updateCluster(doc));
-                        } else {
-                            // choose a cluster color from a palette
-                            const colors = ['#da42429e', '#31ea318c', 'rgba(197, 87, 20, 0.55)', '#4a7ae2c4', 'rgba(216, 9, 255, 0.5)', '#ff7601', '#1dffff', 'yellow', 'rgba(27, 130, 49, 0.55)', 'rgba(0, 0, 0, 0.268)'];
-                            styleProp = colors[cluster % colors.length];
-                            const set = this._clusterSets[cluster]?.filter(s => s.backgroundColor);
-                            // override the cluster color with an explicitly set color on a non-background document.  then override that with an explicitly set color on a background document
-                            set?.map(s => (styleProp = StrCast(s.backgroundColor)));
+                    {
+                        const cluster = NumCast(doc?.layout_cluster);
+                        if (this.Document._freeform_useClusters && doc?.type !== DocumentType.IMG) {
+                            if (this._clusterSets.length <= cluster) {
+                                setTimeout(() => doc && this.updateCluster(doc));
+                            } else {
+                                // choose a cluster color from a palette
+                                const colors = ['#da42429e', '#31ea318c', 'rgba(197, 87, 20, 0.55)', '#4a7ae2c4', 'rgba(216, 9, 255, 0.5)', '#ff7601', '#1dffff', 'yellow', 'rgba(27, 130, 49, 0.55)', 'rgba(0, 0, 0, 0.268)'];
+                                styleProp = colors[cluster % colors.length];
+                                const set = this._clusterSets[cluster]?.filter(s => s.backgroundColor);
+                                // override the cluster color with an explicitly set color on a non-background document.  then override that with an explicitly set color on a background document
+                                set?.forEach(s => {
+                                    styleProp = StrCast(s.backgroundColor);
+                                });
+                            }
                         }
                     }
                     break;
@@ -591,6 +614,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                     if (doc && this.Document._currentFrame !== undefined) {
                         return CollectionFreeFormDocumentView.getStringValues(doc, NumCast(this.Document._currentFrame))?.fillColor;
                     }
+                    break;
+                default:
             }
         return styleProp;
     };
@@ -626,9 +651,10 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                     case InkTool.None:
                         if (!(this._props.layoutEngine?.() || StrCast(this.layoutDoc._layoutEngine))) {
                             this._hitCluster = this.pickCluster(this.screenToFreeformContentsXf.transformPoint(e.clientX, e.clientY));
-                            setupMoveUpEvents(this, e, this.onPointerMove, emptyFunction, emptyFunction, this._hitCluster !== -1 ? true : false, false);
+                            setupMoveUpEvents(this, e, this.onPointerMove, emptyFunction, emptyFunction, this._hitCluster !== -1, false);
                         }
                         break;
+                    default:
                 }
             }
         }
@@ -639,13 +665,34 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     @undoBatch
     onGesture = (e: Event, ge: GestureUtils.GestureEvent) => {
         switch (ge.gesture) {
-            default:
+            // case Gestures.Rectangle:
+            //     {
+            //         const strokes = this.getActiveDocuments()
+            //             .filter(doc => doc.type === DocumentType.INK)
+            //             .map(i => {
+            //                 const d = Cast(i.stroke, InkField);
+            //                 const x = NumCast(i.x) - Math.min(...(d?.inkData.map(pd => pd.X) ?? [0]));
+            //                 const y = NumCast(i.y) - Math.min(...(d?.inkData.map(pd => pd.Y) ?? [0]));
+            //                 return !d ? [] : d.inkData.map(pd => ({ X: x + pd.X, Y: y + pd.Y }));
+            //             });
+
+            //         CognitiveServices.Inking.Appliers.InterpretStrokes(strokes).then(results => {});
+            //     }
+            //     break;
+            case Gestures.Text:
+                if (ge.text) {
+                    const B = this.screenToFreeformContentsXf.transformPoint(ge.points[0].X, ge.points[0].Y);
+                    this.addDocument(Docs.Create.TextDocument(ge.text, { title: ge.text, x: B[0], y: B[1] }));
+                    e.stopPropagation();
+                }
+                break;
             case Gestures.Line:
             case Gestures.Circle:
             case Gestures.Rectangle:
             case Gestures.Triangle:
             case Gestures.Stroke:
-                const points = ge.points;
+            default: {
+                const { points } = ge;
                 const B = this.screenToFreeformContentsXf.transformBounds(ge.bounds.left, ge.bounds.top, ge.bounds.width, ge.bounds.height);
                 const inkWidth = ActiveInkWidth() * this.ScreenToLocalBoxXf().Scale;
                 const inkDoc = Docs.Create.InkDocument(
@@ -664,29 +711,11 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                 }
                 this.addDocument(inkDoc);
                 e.stopPropagation();
-                break;
-            case Gestures.Rectangle:
-                const strokes = this.getActiveDocuments()
-                    .filter(doc => doc.type === DocumentType.INK)
-                    .map(i => {
-                        const d = Cast(i.stroke, InkField);
-                        const x = NumCast(i.x) - Math.min(...(d?.inkData.map(pd => pd.X) ?? [0]));
-                        const y = NumCast(i.y) - Math.min(...(d?.inkData.map(pd => pd.Y) ?? [0]));
-                        return !d ? [] : d.inkData.map(pd => ({ X: x + pd.X, Y: y + pd.Y }));
-                    });
-
-                CognitiveServices.Inking.Appliers.InterpretStrokes(strokes).then(results => {});
-                break;
-            case Gestures.Text:
-                if (ge.text) {
-                    const B = this.screenToFreeformContentsXf.transformPoint(ge.points[0].X, ge.points[0].Y);
-                    this.addDocument(Docs.Create.TextDocument(ge.text, { title: ge.text, x: B[0], y: B[1] }));
-                    e.stopPropagation();
-                }
+            }
         }
     };
     @action
-    onEraserUp = (e: PointerEvent): void => {
+    onEraserUp = (): void => {
         this._deleteList.forEach(ink => ink._props.removeDocument?.(ink.Document));
         this._deleteList = [];
         this._batch?.end();
@@ -792,7 +821,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         return this.childDocs
             .map(doc => DocumentManager.Instance.getDocumentView(doc, this.DocumentView?.()))
             .filter(inkView => inkView?.ComponentView instanceof InkingStroke)
-            .map(inkView => ({ inkViewBounds: inkView!.getBounds, inkStroke: inkView!.ComponentView as InkingStroke, inkView: inkView! }))
+            .map(inkView => inkView!)
+            .map(inkView => ({ inkViewBounds: inkView.getBounds, inkStroke: inkView.ComponentView as InkingStroke, inkView }))
             .filter(
                 ({ inkViewBounds }) =>
                     inkViewBounds && // bounding box of eraser segment and ink stroke overlap
@@ -807,7 +837,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                     // Convert from screen space to ink space for the intersection.
                     const prevPointInkSpace = inkStroke.ptFromScreen(lastPoint);
                     const currPointInkSpace = inkStroke.ptFromScreen(currPoint);
-                    for (var i = 0; i < inkData.length - 3; i += 4) {
+                    for (let i = 0; i < inkData.length - 3; i += 4) {
                         const rawIntersects = InkField.Segment(inkData, i).intersects({
                             // compute all unique intersections
                             p1: { x: prevPointInkSpace.X, y: prevPointInkSpace.Y },
@@ -833,16 +863,17 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     @action
     segmentInkStroke = (ink: DocumentView, excludeT: number): Segment[] => {
         const segments: Segment[] = [];
-        var segment: Segment = [];
-        var startSegmentT = 0;
+        let segment: Segment = [];
+        let startSegmentT = 0;
         const { inkData } = (ink?.ComponentView as InkingStroke).inkScaledData();
         // This iterates through all segments of the curve and splits them where they intersect another curve.
         // if 'excludeT' is specified, then any segment containing excludeT will be skipped (ie, deleted)
-        for (var i = 0; i < inkData.length - 3; i += 4) {
+        for (let i = 0; i < inkData.length - 3; i += 4) {
             const inkSegment = InkField.Segment(inkData, i);
             // Getting all t-value intersections of the current curve with all other curves.
             const tVals = this.getInkIntersections(i, ink, inkSegment).sort();
             if (tVals.length) {
+                // eslint-disable-next-line no-loop-func
                 tVals.forEach((t, index) => {
                     const docCurveTVal = t + Math.floor(i / 4);
                     if (excludeT < startSegmentT || excludeT > docCurveTVal) {
@@ -895,9 +926,10 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                 const { inkData: otherInkData } = otherInk?.inkScaledData() ?? { inkData: [] };
                 const otherScreenPts = otherInkData.map(point => otherInk.ptToScreen(point));
                 const otherCtrlPts = otherScreenPts.map(spt => (ink.ComponentView as InkingStroke).ptFromScreen(spt));
-                for (var j = 0; j < otherCtrlPts.length - 3; j += 4) {
+                for (let j = 0; j < otherCtrlPts.length - 3; j += 4) {
                     const neighboringSegment = i === j || i === j - 4 || i === j + 4;
                     // Ensuring that the curve intersected by the eraser is not checked for further ink intersections.
+                    // eslint-disable-next-line no-continue
                     if (ink?.Document === otherInk.Document && neighboringSegment) continue;
 
                     const otherCurve = new Bezier(otherCtrlPts.slice(j, j + 4).map(p => ({ x: p.X, y: p.Y })));
@@ -908,7 +940,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                     if (apt.d !== undefined && apt.d < 1 && apt.t !== undefined && !tVals.includes(apt.t)) {
                         tVals.push(apt.t);
                     }
-                    this.bintersects(curve, otherCurve).forEach((val: string | number, i: number) => {
+                    this.bintersects(curve, otherCurve).forEach((val: string | number /* , i: number */) => {
                         // Converting the Bezier.js Split type to a t-value number.
                         const t = +val.toString().split('/')[0];
                         if (i % 2 === 0 && !tVals.includes(t)) tVals.push(t); // bcz: Hack!  don't know why but intersection points are doubled from bezier.js (but not identical).
@@ -971,8 +1003,9 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                     this.scrollPan({ deltaX: -deltaX * this.screenToFreeformContentsXf.Scale, deltaY: e.shiftKey ? 0 : -deltaY * this.screenToFreeformContentsXf.Scale });
                     break;
                 }
-            default:
+            // eslint-disable-next-line no-fallthrough
             case freeformScrollMode.Zoom:
+            default:
                 if ((e.ctrlKey || !scrollable) && this._props.isContentActive()) {
                     this.zoom(e.clientX, e.clientY, Math.max(-1, Math.min(1, e.deltaY))); // if (!this._props.isAnnotationOverlay) // bcz: do we want to zoom in on images/videos/etc?
                     // e.preventDefault();
@@ -982,7 +1015,9 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     };
 
     @action
-    setPan(panX: number, panY: number, panTime: number = 0, clamp: boolean = false) {
+    setPan(panXIn: number, panYIn: number, panTime: number = 0, clamp: boolean = false) {
+        let panX = panXIn;
+        let panY = panYIn;
         // this is the easiest way to do this -> will talk with Bob about using mobx to do this to remove this line of code.
         if (Doc.UserDoc()?.presentationMode === 'watching') ReplayMovements.Instance.pauseFromInteraction();
 
@@ -1034,11 +1069,12 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                 scale * NumCast(this.dataDoc._panY_max, nativeHeight) +
                 (!this._props.getScrollHeight?.() ? fitYscroll : 0); // when not zoomed, scrolling is handled via a scrollbar, not panning
             let newPanY = Math.max(minPanY, Math.min(maxPanY, panY));
-            if (false && NumCast(this.layoutDoc.layout_scrollTop) && NumCast(this.layoutDoc._freeform_scale, minScale) !== minScale) {
-            } else if (fitYscroll > 2 && this.layoutDoc.layout_scrollTop === undefined && NumCast(this.layoutDoc._freeform_scale, minScale) === minScale) {
-                const maxPanY = minPanY + fitYscroll;
-                const relTop = (panY - minPanY) / (maxPanY - minPanY);
-                setTimeout(() => (this.layoutDoc.layout_scrollTop = relTop * maxScrollTop), 10);
+            if (fitYscroll > 2 && this.layoutDoc.layout_scrollTop === undefined && NumCast(this.layoutDoc._freeform_scale, minScale) === minScale) {
+                const maxPanScrollY = minPanY + fitYscroll;
+                const relTop = (panY - minPanY) / (maxPanScrollY - minPanY);
+                setTimeout(() => {
+                    this.layoutDoc.layout_scrollTop = relTop * maxScrollTop;
+                }, 10);
                 newPanY = minPanY;
             }
             !this.Document._verticalScroll && (this.Document[this.panXFieldKey] = this.isAnnotationOverlay ? newPanX : panX);
@@ -1090,7 +1126,9 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         this._panZoomTransition = transitionTime;
         this._panZoomTransitionTimer && clearTimeout(this._panZoomTransitionTimer);
         this._panZoomTransitionTimer = setTimeout(
-            action(() => (this._panZoomTransition = 0)),
+            action(() => {
+                this._panZoomTransition = 0;
+            }),
             transitionTime
         );
     };
@@ -1173,6 +1211,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
             e.stopPropagation?.();
             return this.createTextDocCopy(fieldProps, !e.altKey && e.key !== 'Tab');
         }
+        return undefined;
     };
     @computed get childPointerEvents() {
         const engine = this._props.layoutEngine?.() || StrCast(this.Document._layoutEngine);
@@ -1194,6 +1233,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         const childData = entry.pair.data;
         return (
             <CollectionFreeFormDocumentView
+                // eslint-disable-next-line react/jsx-props-no-spreading
                 {...OmitKeys(entry, ['replica', 'pair']).omit}
                 key={childLayout[Id] + (entry.replica || '')}
                 Document={childLayout}
@@ -1205,7 +1245,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                 isGroupActive={this._props.isGroupActive}
                 renderDepth={this._props.renderDepth + 1}
                 hideDecorations={BoolCast(childLayout._layout_isSvg && childLayout.type === DocumentType.LINK)}
-                suppressSetHeight={this.layoutEngine ? true : false}
+                suppressSetHeight={!!this.layoutEngine}
                 RenderCutoffProvider={this.renderCutoffProvider}
                 CollectionFreeFormView={this}
                 LayoutTemplate={childLayout.z ? undefined : this._props.childLayoutTemplate}
@@ -1239,37 +1279,42 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
             />
         );
     }
-    addDocTab = action((doc: Doc, where: OpenWhere) => {
-        if (this._props.isAnnotationOverlay) return this._props.addDocTab(doc, where);
+    addDocTab = action((docsIn: Doc | Doc[], where: OpenWhere) => {
+        const docs = docsIn instanceof Doc ? [docsIn] : docsIn;
+        if (this._props.isAnnotationOverlay) return this._props.addDocTab(docs, where);
         switch (where) {
             case OpenWhere.inParent:
-                return this._props.addDocument?.(doc) || false;
-            case OpenWhere.inParentFromScreen:
-                const docContext = DocCast((doc instanceof Doc ? doc : doc?.[0])?.embedContainer);
+                return this._props.addDocument?.(docs) || false;
+            case OpenWhere.inParentFromScreen: {
+                const docContext = DocCast(docs[0]?.embedContainer);
                 return (
                     (this.addDocument?.(
-                        (doc instanceof Doc ? [doc] : doc).map(doc => {
-                            const pt = this.screenToFreeformContentsXf.transformPoint(NumCast(doc.x), NumCast(doc.y));
-                            doc.x = pt[0];
-                            doc.y = pt[1];
+                        (docs instanceof Doc ? [docs] : docs).map(doc => {
+                            [doc.x, doc.y] = this.screenToFreeformContentsXf.transformPoint(NumCast(doc.x), NumCast(doc.y));
                             return doc;
                         })
                     ) &&
                         (!docContext || this._props.removeDocument?.(docContext))) ||
                     false
                 );
+            }
             case undefined:
             case OpenWhere.lightbox:
-                if (this.layoutDoc._isLightbox) {
-                    this._lightboxDoc = doc;
-                    return true;
-                }
-                if (doc === this.Document || this.childDocList?.includes(doc) || this.childLayoutPairs.map(pair => pair.layout)?.includes(doc)) {
-                    if (doc.hidden) doc.hidden = false;
-                    return true;
+                {
+                    const firstDoc = docs[0];
+                    if (this.layoutDoc._isLightbox) {
+                        this._lightboxDoc = firstDoc;
+                        return true;
+                    }
+                    if (firstDoc === this.Document || this.childDocList?.includes(firstDoc) || this.childLayoutPairs.map(pair => pair.layout)?.includes(firstDoc)) {
+                        if (firstDoc.hidden) firstDoc.hidden = false;
+                        return true;
+                    }
                 }
+                break;
+            default:
         }
-        return this._props.addDocTab(doc, where);
+        return this._props.addDocTab(docs, where);
     });
     @observable _lightboxDoc: Opt<Doc> = undefined;
 
@@ -1314,9 +1359,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         e.stopPropagation();
     };
 
-    viewDefsToJSX = (views: ViewDefBounds[]) => {
-        return !Array.isArray(views) ? [] : views.filter(ele => this.viewDefToJSX(ele)).map(ele => this.viewDefToJSX(ele)!);
-    };
+    viewDefsToJSX = (views: ViewDefBounds[]) => (!Array.isArray(views) ? [] : views.filter(ele => this.viewDefToJSX(ele)).map(ele => this.viewDefToJSX(ele)!));
 
     viewDefToJSX(viewDef: ViewDefBounds): Opt<ViewDefResult> {
         const { x, y, z } = viewDef;
@@ -1337,7 +1380,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                       ),
                       bounds: viewDef,
                   };
-        } else if (viewDef.type === 'div') {
+        }
+        if (viewDef.type === 'div') {
             return [x, y].some(val => val === undefined)
                 ? undefined
                 : {
@@ -1353,9 +1397,11 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                       bounds: viewDef,
                   };
         }
+        return undefined;
     }
 
     renderCutoffProvider = computedFn(
+        // eslint-disable-next-line prefer-arrow-callback
         function renderCutoffProvider(this: any, doc: Doc) {
             return this.Document.isTemplateDoc ? false : !this._renderCutoffData.get(doc[Id] + '');
         }.bind(this)
@@ -1369,7 +1415,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     }
 
     doFreeformLayout(poolData: Map<string, PoolData>) {
-        this.childLayoutPairs.filter(pair => this.isCurrent(pair.layout)).map((pair, i) => poolData.set(pair.layout[Id], this.getCalculatedPositions(pair)));
+        this.childLayoutPairs.filter(pair => this.isCurrent(pair.layout)).map(pair => poolData.set(pair.layout[Id], this.getCalculatedPositions(pair)));
         return [] as ViewDefResult[];
     }
 
@@ -1385,6 +1431,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
             case computeTimelineLayout.name:  return { newPool, computedElementData: this.doEngineLayout(newPool, computeTimelineLayout) };
             case computePivotLayout.name:     return { newPool, computedElementData: this.doEngineLayout(newPool, computePivotLayout) };
             case computeStarburstLayout.name: return { newPool, computedElementData: this.doEngineLayout(newPool, computeStarburstLayout) };
+            default:
         }
         return { newPool, computedElementData: this.doFreeformLayout(newPool) };
     }
@@ -1469,7 +1516,9 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
 
                 this._disposers.pointerevents = reaction(
                     () => this.childPointerEvents,
-                    pointerevents => (this._childPointerEvents = pointerevents as any),
+                    pointerevents => {
+                        this._childPointerEvents = pointerevents as any;
+                    },
                     { fireImmediately: true }
                 );
 
@@ -1643,7 +1692,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         }
     };
 
-    onContextMenu = (e: React.MouseEvent) => {
+    onContextMenu = () => {
         if (this._props.isAnnotationOverlay || !ContextMenu.Instance) return;
 
         const appearance = ContextMenu.Instance.findByDescription('Appearance...');
@@ -1654,7 +1703,15 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
             !appearance && ContextMenu.Instance.addItem({ description: 'Appearance...', subitems: appearanceItems, icon: 'eye' });
             return;
         }
-        !Doc.noviceMode && Doc.UserDoc().defaultTextLayout && appearanceItems.push({ description: 'Reset default note style', event: () => (Doc.UserDoc().defaultTextLayout = undefined), icon: 'eye' });
+        !Doc.noviceMode &&
+            Doc.UserDoc().defaultTextLayout &&
+            appearanceItems.push({
+                description: 'Reset default note style',
+                event: () => {
+                    Doc.UserDoc().defaultTextLayout = undefined;
+                },
+                icon: 'eye',
+            });
         appearanceItems.push({ description: `Pin View`, event: () => this._props.pinToPres(this.Document, { pinViewport: MarqueeView.CurViewBounds(this.dataDoc, this._props.PanelWidth(), this._props.PanelHeight()) }), icon: 'map-pin' });
         !Doc.noviceMode && appearanceItems.push({ description: `update icon`, event: this.updateIcon, icon: 'compress-arrows-alt' });
         this._props.renderDepth && appearanceItems.push({ description: 'Ungroup collection', event: this.promoteCollection, icon: 'table' });
@@ -1669,8 +1726,21 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         const optionItems = options && 'subitems' in options ? options.subitems : [];
         !this._props.isAnnotationOverlay &&
             !Doc.noviceMode &&
-            optionItems.push({ description: (this._showAnimTimeline ? 'Close' : 'Open') + ' Animation Timeline', event: action(() => (this._showAnimTimeline = !this._showAnimTimeline)), icon: 'eye' });
-        this._props.renderDepth && optionItems.push({ description: 'Use Background Color as Default', event: () => (Cast(Doc.UserDoc().emptyCollection, Doc, null).backgroundColor = StrCast(this.layoutDoc.backgroundColor)), icon: 'palette' });
+            optionItems.push({
+                description: (this._showAnimTimeline ? 'Close' : 'Open') + ' Animation Timeline',
+                event: action(() => {
+                    this._showAnimTimeline = !this._showAnimTimeline;
+                }),
+                icon: 'eye',
+            });
+        this._props.renderDepth &&
+            optionItems.push({
+                description: 'Use Background Color as Default',
+                event: () => {
+                    Cast(Doc.UserDoc().emptyCollection, Doc, null).backgroundColor = StrCast(this.layoutDoc.backgroundColor);
+                },
+                icon: 'palette',
+            });
         this._props.renderDepth && optionItems.push({ description: 'Fit Content Once', event: this.fitContentOnce, icon: 'object-group' });
         if (!Doc.noviceMode) {
             optionItems.push({ description: (!Doc.NativeWidth(this.layoutDoc) || !Doc.NativeHeight(this.layoutDoc) ? 'Freeze' : 'Unfreeze') + ' Aspect', event: this.toggleNativeDimensions, icon: 'snowflake' });
@@ -1750,7 +1820,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         );
     }
 
-    showPresPaths = () => CollectionFreeFormView.ShowPresPaths;
+    showPresPaths = () => SnappingManager.ShowPresPaths;
     brushedView = () => this._brushedView;
     gridColor = () =>
         DashColor(lightOrDark(this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BackgroundColor)))
@@ -1908,7 +1978,10 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                 ) : (
                     <>
                         {this._firstRender ? this.placeholder : this.marqueeView}
-                        {this._props.noOverlay ? null : <CollectionFreeFormOverlayView elements={this.elementFunc} />}
+                        {
+                            // eslint-disable-next-line no-use-before-define
+                            this._props.noOverlay ? null : <CollectionFreeFormOverlayView elements={this.elementFunc} />
+                        }
                         {!this.GroupChildDrag ? null : <div className="collectionFreeForm-groupDropper" />}
                     </>
                 )}
@@ -1920,7 +1993,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
 @observer
 class CollectionFreeFormOverlayView extends React.Component<{ elements: () => ViewDefResult[] }> {
     render() {
-        // eslint-disable-next-line react/destructuring-assignment
         return this.props.elements().filter(ele => ele.bounds?.z).map(ele => ele.ele); // prettier-ignore
     }
 }
@@ -1959,6 +2031,7 @@ ScriptingGlobals.add(function curKeyFrame(readOnly: boolean) {
     const selView = SelectionManager.Views;
     if (readOnly) return selView[0].ComponentView?.getKeyFrameEditing?.() ? Colors.MEDIUM_BLUE : 'transparent';
     runInAction(() => selView[0].ComponentView?.setKeyFrameEditing?.(!selView[0].ComponentView?.getKeyFrameEditing?.()));
+    return undefined;
 });
 // eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function pinWithView(pinContent: boolean) {
@@ -1989,7 +2062,7 @@ ScriptingGlobals.add(function datavizFromSchema() {
         if (!view.layoutDoc.schema_columnKeys) {
             view.layoutDoc.schema_columnKeys = new List<string>(['title', 'type', 'author', 'author_date']);
         }
-        const keys = Cast(view.layoutDoc.schema_columnKeys, listSpec('string'))?.filter(key => key != 'text');
+        const keys = Cast(view.layoutDoc.schema_columnKeys, listSpec('string'))?.filter(key => key !== 'text');
         if (!keys) return;
 
         const children = DocListCast(view.Document[Doc.LayoutFieldKey(view.Document)]);
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index b03e435ce..2f9cc49e0 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -196,7 +196,7 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
         const eachCell = ns.join('\t').split('\t');
         let eachRow = [];
         for (let i = 1; i < eachCell.length; i++) {
-            eachRow.push(eachCell[i].replace(/\,/g, ''));
+            eachRow.push(eachCell[i].replace(/,/g, ''));
             if (i % headers.length === 0) {
                 csvRows.push(eachRow);
                 eachRow = [];
@@ -377,7 +377,7 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
     });
 
     @undoBatch
-    pileup = action((e: KeyboardEvent | React.PointerEvent | undefined) => {
+    pileup = action(() => {
         const selected = this.marqueeSelect(false);
         SelectionManager.DeselectAll();
         selected.forEach(d => this._props.removeDocument?.(d));
@@ -485,7 +485,7 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
     });
 
     @undoBatch
-    summary = action((e: KeyboardEvent | React.PointerEvent | undefined) => {
+    summary = action(() => {
         const selected = this.marqueeSelect(false).map(d => {
             this._props.removeDocument?.(d);
             d.x = NumCast(d.x) - this.Bounds.left;
@@ -530,8 +530,8 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
             (e as any).propagationIsStopped = true;
             if (e.key === 'g') this.collection(e, true);
             if (e.key === 'c' || e.key === 't') this.collection(e);
-            if (e.key === 's' || e.key === 'S') this.summary(e);
-            if (e.key === 'p') this.pileup(e);
+            if (e.key === 's' || e.key === 'S') this.summary();
+            if (e.key === 'p') this.pileup();
             this.cleanupInteractions(false);
         }
         if (e.key === 'r' || e.key === ' ') {
diff --git a/src/client/views/collections/collectionSchema/SchemaTableCell.tsx b/src/client/views/collections/collectionSchema/SchemaTableCell.tsx
index 08eb35586..ee30006ae 100644
--- a/src/client/views/collections/collectionSchema/SchemaTableCell.tsx
+++ b/src/client/views/collections/collectionSchema/SchemaTableCell.tsx
@@ -1,3 +1,6 @@
+/* eslint-disable jsx-a11y/alt-text */
+/* eslint-disable react/jsx-props-no-spreading */
+/* eslint-disable no-use-before-define */
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { Popup, Size, Type } from 'browndash-components';
 import { action, computed, makeObservable, observable } from 'mobx';
@@ -26,7 +29,7 @@ import { EditableView } from '../../EditableView';
 import { ObservableReactComponent } from '../../ObservableReactComponent';
 import { DefaultStyleProvider } from '../../StyleProvider';
 import { Colors } from '../../global/globalEnums';
-import { OpenWhere, returnEmptyDocViewList } from '../../nodes/DocumentView';
+import { returnEmptyDocViewList } from '../../nodes/DocumentView';
 import { FieldViewProps } from '../../nodes/FieldView';
 import { KeyValueBox } from '../../nodes/KeyValueBox';
 import { FormattedTextBox } from '../../nodes/formattedText/FormattedTextBox';
@@ -64,8 +67,8 @@ export class SchemaTableCell extends ObservableReactComponent<SchemaTableCellPro
         makeObservable(this);
     }
 
-    static addFieldDoc = (doc: Doc, where: OpenWhere) => {
-        DocFocusOrOpen(doc);
+    static addFieldDoc = (doc: Doc | Doc[] /* , where: OpenWhere */) => {
+        DocFocusOrOpen(doc instanceof Doc ? doc : doc[0]);
         return true;
     };
     public static renderProps(props: SchemaTableCellProps) {
@@ -186,7 +189,7 @@ export class SchemaTableCell extends ObservableReactComponent<SchemaTableCellPro
         return (
             <div
                 className="schema-table-cell"
-                onPointerDown={action(e => !this.selected && this._props.selectCell(this._props.Document, this._props.col))}
+                onPointerDown={action(() => !this.selected && this._props.selectCell(this._props.Document, this._props.col))}
                 style={{ padding: this._props.padding, maxWidth: this._props.maxWidth?.(), width: this._props.columnWidth() || undefined, border: this.selected ? `solid 2px ${Colors.MEDIUM_BLUE}` : undefined }}>
                 {this.content}
             </div>
@@ -207,7 +210,7 @@ export class SchemaImageCell extends ObservableReactComponent<SchemaTableCellPro
     choosePath(url: URL) {
         if (url.protocol === 'data') return url.href; // if the url ises the data protocol, just return the href
         if (url.href.indexOf(window.location.origin) === -1) return ClientUtils.CorsProxy(url.href); // otherwise, put it through the cors proxy erver
-        if (!/\.(png|jpg|jpeg|gif|webp)$/.test(url.href.toLowerCase())) return url.href; //Why is this here — good question
+        if (!/\.(png|jpg|jpeg|gif|webp)$/.test(url.href.toLowerCase())) return url.href; // Why is this here — good question
 
         const ext = extname(url.href);
         return url.href.replace(ext, '_s' + ext);
@@ -246,7 +249,7 @@ export class SchemaImageCell extends ObservableReactComponent<SchemaTableCellPro
     };
 
     @action
-    removeHoverPreview = (e: React.PointerEvent) => {
+    removeHoverPreview = () => {
         if (!this._previewRef) return;
         document.body.removeChild(this._previewRef);
     };
@@ -258,7 +261,7 @@ export class SchemaImageCell extends ObservableReactComponent<SchemaTableCellPro
         const height = this._props.rowHeight() ? this._props.rowHeight() - (this._props.padding || 6) * 2 : undefined;
         const width = height ? height * aspect : undefined; // increase the width of the image if necessary to maintain proportionality
 
-        return <img src={this.url} width={width ? width : undefined} height={height} style={{}} draggable="false" onPointerEnter={this.showHoverPreview} onPointerMove={this.moveHoverPreview} onPointerLeave={this.removeHoverPreview} />;
+        return <img src={this.url} width={width || undefined} height={height} style={{}} draggable="false" onPointerEnter={this.showHoverPreview} onPointerMove={this.moveHoverPreview} onPointerLeave={this.removeHoverPreview} />;
     }
 }
 
@@ -282,15 +285,15 @@ export class SchemaDateCell extends ObservableReactComponent<SchemaTableCellProp
         // } else {
         // ^ DateCast is always undefined for some reason, but that is what the field should be set to
         date && (this._props.Document[this._props.fieldKey] = new DateField(date));
-        //}
+        // }
     }, 'date change');
 
     render() {
-        const { color, textDecoration, fieldProps, cursor, pointerEvents } = SchemaTableCell.renderProps(this._props);
+        const { pointerEvents } = SchemaTableCell.renderProps(this._props);
         return (
             <>
                 <div style={{ pointerEvents: 'none' }}>
-                    <DatePicker dateFormat="Pp" selected={this.date?.date ?? Date.now()} onChange={e => {}} />
+                    <DatePicker dateFormat="Pp" selected={this.date?.date ?? Date.now()} onChange={emptyFunction} />
                 </div>
                 {pointerEvents === 'none' ? null : (
                     <Popup
@@ -301,7 +304,7 @@ export class SchemaDateCell extends ObservableReactComponent<SchemaTableCellProp
                         background={SettingsManager.userBackgroundColor}
                         popup={
                             <div style={{ width: 'fit-content', height: '200px' }}>
-                                <DatePicker open={true} dateFormat="Pp" selected={this.date?.date ?? Date.now()} onChange={this.handleChange} />
+                                <DatePicker open dateFormat="Pp" selected={this.date?.date ?? Date.now()} onChange={this.handleChange} />
                             </div>
                         }
                     />
@@ -329,7 +332,7 @@ export class SchemaRTFCell extends ObservableReactComponent<SchemaTableCellProps
         fieldProps.isContentActive = this.selectedFunc;
         return (
             <div className="schemaRTFCell" style={{ fontStyle: this.selected ? undefined : 'italic', color, textDecoration, cursor, pointerEvents }}>
-                {this.selected ? <FormattedTextBox {...fieldProps} autoFocus={true} onBlur={() => this._props.finishEdit?.()} /> : (field => (field ? Field.toString(field) : ''))(FieldValue(fieldProps.Document[fieldProps.fieldKey]))}
+                {this.selected ? <FormattedTextBox {...fieldProps} autoFocus onBlur={() => this._props.finishEdit?.()} /> : (field => (field ? Field.toString(field) : ''))(FieldValue(fieldProps.Document[fieldProps.fieldKey]))}
             </div>
         );
     }
@@ -355,8 +358,8 @@ export class SchemaBoolCell extends ObservableReactComponent<SchemaTableCellProp
                     checked={BoolCast(this._props.Document[this._props.fieldKey])}
                     onChange={undoBatch((value: React.ChangeEvent<HTMLInputElement> | undefined) => {
                         if ((value?.nativeEvent as any).shiftKey) {
-                            this._props.setColumnValues(this._props.fieldKey.replace(/^_/, ''), (color === 'black' ? '=' : '') + value?.target?.checked.toString());
-                        } else KeyValueBox.SetField(this._props.Document, this._props.fieldKey.replace(/^_/, ''), (color === 'black' ? '=' : '') + value?.target?.checked.toString());
+                            this._props.setColumnValues(this._props.fieldKey.replace(/^_/, ''), (color === 'black' ? '=' : '') + (value?.target?.checked.toString() ?? ''));
+                        } else KeyValueBox.SetField(this._props.Document, this._props.fieldKey.replace(/^_/, ''), (color === 'black' ? '=' : '') + (value?.target?.checked.toString() ?? ''));
                     })}
                 />
                 <EditableView
@@ -391,7 +394,7 @@ export class SchemaEnumerationCell extends ObservableReactComponent<SchemaTableC
         return this._props.isRowActive() && selected?.[0] === this._props.Document && selected[1] === this._props.col;
     }
     render() {
-        const { color, textDecoration, fieldProps, cursor, pointerEvents } = SchemaTableCell.renderProps(this._props);
+        const { color, textDecoration, cursor, pointerEvents } = SchemaTableCell.renderProps(this._props);
         const options = this._props.options?.map(facet => ({ value: facet, label: facet }));
         return (
             <div className="schemaSelectionCell" style={{ color, textDecoration, cursor, pointerEvents }}>
diff --git a/src/client/views/global/globalScripts.ts b/src/client/views/global/globalScripts.ts
index 50cf26cdb..231bac541 100644
--- a/src/client/views/global/globalScripts.ts
+++ b/src/client/views/global/globalScripts.ts
@@ -21,6 +21,7 @@ import { ImageBox } from '../nodes/ImageBox';
 import { VideoBox } from '../nodes/VideoBox';
 import { WebBox } from '../nodes/WebBox';
 import { RichTextMenu } from '../nodes/formattedText/RichTextMenu';
+
 // import { InkTranscription } from '../InkTranscription';
 
 // eslint-disable-next-line prefer-arrow-callback
@@ -162,15 +163,15 @@ ScriptingGlobals.add(function setFontAttr(attr: 'font' | 'fontColor' | 'highligh
     const map: Map<'font'|'fontColor'|'highlight'|'fontSize'|'alignment', { checkResult: () => any; setDoc: () => void;}> = new Map([
         ['font', {
             checkResult: () => RichTextMenu.Instance?.fontFamily,
-            setDoc: () => value && RichTextMenu.Instance?.setFontFamily(value),
+            setDoc: () => value && RichTextMenu.Instance?.setFontField(value, 'fontFamily'),
         }],
         ['highlight', {
             checkResult: () => RichTextMenu.Instance?.fontHighlight,
-            setDoc: () => value  && RichTextMenu.Instance?.setHighlight(value),
+            setDoc: () => value  && RichTextMenu.Instance?.setFontField(value, 'fontHighlight'),
         }],
         ['fontColor', {
             checkResult: () => RichTextMenu.Instance?.fontColor,
-            setDoc: () => value && RichTextMenu.Instance?.setColor(value),
+            setDoc: () => value && RichTextMenu.Instance?.setFontField(value, 'fontColor'),
         }],
         ['alignment', {
             checkResult: () => RichTextMenu.Instance?.textAlign,
@@ -182,7 +183,7 @@ ScriptingGlobals.add(function setFontAttr(attr: 'font' | 'fontColor' | 'highligh
                 let fsize = value;
                 if (typeof fsize === 'number') fsize = fsize.toString();
                 if (fsize && Number(fsize).toString() === fsize) fsize += 'px';
-                RichTextMenu.Instance?.setFontSize(fsize);
+                RichTextMenu.Instance?.setFontField(fsize, 'fontSize');
             },
         }],
     ]);
@@ -207,7 +208,7 @@ ScriptingGlobals.add(function toggleCharStyle(charStyle: attrname, checkResult?:
                                                                              (RichTextMenu.Instance?.textAlign === where)): 
                                                          where === 'vcent' ?  BoolCast(Doc.UserDoc()._layout_centered):
                                                                             (Doc.UserDoc().textAlign === where),
-                        toggle: () => { editorView?.state ? (where === 'vcent' ? RichTextMenu.Instance?.vcenterToggle(editorView, editorView.dispatch):
+                        toggle: () => { editorView?.state ? (where === 'vcent' ? RichTextMenu.Instance?.vcenterToggle():
                                                                                  RichTextMenu.Instance?.align(editorView, editorView.dispatch, where)):
                                                             where === 'vcent' ?  Doc.UserDoc()._layout_centered = !Doc.UserDoc()._layout_centered:
                                                                                 (Doc.UserDoc().textAlign = where); }
@@ -470,7 +471,7 @@ ScriptingGlobals.add(function setGroupBy(key: string, checkResult?: boolean) {
     if (checkResult) {
         return StrCast((editorView ? RichTextMenu.Instance : Doc.UserDoc())?.fontFamily);
     }
-    if (editorView) RichTextMenu.Instance?.setFontFamily(key);
+    if (editorView) RichTextMenu.Instance?.setFontField(key, 'fontFamily');
     else Doc.UserDoc().fontFamily = key;
     return undefined;
 });
diff --git a/src/client/views/linking/LinkMenuItem.tsx b/src/client/views/linking/LinkMenuItem.tsx
index 38ef08ef9..9be78a6cb 100644
--- a/src/client/views/linking/LinkMenuItem.tsx
+++ b/src/client/views/linking/LinkMenuItem.tsx
@@ -1,3 +1,5 @@
+/* eslint-disable jsx-a11y/no-static-element-interactions */
+/* eslint-disable jsx-a11y/click-events-have-key-events */
 import { IconProp } from '@fortawesome/fontawesome-svg-core';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { Tooltip } from '@mui/material';
@@ -36,14 +38,14 @@ interface LinkMenuItemProps {
 
 // drag links and drop link targets (embedding them if needed)
 export async function StartLinkTargetsDrag(dragEle: HTMLElement, docView: DocumentView, downX: number, downY: number, sourceDoc: Doc, specificLinks?: Doc[]) {
-    const draggedDocs = (specificLinks ? specificLinks : LinkManager.Links(sourceDoc)).map(link => LinkManager.getOppositeAnchor(link, sourceDoc)).filter(l => l) as Doc[];
+    const draggedDocs = (specificLinks || LinkManager.Links(sourceDoc)).map(link => LinkManager.getOppositeAnchor(link, sourceDoc)).filter(l => l) as Doc[];
 
     if (draggedDocs.length) {
         const moddrag: Doc[] = [];
-        for (const draggedDoc of draggedDocs) {
+        draggedDocs.forEach(async draggedDoc => {
             const doc = await Cast(draggedDoc.annotationOn, Doc);
             if (doc) moddrag.push(doc);
-        }
+        });
 
         const dragData = new DragManager.DocumentDragData(moddrag.length ? moddrag : draggedDocs);
         dragData.canEmbed = true;
@@ -108,7 +110,11 @@ export class LinkMenuItem extends ObservableReactComponent<LinkMenuItemProps> {
                     LinkManager.Instance.currentLinkAnchor = LinkManager.Instance.currentLink ? this.sourceAnchor : undefined;
 
                     if ((SettingsManager.Instance.propertiesWidth ?? 0) < 100) {
-                        setTimeout(action(() => (SettingsManager.Instance.propertiesWidth = 250)));
+                        setTimeout(
+                            action(() => {
+                                SettingsManager.Instance.propertiesWidth = 250;
+                            })
+                        );
                     }
                 }
             })
@@ -166,8 +172,12 @@ export class LinkMenuItem extends ObservableReactComponent<LinkMenuItemProps> {
         return (
             <div
                 className="linkMenu-item"
-                onPointerEnter={action(e => (this._hover = true))}
-                onPointerLeave={action(e => (this._hover = false))}
+                onPointerEnter={action(() => {
+                    this._hover = true;
+                })}
+                onPointerLeave={action(() => {
+                    this._hover = false;
+                })}
                 style={{
                     fontSize: this._hover ? 'larger' : undefined,
                     fontWeight: this._hover ? 'bold' : undefined,
@@ -176,15 +186,15 @@ export class LinkMenuItem extends ObservableReactComponent<LinkMenuItemProps> {
                 <div className="linkMenu-item-content expand-two">
                     <div
                         ref={this._drag}
-                        className="linkMenu-name" //title="drag to view target. click to customize."
+                        className="linkMenu-name" // title="drag to view target. click to customize."
                         onPointerDown={this.onLinkButtonDown}>
                         <div className="linkMenu-item-buttons">
-                            <Tooltip disableInteractive={true} title={<div className="dash-tooltip">Edit Link</div>}>
+                            <Tooltip disableInteractive title={<div className="dash-tooltip">Edit Link</div>}>
                                 <div className="linkMenu-icon-wrapper" ref={this._editRef} onPointerDown={this.onEdit} onClick={e => e.stopPropagation()}>
                                     <FontAwesomeIcon className="linkMenu-icon" icon="edit" size="sm" />
                                 </div>
                             </Tooltip>
-                            <Tooltip disableInteractive={true} title={<div className="dash-tooltip">Show/Hide Link</div>}>
+                            <Tooltip disableInteractive title={<div className="dash-tooltip">Show/Hide Link</div>}>
                                 <div className="linkMenu-icon-wrapper" onPointerDown={this.onIconDown}>
                                     <FontAwesomeIcon className="linkMenu-icon" icon={destinationIcon} size="sm" />
                                 </div>
@@ -213,7 +223,7 @@ export class LinkMenuItem extends ObservableReactComponent<LinkMenuItemProps> {
                                 </p>
                             ) : null}
                             <div className="linkMenu-title-wrapper">
-                                <Tooltip disableInteractive={true} title={<div className="dash-tooltip">Follow Link</div>}>
+                                <Tooltip disableInteractive title={<div className="dash-tooltip">Follow Link</div>}>
                                     <p className="linkMenu-destination-title">
                                         {this._props.linkDoc.linksToAnnotation && Cast(this._props.destinationDoc.data, WebField)?.url.href === this._props.linkDoc.annotationUri ? 'Annotation in' : ''} {StrCast(title)}
                                     </p>
@@ -223,7 +233,7 @@ export class LinkMenuItem extends ObservableReactComponent<LinkMenuItemProps> {
                         </div>
 
                         <div className="linkMenu-item-buttons">
-                            <Tooltip disableInteractive={true} title={<div className="dash-tooltip">Delete Link</div>}>
+                            <Tooltip disableInteractive title={<div className="dash-tooltip">Delete Link</div>}>
                                 <div className="linkMenu-deleteButton" onPointerDown={this.deleteLink} onClick={e => e.stopPropagation()}>
                                     <FontAwesomeIcon className="fa-icon" icon="trash" size="sm" />
                                 </div>
diff --git a/src/client/views/linking/LinkPopup.tsx b/src/client/views/linking/LinkPopup.tsx
index f2681bf2e..9fb1c0fdc 100644
--- a/src/client/views/linking/LinkPopup.tsx
+++ b/src/client/views/linking/LinkPopup.tsx
@@ -1,15 +1,12 @@
-import { action, observable } from 'mobx';
+/* eslint-disable react/require-default-props */
 import { observer } from 'mobx-react';
-import { EditorView } from 'prosemirror-view';
 import * as React from 'react';
-import { emptyFunction } from '../../../Utils';
 import { returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue } from '../../../ClientUtils';
+import { emptyFunction } from '../../../Utils';
 import { Doc } from '../../../fields/Doc';
 import { Transform } from '../../util/Transform';
-import { undoBatch } from '../../util/UndoManager';
 import { DefaultStyleProvider } from '../StyleProvider';
-import { OpenWhere, returnEmptyDocViewList } from '../nodes/DocumentView';
-import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox';
+import { returnEmptyDocViewList } from '../nodes/DocumentView';
 import { SearchBox } from '../search/SearchBox';
 import './LinkPopup.scss';
 
@@ -29,16 +26,6 @@ interface LinkPopupProps {
 
 @observer
 export class LinkPopup extends React.Component<LinkPopupProps> {
-    @observable private linkURL: string = '';
-    @observable public view?: EditorView = undefined;
-
-    // TODO: should check for valid URL
-    @undoBatch
-    makeLinkToURL = (target: string, lcoation: string) => ((this.view as any)?.TextView as FormattedTextBox).makeLinkAnchor(undefined, OpenWhere.addRight, target, target);
-
-    @action
-    onLinkChange = (e: React.ChangeEvent<HTMLInputElement>) => (this.linkURL = e.target.value);
-
     getPWidth = () => 500;
     getPHeight = () => 500;
 
@@ -65,7 +52,7 @@ export class LinkPopup extends React.Component<LinkPopupProps> {
                         docViewPath={returnEmptyDocViewList}
                         linkFrom={linkDoc}
                         linkCreateAnchor={this.props.linkCreateAnchor}
-                        linkSearch={true}
+                        linkSearch
                         linkCreated={this.props.linkCreated}
                         fieldKey="data"
                         isSelected={returnTrue}
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
index 958d63267..6a86af6a7 100644
--- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
@@ -97,7 +97,9 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
         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)),
+                action(() => {
+                    this.Document[TransitionTimer] = this.Transition = undefined;
+                }),
                 num
             );
         }
@@ -105,7 +107,11 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
 
     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])));
+        this.WrapperKeys.forEach(
+            action(keys => {
+                (this as any)[keys.upper] = (this.props as any)[keys.lower];
+            })
+        );
     }
 
     CollectionFreeFormView = this.props.CollectionFreeFormView; // needed for type checking
@@ -121,6 +127,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
                 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;
+                default:
             } // prettier-ignore
         }
         return this._props.styleProvider?.(doc, props, property);
@@ -129,7 +136,10 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
     public static getValues(doc: Doc, time: number, fillIn: boolean = true) {
         return CollectionFreeFormDocumentView.animFields.reduce(
             (p, val) => {
-                p[val.key] = Cast(doc[`${val.key}_indexed`], listSpec('number'), fillIn ? [NumCast(doc[val.key], val.val)] : []).reduce((p, v, i) => ((i <= Math.round(time) && v !== undefined) || p === undefined ? v : p), undefined as any as number);
+                p[val.key] = Cast(doc[`${val.key}_indexed`], listSpec('number'), fillIn ? [NumCast(doc[val.key], val.val)] : []).reduce(
+                    (prev, v, i) => ((i <= Math.round(time) && v !== undefined) || prev === undefined ? v : prev),
+                    undefined as any as number
+                );
                 return p;
             },
             {} as { [val: string]: Opt<number> }
@@ -139,7 +149,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
     public static getStringValues(doc: Doc, time: number) {
         return CollectionFreeFormDocumentView.animStringFields.reduce(
             (p, val) => {
-                p[val] = Cast(doc[`${val}_indexed`], listSpec('string'), [StrCast(doc[val])]).reduce((p, v, i) => ((i <= Math.round(time) && v !== undefined) || p === undefined ? v : p), undefined as any as string);
+                p[val] = Cast(doc[`${val}_indexed`], listSpec('string'), [StrCast(doc[val])]).reduce((prev, v, i) => ((i <= Math.round(time) && v !== undefined) || prev === undefined ? v : prev), undefined as any as string);
                 return p;
             },
             {} as { [val: string]: Opt<string> }
@@ -179,15 +189,21 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
     public static setupKeyframes(docs: Doc[], currTimecode: number, makeAppear: boolean = false) {
         docs.forEach(doc => {
             if (doc.appearFrame === undefined) doc.appearFrame = currTimecode;
-            if (!doc['opacity_indexed']) {
+            if (!doc.opacity_indexed) {
                 // opacity is unlike other fields because it's value should not be undefined before it appears to enable it to fade-in
-                doc['opacity_indexed'] = new List<number>(numberRange(currTimecode + 1).map(t => (!doc.z && makeAppear && t < NumCast(doc.appearFrame) ? 0 : 1)));
+                doc.opacity_indexed = new List<number>(numberRange(currTimecode + 1).map(t => (!doc.z && makeAppear && t < NumCast(doc.appearFrame) ? 0 : 1)));
             }
-            CollectionFreeFormDocumentView.animFields.forEach(val => (doc[val.key] = ComputedField.MakeInterpolatedNumber(val.key, 'activeFrame', doc, currTimecode, val.val)));
-            CollectionFreeFormDocumentView.animStringFields.forEach(val => (doc[val] = ComputedField.MakeInterpolatedString(val, 'activeFrame', doc, currTimecode)));
-            CollectionFreeFormDocumentView.animDataFields(doc).forEach(val => (doc[val] = ComputedField.MakeInterpolatedDataField(val, 'activeFrame', doc, currTimecode)));
+            CollectionFreeFormDocumentView.animFields.forEach(val => {
+                doc[val.key] = ComputedField.MakeInterpolatedNumber(val.key, 'activeFrame', doc, currTimecode, val.val);
+            });
+            CollectionFreeFormDocumentView.animStringFields.forEach(val => {
+                doc[val] = ComputedField.MakeInterpolatedString(val, 'activeFrame', doc, currTimecode);
+            });
+            CollectionFreeFormDocumentView.animDataFields(doc).forEach(val => {
+                doc[val] = ComputedField.MakeInterpolatedDataField(val, 'activeFrame', doc, currTimecode);
+            });
             const targetDoc = doc; // data fields, like rtf 'text' exist on the data doc, so
-            //doc !== targetDoc && (targetDoc.embedContainer = doc.embedContainer); // the computed fields don't see the layout doc -- need to copy the embedContainer to the data doc (HACK!!!) and set the activeFrame on the data doc (HACK!!!)
+            // doc !== targetDoc && (targetDoc.embedContainer = doc.embedContainer); // the computed fields don't see the layout doc -- need to copy the embedContainer to the data doc (HACK!!!) and set the activeFrame on the data doc (HACK!!!)
             targetDoc.activeFrame = ComputedField.MakeFunction('this.embedContainer?._currentFrame||0');
             targetDoc.dataTransition = 'inherit';
         });
@@ -202,16 +218,14 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
             if (topDoc.z) {
                 const spt = screenXf.inverse().transformPoint(NumCast(topDoc.x), NumCast(topDoc.y));
                 topDoc.z = 0;
-                topDoc.x = spt[0];
-                topDoc.y = spt[1];
+                [topDoc.x, topDoc.y] = spt;
                 this._props.removeDocument?.(topDoc);
                 this._props.addDocTab(topDoc, OpenWhere.inParentFromScreen);
             } else {
                 const spt = this.screenToLocalTransform().inverse().transformPoint(0, 0);
                 const fpt = screenXf.transformPoint(spt[0], spt[1]);
                 topDoc.z = 1;
-                topDoc.x = fpt[0];
-                topDoc.y = fpt[1];
+                [topDoc.x, topDoc.y] = fpt;
             }
             setTimeout(() => SelectionManager.SelectView(DocumentManager.Instance.getDocumentView(topDoc, containerDocView), false), 0);
         }
@@ -259,6 +273,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
                     <div style={{ position: 'absolute', width: this.PanelWidth(), height: this.PanelHeight(), background: 'lightGreen' }} />
                 ) : (
                     <DocumentView
+                        // eslint-disable-next-line react/jsx-props-no-spreading
                         {...OmitKeys(this._props,this.WrapperKeys.map(val => val.lower)).omit} // prettier-ignore
                         DataTransition={this.DataTransition}
                         LocalRotation={this.localRotation}
@@ -274,6 +289,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
         );
     }
 }
+// eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function gotoFrame(doc: any, newFrame: any) {
     CollectionFreeFormDocumentView.gotoKeyFrame(doc, newFrame);
 });
diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx
index 708536de0..c1aa1c699 100644
--- a/src/client/views/nodes/ComparisonBox.tsx
+++ b/src/client/views/nodes/ComparisonBox.tsx
@@ -1,5 +1,5 @@
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { action, computed, makeObservable, observable, trace } from 'mobx';
+import { action, computed, makeObservable, observable } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
 import { returnFalse, returnNone, returnZero, setupMoveUpEvents } from '../../../ClientUtils';
@@ -69,7 +69,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
                 e,
                 this.onPointerMove,
                 emptyFunction,
-                action((e, doubleTap) => {
+                action((moveEv, doubleTap) => {
                     if (doubleTap) {
                         this._isAnyChildContentActive = true;
                         if (!this.dataDoc[this.fieldKey + '_1'] && !this.dataDoc[this.fieldKey]) this.dataDoc[this.fieldKey + '_1'] = DocUtils.copyDragFactory(Doc.UserDoc().emptyNote as Doc);
@@ -140,14 +140,14 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
         setupMoveUpEvents(
             this,
             e,
-            e => {
+            moveEv => {
                 const de = new DragManager.DocumentDragData([DocCast(this.dataDoc[which])], dropActionType.move);
                 de.moveDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (doc: Doc | Doc[]) => boolean): boolean => {
                     this.clearDoc(which);
                     return addDocument(doc);
                 };
                 de.canEmbed = true;
-                DragManager.StartDocumentDrag([this._closeRef.current!], de, e.clientX, e.clientY);
+                DragManager.StartDocumentDrag([this._closeRef.current!], de, moveEv.clientX, moveEv.clientY);
                 return true;
             },
             emptyFunction,
@@ -158,10 +158,10 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
         if (property === StyleProp.PointerEvents) return 'none';
         return this._props.styleProvider?.(doc, props, property);
     };
-    moveDoc1 = (doc: Doc | Doc[], targetCol: Doc | undefined, addDoc: any) => (doc instanceof Doc ? [doc] : doc).reduce((res, doc: Doc) => res && this.moveDoc(doc, addDoc, this.fieldKey + '_1'), true);
-    moveDoc2 = (doc: Doc | Doc[], targetCol: Doc | undefined, addDoc: any) => (doc instanceof Doc ? [doc] : doc).reduce((res, doc: Doc) => res && this.moveDoc(doc, addDoc, this.fieldKey + '_2'), true);
-    remDoc1 = (doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).reduce((res, doc) => res && this.remDoc(doc, this.fieldKey + '_1'), true);
-    remDoc2 = (doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).reduce((res, doc) => res && this.remDoc(doc, this.fieldKey + '_2'), true);
+    moveDoc1 = (docs: Doc | Doc[], targetCol: Doc | undefined, addDoc: any) => (docs instanceof Doc ? [docs] : docs).reduce((res, doc: Doc) => res && this.moveDoc(doc, addDoc, this.fieldKey + '_1'), true);
+    moveDoc2 = (docs: Doc | Doc[], targetCol: Doc | undefined, addDoc: any) => (docs instanceof Doc ? [docs] : docs).reduce((res, doc: Doc) => res && this.moveDoc(doc, addDoc, this.fieldKey + '_2'), true);
+    remDoc1 = (docs: Doc | Doc[]) => (docs instanceof Doc ? [docs] : docs).reduce((res, doc) => res && this.remDoc(doc, this.fieldKey + '_1'), true);
+    remDoc2 = (docs: Doc | Doc[]) => (docs instanceof Doc ? [docs] : docs).reduce((res, doc) => res && this.remDoc(doc, this.fieldKey + '_2'), true);
 
     /**
      * Tests for whether a comparison box slot (ie, before or after) has renderable text content
@@ -196,7 +196,6 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
 
     _closeRef = React.createRef<HTMLDivElement>();
     render() {
-        trace();
         const clearButton = (which: string) => (
             <div
                 ref={this._closeRef}
diff --git a/src/client/views/nodes/DataVizBox/DataVizBox.tsx b/src/client/views/nodes/DataVizBox/DataVizBox.tsx
index 2a0bc42ee..b87fead4f 100644
--- a/src/client/views/nodes/DataVizBox/DataVizBox.tsx
+++ b/src/client/views/nodes/DataVizBox/DataVizBox.tsx
@@ -1,3 +1,4 @@
+/* eslint-disable react/jsx-props-no-spreading */
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { Checkbox } from '@mui/material';
 import { Colors, Toggle, ToggleType, Type } from 'browndash-components';
@@ -9,7 +10,6 @@ import { emptyFunction } from '../../../../Utils';
 import { Doc, DocListCast, Field, Opt, StrListCast } from '../../../../fields/Doc';
 import { InkTool } from '../../../../fields/InkField';
 import { List } from '../../../../fields/List';
-import { listSpec } from '../../../../fields/Schema';
 import { Cast, CsvCast, DocCast, NumCast, StrCast } from '../../../../fields/Types';
 import { CsvField } from '../../../../fields/URLField';
 import { TraceMobx } from '../../../../fields/util';
@@ -62,9 +62,9 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() im
             setupMoveUpEvents(
                 this,
                 e,
-                action(e => {
+                action(moveEv => {
                     MarqueeAnnotator.clearAnnotations(this._savedAnnotations);
-                    this._marqueeref.current?.onInitiateSelection([e.clientX, e.clientY]);
+                    this._marqueeref.current?.onInitiateSelection([moveEv.clientX, moveEv.clientY]);
                     return true;
                 }),
                 returnFalse,
@@ -95,7 +95,7 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() im
 
     // all CSV records in the dataset (that aren't an empty row)
     @computed.struct get records() {
-        var records = DataVizBox.dataset.get(CsvCast(this.dataDoc[this.fieldKey]).url.href);
+        const records = DataVizBox.dataset.get(CsvCast(this.dataDoc[this.fieldKey]).url.href);
         return records?.filter(record => Object.keys(record).some(key => record[key])) ?? [];
     }
 
@@ -110,11 +110,15 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() im
     @computed.struct get axes() {
         return StrListCast(this.layoutDoc._dataViz_axes);
     }
-    selectAxes = (axes: string[]) => (this.layoutDoc._dataViz_axes = new List<string>(axes));
+    selectAxes = (axes: string[]) => {
+        this.layoutDoc._dataViz_axes = new List<string>(axes);
+    };
     @computed.struct get titleCol() {
         return StrCast(this.layoutDoc._dataViz_titleCol);
     }
-    selectTitleCol = (titleCol: string) => (this.layoutDoc._dataViz_titleCol = titleCol);
+    selectTitleCol = (titleCol: string) => {
+        this.layoutDoc._dataViz_titleCol = titleCol;
+    };
 
     @action // pinned / linked anchor doc includes selected rows, graph titles, and graph colors
     restoreView = (data: Doc) => {
@@ -124,7 +128,7 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() im
         this.layoutDoc.dataViz_histogram_barColors = Field.Copy(data.dataViz_histogram_barColors);
         this.layoutDoc.dataViz_histogram_defaultColor = data.dataViz_histogram_defaultColor;
         this.layoutDoc.dataViz_pie_sliceColors = Field.Copy(data.dataViz_pie_sliceColors);
-        Object.keys(this.layoutDoc).map(key => {
+        Object.keys(this.layoutDoc).forEach(key => {
             if (key.startsWith('dataViz_histogram_title') || key.startsWith('dataViz_lineChart_title') || key.startsWith('dataViz_pieChart_title')) {
                 this.layoutDoc['_' + key] = data[key];
             }
@@ -150,7 +154,7 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() im
                   annotationOn: this.Document,
                   // when we clear selection -> we should have it so chartBox getAnchor returns undefined
                   // this is for when we want the whole doc (so when the chartBox getAnchor returns without a marker)
-                  /*put in some options*/
+                  /* put in some options */
               });
         anchor.config_dataViz = this.dataVizView;
         anchor.config_dataVizAxes = this.axes.length ? new List<string>(this.axes) : undefined;
@@ -158,24 +162,21 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() im
         anchor.dataViz_histogram_barColors = Field.Copy(this.layoutDoc.dataViz_histogram_barColors);
         anchor.dataViz_histogram_defaultColor = this.layoutDoc.dataViz_histogram_defaultColor;
         anchor.dataViz_pie_sliceColors = Field.Copy(this.layoutDoc.dataViz_pie_sliceColors);
-        Object.keys(this.layoutDoc).map(key => {
+        Object.keys(this.layoutDoc).forEach(key => {
             if (key.startsWith('dataViz_histogram_title') || key.startsWith('dataViz_lineChart_title') || key.startsWith('dataViz_pieChart_title')) {
                 anchor[key] = this.layoutDoc[key];
             }
         });
 
         this.addDocument(anchor);
-        //addAsAnnotation && this.addDocument(anchor);
+        // addAsAnnotation && this.addDocument(anchor);
         return anchor;
     };
 
     createNoteAnnotation = () => {
-        const createFunc = undoable(
-            action(() => {
-                const note = this._sidebarRef.current?.anchorMenuClick(this.getAnchor(false), ['latitude', 'longitude', '-linkedTo']);
-            }),
-            'create note annotation'
-        );
+        const createFunc = undoable(() => {
+            this._sidebarRef.current?.anchorMenuClick(this.getAnchor(false), ['latitude', 'longitude', '-linkedTo']);
+        }, 'create note annotation');
         if (!this.layoutDoc.layout_showSidebar) {
             this.toggleSidebar();
             setTimeout(createFunc);
@@ -192,7 +193,7 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() im
         this.layoutDoc._width = this.layoutDoc._layout_showSidebar ? NumCast(this.layoutDoc._width) * 1.2 : Math.max(20, NumCast(this.layoutDoc._width) - prevWidth);
     };
     @computed get SidebarShown() {
-        return this.layoutDoc._layout_showSidebar ? true : false;
+        return !!this.layoutDoc._layout_showSidebar;
     }
     @computed get sidebarHandle() {
         return (
@@ -206,7 +207,7 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() im
                     backgroundColor: this.SidebarShown ? Colors.MEDIUM_BLUE : Colors.BLACK,
                 }}
                 onPointerDown={this.sidebarBtnDown}>
-                <FontAwesomeIcon style={{ color: Colors.WHITE }} icon={'comment-alt'} size="sm" />
+                <FontAwesomeIcon style={{ color: Colors.WHITE }} icon="comment-alt" size="sm" />
             </div>
         );
     }
@@ -218,7 +219,7 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() im
         setupMoveUpEvents(
             this,
             e,
-            (e, down, delta) =>
+            (moveEv, down, delta) =>
                 runInAction(() => {
                     const localDelta = this._props
                         .ScreenToLocalTransform()
@@ -246,7 +247,9 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() im
             options.didMove = true;
             this.toggleSidebar();
         }
-        return new Promise<Opt<DocumentView>>(res => DocumentManager.Instance.AddViewRenderedCb(doc, dv => res(dv)));
+        return new Promise<Opt<DocumentView>>(res => {
+            DocumentManager.Instance.AddViewRenderedCb(doc, dv => res(dv));
+        });
     };
     @computed get sidebarWidthPercent() {
         return StrCast(this.layoutDoc._layout_sidebarWidthPercent, '0%');
@@ -266,32 +269,31 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() im
         if (!DataVizBox.dataset.has(CsvCast(this.dataDoc[this.fieldKey]).url.href)) this.fetchData();
         this._disposers.datavis = reaction(
             () => {
-                if (this.layoutDoc.dataViz_schemaLive == undefined) this.layoutDoc.dataViz_schemaLive = true;
+                if (this.layoutDoc.dataViz_schemaLive === undefined) this.layoutDoc.dataViz_schemaLive = true;
                 const getFrom = DocCast(this.layoutDoc.dataViz_asSchema);
-                const keys = Cast(getFrom?.schema_columnKeys, listSpec('string'))?.filter(key => key != 'text');
-                if (!keys) return;
-                const children = DocListCast(getFrom[Doc.LayoutFieldKey(getFrom)]);
-                var current: { [key: string]: string }[] = [];
+                const keys = StrListCast(getFrom?.schema_columnKeys).filter(key => key !== 'text');
+                const children = DocListCast(getFrom?.[Doc.LayoutFieldKey(getFrom)]);
+                const current: { [key: string]: string }[] = [];
                 children
                     .filter(child => child)
                     .forEach(child => {
                         const row: { [key: string]: string } = {};
                         keys.forEach(key => {
-                            var cell = child[key];
-                            if (cell && (cell as string)) cell = cell.toString().replace(/\,/g, '');
+                            let cell = child[key];
+                            if (cell && (cell as string)) cell = cell.toString().replace(/,/g, '');
                             row[key] = StrCast(cell);
                         });
                         current.push(row);
                     });
                 if (!this.layoutDoc._dataViz_schemaOG) {
                     // makes a copy of the original table for the "live" toggle
-                    let csvRows = [];
+                    const csvRows = [];
                     csvRows.push(keys.join(','));
                     for (let i = 0; i < children.length - 1; i++) {
-                        let eachRow = [];
+                        const eachRow = [];
                         for (let j = 0; j < keys.length; j++) {
-                            var cell = children[i][keys[j]];
-                            if (cell && (cell as string)) cell = cell.toString().replace(/\,/g, '');
+                            let cell = children[i][keys[j]];
+                            if (cell && (cell as string)) cell = cell.toString().replace(/,/g, '');
                             eachRow.push(cell);
                         }
                         csvRows.push(eachRow);
@@ -305,19 +307,19 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() im
                 }
                 const ogDoc = this.layoutDoc._dataViz_schemaOG as Doc;
                 const ogHref = CsvCast(ogDoc[this.fieldKey]) ? CsvCast(ogDoc[this.fieldKey]).url.href : undefined;
-                const href = CsvCast(this.Document[this.fieldKey]).url.href;
+                const { href } = CsvCast(this.Document[this.fieldKey]).url;
                 if (ogHref && !DataVizBox.datasetSchemaOG.has(href)) {
                     // sets original dataset to the var
                     const lastRow = current.pop();
                     DataVizBox.datasetSchemaOG.set(href, current);
                     current.push(lastRow!);
-                    fetch('/csvData?uri=' + ogHref).then(res => res.json().then(action(res => !res.errno && DataVizBox.datasetSchemaOG.set(href, res))));
+                    fetch('/csvData?uri=' + ogHref).then(res => res.json().then(action(jsonRes => !jsonRes.errno && DataVizBox.datasetSchemaOG.set(href, jsonRes))));
                 }
                 return current;
             },
             current => {
                 if (current) {
-                    const href = CsvCast(this.Document[this.fieldKey]).url.href;
+                    const { href } = CsvCast(this.Document[this.fieldKey]).url;
                     if (this.layoutDoc.dataViz_schemaLive) DataVizBox.dataset.set(href, current);
                     else DataVizBox.dataset.set(href, DataVizBox.datasetSchemaOG.get(href)!);
                 }
@@ -329,8 +331,8 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() im
     fetchData = () => {
         if (!this.Document.dataViz_asSchema) {
             DataVizBox.dataset.set(CsvCast(this.dataDoc[this.fieldKey]).url.href, []); // assign temporary dataset as a lock to prevent duplicate server requests
-            fetch('/csvData?uri=' + this.dataUrl?.url.href) //
-                .then(res => res.json().then(action(res => !res.errno && DataVizBox.dataset.set(CsvCast(this.dataDoc[this.fieldKey]).url.href, res))));
+            fetch('/csvData?uri=' + (this.dataUrl?.url.href ?? '')) //
+                .then(res => res.json().then(action(jsonRes => !jsonRes.errno && DataVizBox.dataset.set(CsvCast(this.dataDoc[this.fieldKey]).url.href, jsonRes))));
         }
     };
 
@@ -343,7 +345,7 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() im
             records: this.records,
             axes: this.axes,
             titleCol: this.titleCol,
-            //width: this.SidebarShown? this._props.PanelWidth()*.9/1.2: this._props.PanelWidth() * 0.9,
+            // width: this.SidebarShown? this._props.PanelWidth()*.9/1.2: this._props.PanelWidth() * 0.9,
             height: (this._props.PanelHeight() / scale - 32) /* height of 'change view' button */ * 0.9,
             width: ((this._props.PanelWidth() - this.sidebarWidth()) / scale) * 0.9,
             margin: { top: 10, right: 25, bottom: 75, left: 45 },
@@ -351,11 +353,13 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() im
         if (!this.records.length) return 'no data/visualization';
         switch (this.dataVizView) {
             case DataVizView.TABLE:     return <TableBox  {...sharedProps} docView={this.DocumentView} selectAxes={this.selectAxes} selectTitleCol={this.selectTitleCol}/>;
-            case DataVizView.LINECHART: return <LineChart {...sharedProps} dataDoc={this.dataDoc} fieldKey={this.fieldKey} ref={r => (this._vizRenderer = r ?? undefined)} vizBox={this} />;
-            case DataVizView.HISTOGRAM: return <Histogram {...sharedProps} dataDoc={this.dataDoc} fieldKey={this.fieldKey} ref={r => (this._vizRenderer = r ?? undefined)} />;
-            case DataVizView.PIECHART:  return <PieChart  {...sharedProps} dataDoc={this.dataDoc} fieldKey={this.fieldKey} ref={r => (this._vizRenderer = r ?? undefined)} 
+            case DataVizView.LINECHART: return <LineChart {...sharedProps} dataDoc={this.dataDoc} fieldKey={this.fieldKey} ref={r => {this._vizRenderer = r ?? undefined;}} vizBox={this} />;
+            case DataVizView.HISTOGRAM: return <Histogram {...sharedProps} dataDoc={this.dataDoc} fieldKey={this.fieldKey} ref={r => {this._vizRenderer = r ?? undefined;}} />;
+            case DataVizView.PIECHART:  return <PieChart  {...sharedProps} dataDoc={this.dataDoc} fieldKey={this.fieldKey} ref={r => {this._vizRenderer = r ?? undefined;}} 
             margin={{ top: 10, right: 15, bottom: 15, left: 15 }} />;
+            default:
         } // prettier-ignore
+        return null;
     }
 
     @action
@@ -367,10 +371,13 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() im
             this._marqueeing = [e.clientX, e.clientY];
             const target = e.target as any;
             if (e.target && (target.className.includes('endOfContent') || (target.parentElement.className !== 'textLayer' && target.parentElement.parentElement?.className !== 'textLayer'))) {
+                /* empty */
             } else {
                 // if textLayer is hit, then we select text instead of using a marquee so clear out the marquee.
                 setTimeout(
-                    action(() => (this._marqueeing = undefined)),
+                    action(() => {
+                        this._marqueeing = undefined;
+                    }),
                     100
                 ); // bcz: hack .. anchor menu is setup within MarqueeAnnotator so we need to at least create the marqueeAnnotator even though we aren't using it.
 
@@ -405,9 +412,21 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() im
 
     render() {
         const scale = this._props.NativeDimScaling?.() || 1;
+        const toggleBtn = (name: string, type: DataVizView) => (
+            <Toggle
+                text={name}
+                toggleType={ToggleType.BUTTON}
+                type={Type.SEC}
+                color="black"
+                onClick={() => {
+                    this.layoutDoc._dataViz = type;
+                }}
+                toggleStatus={this.layoutDoc._dataViz === type}
+            />
+        );
         return !this.records.length ? (
             // displays how to get data into the DataVizBox if its empty
-            <div className="start-message">To create a DataViz box, either import / drag a CSV file into your canvas or copy a data table and use the command 'ctrl + p' to bring the data table to your canvas.</div>
+            <div className="start-message">To create a DataViz box, either import / drag a CSV file into your canvas or copy a data table and use the command (ctrl + p) to bring the data table to your canvas.</div>
         ) : (
             <div
                 className="dataViz-box"
@@ -422,14 +441,14 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() im
                 onWheel={e => e.stopPropagation()}
                 ref={this._mainCont}>
                 <div className="datatype-button">
-                    <Toggle text={'  TABLE  '} toggleType={ToggleType.BUTTON} type={Type.SEC} color={'black'} onClick={e => (this.layoutDoc._dataViz = DataVizView.TABLE)} toggleStatus={this.layoutDoc._dataViz === DataVizView.TABLE} />
-                    <Toggle text={'LINECHART'} toggleType={ToggleType.BUTTON} type={Type.SEC} color={'black'} onClick={e => (this.layoutDoc._dataViz = DataVizView.LINECHART)} toggleStatus={this.layoutDoc._dataViz === DataVizView.LINECHART} />
-                    <Toggle text={'HISTOGRAM'} toggleType={ToggleType.BUTTON} type={Type.SEC} color={'black'} onClick={e => (this.layoutDoc._dataViz = DataVizView.HISTOGRAM)} toggleStatus={this.layoutDoc._dataViz === DataVizView.HISTOGRAM} />
-                    <Toggle text={'PIE CHART'} toggleType={ToggleType.BUTTON} type={Type.SEC} color={'black'} onClick={e => (this.layoutDoc._dataViz = DataVizView.PIECHART)} toggleStatus={this.layoutDoc._dataViz == -DataVizView.PIECHART} />
+                    {toggleBtn('  TABLE  ', DataVizView.TABLE)}
+                    {toggleBtn('LINECHART', DataVizView.LINECHART)}
+                    {toggleBtn('HISTOGRAM', DataVizView.HISTOGRAM)}
+                    {toggleBtn('PIE CHART', DataVizView.PIECHART)}
                 </div>
 
                 {this.layoutDoc && this.layoutDoc.dataViz_asSchema ? (
-                    <div className={'liveSchema-checkBox'} style={{ width: this._props.width }}>
+                    <div className="liveSchema-checkBox" style={{ width: this._props.width }}>
                         <Checkbox color="primary" onChange={this.changeLiveSchemaCheckbox} checked={this.layoutDoc.dataViz_schemaLive as boolean} />
                         Display Live Updates to Canvas
                     </div>
@@ -445,7 +464,7 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() im
                         Document={this.Document}
                         layoutDoc={this.layoutDoc}
                         dataDoc={this.dataDoc}
-                        usePanelWidth={true}
+                        usePanelWidth
                         showSidebar={this.SidebarShown}
                         nativeWidth={NumCast(this.layoutDoc._nativeWidth)}
                         whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
diff --git a/src/client/views/nodes/DataVizBox/components/Histogram.tsx b/src/client/views/nodes/DataVizBox/components/Histogram.tsx
index 58cacef76..f0ffdbdcf 100644
--- a/src/client/views/nodes/DataVizBox/components/Histogram.tsx
+++ b/src/client/views/nodes/DataVizBox/components/Histogram.tsx
@@ -64,14 +64,13 @@ export class Histogram extends ObservableReactComponent<HistogramProps> {
     @computed get _histogramData() {
         if (this._props.axes.length < 1) return [];
         if (this._props.axes.length < 2) {
-            var ax0 = this._props.axes[0];
+            const ax0 = this._props.axes[0];
             if (!/[A-Za-z-:]/.test(this._props.records[0][ax0])) {
                 this.numericalXData = true;
             }
             return this._tableData.map(record => ({ [ax0]: record[this._props.axes[0]] }));
         }
-        var ax0 = this._props.axes[0];
-        var ax1 = this._props.axes[1];
+        const [ax0, ax1] = this._props.axes;
         if (!/[A-Za-z-:]/.test(this._props.records[0][ax0])) {
             this.numericalXData = true;
         }
@@ -82,11 +81,11 @@ export class Histogram extends ObservableReactComponent<HistogramProps> {
     }
 
     @computed get defaultGraphTitle() {
-        var ax0 = this._props.axes[0];
-        var ax1 = this._props.axes.length > 1 ? this._props.axes[1] : undefined;
+        const [ax0, ax1] = this._props.axes;
         if (this._props.axes.length < 2 || !ax1 || !/\d/.test(this._props.records[0][ax1]) || !this.numericalYData) {
             return ax0 + ' Histogram';
-        } else return ax0 + ' by ' + ax1 + ' Histogram';
+        }
+        return ax0 + ' by ' + ax1 + ' Histogram';
     }
 
     @computed get parentViz() {
@@ -112,8 +111,7 @@ export class Histogram extends ObservableReactComponent<HistogramProps> {
         );
     }
 
-    @action
-    restoreView = (data: Doc) => {};
+    restoreView = () => {};
     // create a document anchor that stores whatever is needed to reconstruct the viewing state (selection,zoom,etc)
     getAnchor = (pinProps?: PinProps) => {
         const anchor = Docs.Create.ConfigDocument({
@@ -133,36 +131,36 @@ export class Histogram extends ObservableReactComponent<HistogramProps> {
 
     // cleans data by converting numerical data to numbers and taking out empty cells
     data = (dataSet: any) => {
-        var validData = dataSet.filter((d: { [x: string]: unknown }) => !Object.keys(dataSet[0]).some(key => !d[key] || isNaN(d[key])));
+        const validData = dataSet.filter((d: { [x: string]: unknown }) => !Object.keys(dataSet[0]).some(key => !d[key] || isNaN(d[key])));
         const field = dataSet[0] ? Object.keys(dataSet[0])[0] : undefined;
         return !field
             ? []
             : validData.map((d: { [x: string]: any }) =>
                   !this.numericalXData //
                       ? d[field]
-                      : +d[field!].replace(/\$/g, '').replace(/\%/g, '').replace(/\</g, '')
+                      : +d[field!].replace(/\$/g, '').replace(/%/g, '').replace(/</g, '')
               );
     };
 
     // outlines the bar selected / hovered over
     highlightSelectedBar = (changeSelectedVariables: boolean, svg: any, eachRectWidth: any, pointerX: any, xAxisTitle: any, yAxisTitle: any, histDataSet: any) => {
-        var sameAsCurrent: boolean;
-        var barCounter = -1;
+        let sameAsCurrent: boolean;
+        let barCounter = -1;
         const selected = svg.selectAll('.histogram-bar').filter((d: any) => {
             barCounter++; // uses the order of bars and width of each bar to find which one the pointer is over
             if (barCounter * eachRectWidth <= pointerX && pointerX <= (barCounter + 1) * eachRectWidth) {
-                var showSelected = this.numericalYData
-                    ? this._histogramData.filter((data: { [x: string]: any }) => StrCast(data[xAxisTitle]).replace(/\$/g, '').replace(/\%/g, '').replace(/\</g, '') == d[0])[0]
-                    : histDataSet.filter((data: { [x: string]: any }) => data[xAxisTitle].replace(/\$/g, '').replace(/\%/g, '').replace(/\</g, '') == d[0])[0];
+                let showSelected = this.numericalYData
+                    ? this._histogramData.filter((data: { [x: string]: any }) => StrCast(data[xAxisTitle]).replace(/$/g, '').replace(/%/g, '').replace(/</g, '') === d[0])[0]
+                    : histDataSet.filter((data: { [x: string]: any }) => data[xAxisTitle].replace(/$/g, '').replace(/%/g, '').replace(/</g, '') === d[0])[0];
                 if (this.numericalXData) {
                     // calculating frequency
-                    if (d[0] && d[1] && d[0] != d[1]) {
+                    if (d[0] && d[1] && d[0] !== d[1]) {
                         showSelected = { [xAxisTitle]: d3.min(d) + ' to ' + d3.max(d), frequency: d.length };
                     } else if (!this.numericalYData) showSelected = { [xAxisTitle]: showSelected[xAxisTitle], frequency: d.length };
                 }
                 if (changeSelectedVariables) {
                     // for when a bar is selected - not just hovered over
-                    sameAsCurrent = this._currSelected ? showSelected[xAxisTitle] == this._currSelected![xAxisTitle] && showSelected[yAxisTitle] == this._currSelected![yAxisTitle] : false;
+                    sameAsCurrent = this._currSelected ? showSelected[xAxisTitle] === this._currSelected![xAxisTitle] && showSelected[yAxisTitle] === this._currSelected![yAxisTitle] : false;
                     this._currSelected = sameAsCurrent ? undefined : showSelected;
                     this.selectedData = sameAsCurrent ? undefined : d;
                 } else this.hoverOverData = d;
@@ -185,16 +183,16 @@ export class Histogram extends ObservableReactComponent<HistogramProps> {
         const xAxisTitle = Object.keys(dataSet[0])[0];
         const yAxisTitle = this.numericalYData ? Object.keys(dataSet[0])[1] : 'frequency';
         const uniqueArr: unknown[] = [...new Set(data)];
-        var numBins = this.numericalXData && Number.isInteger(data[0]) ? this.rangeVals.xMax! - this.rangeVals.xMin! : uniqueArr.length;
-        var translateXAxis = !this.numericalXData || numBins < this.maxBins ? width / (numBins + 1) / 2 : 0;
+        let numBins = this.numericalXData && Number.isInteger(data[0]) ? this.rangeVals.xMax! - this.rangeVals.xMin! : uniqueArr.length;
+        let translateXAxis = !this.numericalXData || numBins < this.maxBins ? width / (numBins + 1) / 2 : 0;
         if (numBins > this.maxBins) numBins = this.maxBins;
         const startingPoint = this.numericalXData ? this.rangeVals.xMin! : 0;
         const endingPoint = this.numericalXData ? this.rangeVals.xMax! : numBins;
 
         // converts data into Objects
-        var histDataSet = dataSet.filter((d: { [x: string]: unknown }) => !Object.keys(dataSet[0]).some(key => !d[key] || isNaN(d[key])));
+        let histDataSet = dataSet.filter((d: { [x: string]: unknown }) => !Object.keys(dataSet[0]).some(key => !d[key] || isNaN(d[key])));
         if (!this.numericalXData) {
-            var histStringDataSet: { [x: string]: unknown }[] = [];
+            const histStringDataSet: { [x: string]: unknown }[] = [];
             if (this.numericalYData) {
                 for (let i = 0; i < dataSet.length; i++) {
                     histStringDataSet.push({ [yAxisTitle]: dataSet[i][yAxisTitle], [xAxisTitle]: dataSet[i][xAxisTitle] });
@@ -204,15 +202,15 @@ export class Histogram extends ObservableReactComponent<HistogramProps> {
                     histStringDataSet.push({ [yAxisTitle]: 0, [xAxisTitle]: uniqueArr[i] });
                 }
                 for (let i = 0; i < data.length; i++) {
-                    let barData = histStringDataSet.filter(each => each[xAxisTitle] == data[i]);
-                    histStringDataSet.filter(each => each[xAxisTitle] == data[i])[0][yAxisTitle] = Number(barData[0][yAxisTitle]) + 1;
+                    const barData = histStringDataSet.filter(each => each[xAxisTitle] === data[i]);
+                    histStringDataSet.filter(each => each[xAxisTitle] === data[i])[0][yAxisTitle] = Number(barData[0][yAxisTitle]) + 1;
                 }
             }
             histDataSet = histStringDataSet;
         }
 
         // initial graph and binning data for histogram
-        var svg = (this._histogramSvg = d3
+        const svg = (this._histogramSvg = d3
             .select(this._histogramRef.current)
             .append('svg')
             .attr('class', 'graph')
@@ -220,23 +218,21 @@ export class Histogram extends ObservableReactComponent<HistogramProps> {
             .attr('height', height + this._props.margin.top + this._props.margin.bottom)
             .append('g')
             .attr('transform', 'translate(' + this._props.margin.left + ',' + this._props.margin.top + ')'));
-        var x = d3
+        let x = d3
             .scaleLinear()
             .domain(this.numericalXData ? [startingPoint!, endingPoint!] : [0, numBins])
             .range([0, width]);
-        var histogram = d3
+        const histogram = d3
             .histogram()
-            .value(function (d) {
-                return d;
-            })
+            .value(d => d)
             .domain([startingPoint!, endingPoint!])
             .thresholds(x.ticks(numBins));
-        var bins = histogram(data);
-        var eachRectWidth = width / bins.length;
-        var graphStartingPoint = bins[0].x1 && bins[1] ? bins[0].x1! - (bins[1].x1! - bins[1].x0!) : 0;
+        const bins = histogram(data);
+        let eachRectWidth = width / bins.length;
+        const graphStartingPoint = bins[0].x1 && bins[1] ? bins[0].x1! - (bins[1].x1! - bins[1].x0!) : 0;
         bins[0].x0 = graphStartingPoint;
         x = x.domain([graphStartingPoint, endingPoint]).range([0, Number.isInteger(this.rangeVals.xMin!) ? width - eachRectWidth : width]);
-        var xAxis;
+        let xAxis;
 
         // more calculations based on bins
         // x-axis
@@ -245,9 +241,9 @@ export class Histogram extends ObservableReactComponent<HistogramProps> {
             // uniqueArr.sort()
             histDataSet.sort();
             for (let i = 0; i < data.length; i++) {
-                var index = 0;
+                let index = 0;
                 for (let j = 0; j < uniqueArr.length; j++) {
-                    if (uniqueArr[j] == data[i]) {
+                    if (uniqueArr[j] === data[i]) {
                         index = j;
                     }
                 }
@@ -255,7 +251,9 @@ export class Histogram extends ObservableReactComponent<HistogramProps> {
             }
             bins.pop();
             eachRectWidth = width / bins.length;
-            bins.forEach(d => (d.x0 = d.x0!));
+            bins.forEach(d => {
+                d.x0 = d.x0!;
+            });
             xAxis = d3
                 .axisBottom(x)
                 .ticks(bins.length > 1 ? bins.length - 1 : 1)
@@ -265,12 +263,12 @@ export class Histogram extends ObservableReactComponent<HistogramProps> {
             x.domain([0, bins.length - 1]);
             translateXAxis = eachRectWidth / 2;
         } else {
-            var allSame = true;
-            for (var i = 0; i < bins.length; i++) {
+            let allSame = true;
+            for (let i = 0; i < bins.length; i++) {
                 if (bins[i] && bins[i][0]) {
-                    var compare = bins[i][0];
+                    const compare = bins[i][0];
                     for (let j = 1; j < bins[i].length; j++) {
-                        if (bins[i][j] != compare) allSame = false;
+                        if (bins[i][j] !== compare) allSame = false;
                     }
                 }
             }
@@ -279,8 +277,8 @@ export class Histogram extends ObservableReactComponent<HistogramProps> {
                 eachRectWidth = width / bins.length;
             } else {
                 eachRectWidth = width / (bins.length + 1);
-                var tickDiff = bins.length >= 2 ? bins[bins.length - 2].x1! - bins[bins.length - 2].x0! : 0;
-                var curDomain = x.domain();
+                const tickDiff = bins.length >= 2 ? bins[bins.length - 2].x1! - bins[bins.length - 2].x0! : 0;
+                const curDomain = x.domain();
                 x.domain([curDomain[0], curDomain[0] + tickDiff * bins.length]);
             }
 
@@ -288,16 +286,13 @@ export class Histogram extends ObservableReactComponent<HistogramProps> {
             x.range([0, width - eachRectWidth]);
         }
         // y-axis
-        const maxFrequency = this.numericalYData
-            ? d3.max(histDataSet, function (d: any) {
-                  return d[yAxisTitle] ? Number(d[yAxisTitle]!.replace(/\$/g, '').replace(/\%/g, '').replace(/\</g, '')) : 0;
-              })
-            : d3.max(bins, function (d) {
-                  return d.length;
-              });
-        var y = d3.scaleLinear().range([height, 0]);
+        const maxFrequency = this.numericalYData ? 
+              d3.max(histDataSet, (d: any) => (d[yAxisTitle] ? Number(d[yAxisTitle]!.replace(/\$/g, '')
+                .replace(/%/g, '').replace(/</g, '')) : 0)) : 
+              d3.max(bins, d => d.length); // prettier-ignore
+        const y = d3.scaleLinear().range([height, 0]);
         y.domain([0, +maxFrequency!]);
-        var yAxis = d3.axisLeft(y).ticks(maxFrequency!);
+        const yAxis = d3.axisLeft(y).ticks(maxFrequency!);
         if (this.numericalYData) {
             const yScale = scaleCreatorNumerical(0, Number(maxFrequency), height, 0);
             yAxisCreator(svg.append('g'), width, yScale);
@@ -312,18 +307,17 @@ export class Histogram extends ObservableReactComponent<HistogramProps> {
         const onPointClick = action((e: any) => this.highlightSelectedBar(true, svg, eachRectWidth, d3.pointer(e)[0], xAxisTitle, yAxisTitle, histDataSet));
         const onHover = action((e: any) => {
             this.highlightSelectedBar(false, svg, eachRectWidth, d3.pointer(e)[0], xAxisTitle, yAxisTitle, histDataSet);
+            // eslint-disable-next-line no-use-before-define
             updateHighlights();
         });
-        const mouseOut = action((e: any) => {
+        const mouseOut = action(() => {
             this.hoverOverData = undefined;
+            // eslint-disable-next-line no-use-before-define
             updateHighlights();
         });
         const updateHighlights = () => {
-            const hoverOverBar = this.hoverOverData;
-            const selectedData = this.selectedData;
-            svg.selectAll('rect').attr('class', function (d: any) {
-                return (hoverOverBar && hoverOverBar[0] == d[0]) || (selectedData && selectedData[0] == d[0]) ? 'histogram-bar hover' : 'histogram-bar';
-            });
+            const { hoverOverData: hoverOverBar, selectedData } = this;
+            svg.selectAll('rect').attr('class', (d: any) => ((hoverOverBar && hoverOverBar[0] === d[0]) || (selectedData && selectedData[0] === d[0]) ? 'histogram-bar hover' : 'histogram-bar'));
         };
         svg.on('click', onPointClick).on('mouseover', onHover).on('mouseout', mouseOut);
 
@@ -333,7 +327,7 @@ export class Histogram extends ObservableReactComponent<HistogramProps> {
             .style('text-anchor', 'middle')
             .text(xAxisTitle);
         svg.append('text')
-            .attr('transform', 'rotate(-90)' + ' ' + 'translate( 0, ' + -10 + ')')
+            .attr('transform', 'rotate(-90) translate( 0, ' + -10 + ')')
             .attr('x', -(height / 2))
             .attr('y', -20)
             .style('text-anchor', 'middle')
@@ -341,7 +335,7 @@ export class Histogram extends ObservableReactComponent<HistogramProps> {
         d3.format('.0f');
 
         // draw bars
-        var selected = this.selectedData;
+        const selected = this.selectedData;
         svg.selectAll('rect')
             .data(bins)
             .enter()
@@ -349,49 +343,34 @@ export class Histogram extends ObservableReactComponent<HistogramProps> {
             .attr(
                 'transform',
                 this.numericalYData
-                    ? function (d) {
-                          const eachData = histDataSet.filter((data: { [x: string]: number }) => {
-                              return data[xAxisTitle] == d[0];
-                          });
-                          const length = eachData.length ? eachData[0][yAxisTitle].replace(/\$/g, '').replace(/\%/g, '').replace(/\</g, '') : 0;
+                    ? d => {
+                          const eachData = histDataSet.filter((hData: { [x: string]: number }) => hData[xAxisTitle] === d[0]);
+                          const length = eachData.length ? eachData[0][yAxisTitle].replace(/\$/g, '').replace(/%/g, '').replace(/</g, '') : 0;
                           return 'translate(' + x(d.x0!) + ',' + y(length) + ')';
                       }
-                    : function (d) {
-                          return 'translate(' + x(d.x0!) + ',' + y(d.length) + ')';
-                      }
+                    : d => 'translate(' + x(d.x0!) + ',' + y(d.length) + ')'
             )
             .attr(
                 'height',
                 this.numericalYData
-                    ? function (d) {
-                          const eachData = histDataSet.filter((data: { [x: string]: number }) => {
-                              return data[xAxisTitle] == d[0];
-                          });
-                          const length = eachData.length ? eachData[0][yAxisTitle].replace(/\$/g, '').replace(/\%/g, '').replace(/\</g, '') : 0;
+                    ? d => {
+                          const eachData = histDataSet.filter((hData: { [x: string]: number }) => hData[xAxisTitle] === d[0]);
+                          const length = eachData.length ? eachData[0][yAxisTitle].replace(/\$/g, '').replace(/%/g, '').replace(/</g, '') : 0;
                           return height - y(length);
                       }
-                    : function (d) {
-                          return height - y(d.length);
-                      }
+                    : d => height - y(d.length)
             )
             .attr('width', eachRectWidth)
-            .attr(
-                'class',
-                selected
-                    ? function (d) {
-                          return selected && selected[0] === d[0] ? 'histogram-bar hover' : 'histogram-bar';
-                      }
-                    : function (d) {
-                          return 'histogram-bar';
-                      }
-            )
+            .attr('class', selected ? d => (selected && selected[0] === d[0] ? 'histogram-bar hover' : 'histogram-bar') : () => 'histogram-bar')
             .attr('fill', d => {
-                var barColor;
+                let barColor;
                 const barColors = StrListCast(this._props.layoutDoc.dataViz_histogram_barColors).map(each => each.split('::'));
                 barColors.forEach(each => {
-                    if (d[0] && d[0].toString() && each[0] == d[0].toString()) barColor = each[1];
+                    // eslint-disable-next-line prefer-destructuring
+                    if (d[0] && d[0].toString() && each[0] === d[0].toString()) barColor = each[1];
                     else {
                         const range = StrCast(each[0]).split(' to ');
+                        // eslint-disable-next-line prefer-destructuring
                         if (Number(range[0]) <= d[0] && d[0] <= Number(range[1])) barColor = each[1];
                     }
                 });
@@ -401,7 +380,7 @@ export class Histogram extends ObservableReactComponent<HistogramProps> {
 
     @action changeSelectedColor = (color: string) => {
         this.curBarSelected.attr('fill', color);
-        const barName = StrCast(this._currSelected[this._props.axes[0]].replace(/\$/g, '').replace(/\%/g, '').replace(/\</g, ''));
+        const barName = StrCast(this._currSelected[this._props.axes[0]].replace(/\$/g, '').replace(/%/g, '').replace(/</g, ''));
 
         const barColors = Cast(this._props.layoutDoc.dataViz_histogram_barColors, listSpec('string'), null);
         barColors.forEach(each => each.split('::')[0] === barName && barColors.splice(barColors.indexOf(each), 1));
@@ -410,22 +389,24 @@ export class Histogram extends ObservableReactComponent<HistogramProps> {
 
     @action eraseSelectedColor = () => {
         this.curBarSelected.attr('fill', this._props.layoutDoc.dataViz_histogram_defaultColor);
-        const barName = StrCast(this._currSelected[this._props.axes[0]].replace(/\$/g, '').replace(/\%/g, '').replace(/\</g, ''));
+        const barName = StrCast(this._currSelected[this._props.axes[0]].replace(/\$/g, '').replace(/%/g, '').replace(/</g, ''));
 
         const barColors = Cast(this._props.layoutDoc.dataViz_histogram_barColors, listSpec('string'), null);
         barColors.forEach(each => each.split('::')[0] === barName && barColors.splice(barColors.indexOf(each), 1));
     };
 
     updateBarColors = () => {
-        var svg = this._histogramSvg;
+        const svg = this._histogramSvg;
         if (svg)
             svg.selectAll('rect').attr('fill', (d: any) => {
-                var barColor;
+                let barColor;
                 const barColors = StrListCast(this._props.layoutDoc.dataViz_histogram_barColors).map(each => each.split('::'));
                 barColors.forEach(each => {
-                    if (d[0] && d[0].toString() && each[0] == d[0].toString()) barColor = each[1];
+                    // eslint-disable-next-line prefer-destructuring
+                    if (d[0] && d[0].toString() && each[0] === d[0].toString()) barColor = each[1];
                     else {
                         const range = StrCast(each[0]).split(' to ');
+                        // eslint-disable-next-line prefer-destructuring
                         if (Number(range[0]) <= d[0] && d[0] <= Number(range[1])) barColor = each[1];
                     }
                 });
@@ -436,38 +417,41 @@ export class Histogram extends ObservableReactComponent<HistogramProps> {
     render() {
         this.updateBarColors();
         this._histogramData;
-        var curSelectedBarName = '';
-        var titleAccessor: any = 'dataViz_histogram_title';
-        if (this._props.axes.length == 2) titleAccessor = titleAccessor + this._props.axes[0] + '-' + this._props.axes[1];
-        else if (this._props.axes.length > 0) titleAccessor = titleAccessor + this._props.axes[0];
+        let curSelectedBarName = '';
+        let titleAccessor: any = 'dataViz_histogram_title';
+        if (this._props.axes.length === 2) titleAccessor = titleAccessor + this._props.axes[0] + '-' + this._props.axes[1];
+        else if (this._props.axes.length > 0) titleAccessor += this._props.axes[0];
         if (!this._props.layoutDoc[titleAccessor]) this._props.layoutDoc[titleAccessor] = this.defaultGraphTitle;
         if (!this._props.layoutDoc.dataViz_histogram_defaultColor) this._props.layoutDoc.dataViz_histogram_defaultColor = '#69b3a2';
         if (!this._props.layoutDoc.dataViz_histogram_barColors) this._props.layoutDoc.dataViz_histogram_barColors = new List<string>();
-        var selected = 'none';
+        let selected = 'none';
         if (this._currSelected) {
-            curSelectedBarName = StrCast(this._currSelected![this._props.axes[0]].replace(/\$/g, '').replace(/\%/g, '').replace(/\</g, ''));
+            curSelectedBarName = StrCast(this._currSelected![this._props.axes[0]].replace(/\$/g, '').replace(/%/g, '').replace(/</g, ''));
             selected = '{ ';
-            Object.keys(this._currSelected).forEach(key =>
+            Object.keys(this._currSelected).forEach(key => {
                 key //
                     ? (selected += key + ': ' + this._currSelected[key] + ', ')
-                    : ''
-            );
+                    : '';
+            });
             selected = selected.substring(0, selected.length - 2) + ' }';
-            if (this._props.titleCol != '' && (!this._currSelected['frequency'] || this._currSelected['frequency'] < 10)) {
+            if (this._props.titleCol !== '' && (!this._currSelected.frequency || this._currSelected.frequency < 10)) {
                 selected += '\n' + this._props.titleCol + ': ';
                 this._tableData.forEach(each => {
-                    if (this._currSelected[this._props.axes[0]] == each[this._props.axes[0]]) {
+                    if (this._currSelected[this._props.axes[0]] === each[this._props.axes[0]]) {
                         if (this._props.axes[1]) {
-                            if (this._currSelected[this._props.axes[1]] == each[this._props.axes[1]]) selected += each[this._props.titleCol] + ', ';
+                            if (this._currSelected[this._props.axes[1]] === each[this._props.axes[1]]) selected += each[this._props.titleCol] + ', ';
                         } else selected += each[this._props.titleCol] + ', ';
                     }
                 });
                 selected = selected.slice(0, -1).slice(0, -1);
             }
         }
-        var selectedBarColor;
-        var barColors = StrListCast(this._props.layoutDoc.histogramBarColors).map(each => each.split('::'));
-        barColors.forEach(each => each[0] === curSelectedBarName && (selectedBarColor = each[1]));
+        let selectedBarColor;
+        const barColors = StrListCast(this._props.layoutDoc.histogramBarColors).map(each => each.split('::'));
+        barColors.forEach(each => {
+            // eslint-disable-next-line prefer-destructuring
+            each[0] === curSelectedBarName && (selectedBarColor = each[1]);
+        });
 
         if (this._histogramData.length > 0 || !this.parentViz) {
             return this._props.axes.length >= 1 ? (
@@ -476,45 +460,51 @@ export class Histogram extends ObservableReactComponent<HistogramProps> {
                         <EditableText
                             val={StrCast(this._props.layoutDoc[titleAccessor])}
                             setVal={undoable(
-                                action(val => (this._props.layoutDoc[titleAccessor] = val as string)),
+                                action(val => {
+                                    this._props.layoutDoc[titleAccessor] = val as string;
+                                }),
                                 'Change Graph Title'
                             )}
-                            color={'black'}
+                            color="black"
                             size={Size.LARGE}
                             fillWidth
                         />
                         &nbsp; &nbsp;
                         <ColorPicker
-                            tooltip={'Change Default Bar Color'}
+                            tooltip="Change Default Bar Color"
                             type={Type.SEC}
                             icon={<FaFillDrip />}
                             selectedColor={StrCast(this._props.layoutDoc.dataViz_histogram_defaultColor)}
-                            setFinalColor={undoable(color => (this._props.layoutDoc.dataViz_histogram_defaultColor = color), 'Change Default Bar Color')}
-                            setSelectedColor={undoable(color => (this._props.layoutDoc.dataViz_histogram_defaultColor = color), 'Change Default Bar Color')}
+                            setFinalColor={undoable(color => {
+                                this._props.layoutDoc.dataViz_histogram_defaultColor = color;
+                            }, 'Change Default Bar Color')}
+                            setSelectedColor={undoable(color => {
+                                this._props.layoutDoc.dataViz_histogram_defaultColor = color;
+                            }, 'Change Default Bar Color')}
                             size={Size.XSMALL}
                         />
                     </div>
                     <div ref={this._histogramRef} />
-                    {selected != 'none' ? (
-                        <div className={'selected-data'}>
+                    {selected !== 'none' ? (
+                        <div className="selected-data">
                             Selected: {selected}
                             &nbsp; &nbsp;
                             <ColorPicker
-                                tooltip={'Change Bar Color'}
+                                tooltip="Change Bar Color"
                                 type={Type.SEC}
                                 icon={<FaFillDrip />}
-                                selectedColor={selectedBarColor ? selectedBarColor : this.curBarSelected.attr('fill')}
+                                selectedColor={selectedBarColor || this.curBarSelected.attr('fill')}
                                 setFinalColor={undoable(color => this.changeSelectedColor(color), 'Change Selected Bar Color')}
                                 setSelectedColor={undoable(color => this.changeSelectedColor(color), 'Change Selected Bar Color')}
                                 size={Size.XSMALL}
                             />
                             &nbsp;
                             <IconButton
-                                icon={<FontAwesomeIcon icon={'eraser'} />}
+                                icon={<FontAwesomeIcon icon="eraser" />}
                                 size={Size.XSMALL}
-                                color={'black'}
+                                color="black"
                                 type={Type.SEC}
-                                tooltip={'Revert to the default bar color'}
+                                tooltip="Revert to the default bar color"
                                 onClick={undoable(
                                     action(() => this.eraseSelectedColor()),
                                     'Change Selected Bar Color'
@@ -524,7 +514,7 @@ export class Histogram extends ObservableReactComponent<HistogramProps> {
                     ) : null}
                 </div>
             ) : (
-                <span className="chart-container"> {'first use table view to select a column to graph'}</span>
+                <span className="chart-container"> first use table view to select a column to graph</span>
             );
         }
         // when it is a brushed table and the incoming table doesn't have any rows selected
diff --git a/src/client/views/nodes/DataVizBox/components/LineChart.tsx b/src/client/views/nodes/DataVizBox/components/LineChart.tsx
index c667a15de..8105adf1e 100644
--- a/src/client/views/nodes/DataVizBox/components/LineChart.tsx
+++ b/src/client/views/nodes/DataVizBox/components/LineChart.tsx
@@ -3,7 +3,7 @@ import * as d3 from 'd3';
 import { IReactionDisposer, action, computed, makeObservable, observable, reaction } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
-import { Doc, DocListCast, NumListCast, StrListCast } from '../../../../../fields/Doc';
+import { Doc, DocListCast, NumListCast } from '../../../../../fields/Doc';
 import { List } from '../../../../../fields/List';
 import { listSpec } from '../../../../../fields/Schema';
 import { Cast, DocCast, StrCast } from '../../../../../fields/Types';
@@ -63,7 +63,6 @@ export class LineChart extends ObservableReactComponent<LineChartProps> {
         return !this.parentViz ? this._props.records : this._tableDataIds.map(rowId => this._props.records[rowId]);
     }
     @computed get _lineChartData() {
-        var guids = StrListCast(this._props.layoutDoc.dataViz_rowIds);
         if (this._props.axes.length <= 1) return [];
         return this._tableData.map(record => ({ x: Number(record[this._props.axes[0]]), y: Number(record[this._props.axes[1]]) })).sort((a, b) => (a.x < b.x ? -1 : 1));
     }
@@ -103,7 +102,7 @@ export class LineChart extends ObservableReactComponent<LineChartProps> {
         );
         this._disposers.annos = reaction(
             () => DocListCast(this._props.dataDoc[this._props.fieldKey + '_annotations']),
-            annotations => {
+            (/* annotations */) => {
                 // modify how d3 renders so that anything in this annotations list would be potentially highlighted in some way
                 // could be blue colored to make it look like anchor
                 // this.drawAnnotations()
@@ -176,7 +175,7 @@ export class LineChart extends ObservableReactComponent<LineChartProps> {
     getAnchor = (pinProps?: PinProps) => {
         const anchor = Docs.Create.ConfigDocument({
             //
-            title: 'line doc selection' + this._currSelected?.x,
+            title: 'line doc selection' + (this._currSelected?.x ?? ''),
         });
         PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: pinProps?.pinData }, this._props.Document);
         anchor.config_dataVizSelection = this._currSelected ? new List<number>([this._currSelected.x, this._currSelected.y]) : undefined;
@@ -192,11 +191,12 @@ export class LineChart extends ObservableReactComponent<LineChartProps> {
     }
 
     @computed get defaultGraphTitle() {
-        var ax0 = this._props.axes[0];
-        var ax1 = this._props.axes.length > 1 ? this._props.axes[1] : undefined;
+        const ax0 = this._props.axes[0];
+        const ax1 = this._props.axes.length > 1 ? this._props.axes[1] : undefined;
         if (this._props.axes.length < 2 || !/\d/.test(this._props.records[0][ax0]) || !ax1) {
             return ax0 + ' Line Chart';
-        } else return ax1 + ' by ' + ax0 + ' Line Chart';
+        }
+        return ax1 + ' by ' + ax0 + ' Line Chart';
     }
 
     setupTooltip() {
@@ -216,9 +216,11 @@ export class LineChart extends ObservableReactComponent<LineChartProps> {
     @action
     setCurrSelected(x?: number, y?: number) {
         // TODO: nda - get rid of svg element in the list?
-        if (this._currSelected && this._currSelected.x == x && this._currSelected.y == y) this._currSelected = undefined;
+        if (this._currSelected && this._currSelected.x === x && this._currSelected.y === y) this._currSelected = undefined;
         else this._currSelected = x !== undefined && y !== undefined ? { x, y } : undefined;
-        this._props.records.forEach(record => record[this._props.axes[0]] === x && record[this._props.axes[1]] === y && (record.selected = true));
+        this._props.records.forEach(record => {
+            record[this._props.axes[0]] === x && record[this._props.axes[1]] === y && (record.selected = true);
+        });
     }
 
     drawDataPoints(data: DataPoint[], idx: number, xScale: d3.ScaleLinear<number, number, never>, yScale: d3.ScaleLinear<number, number, never>) {
@@ -242,13 +244,13 @@ export class LineChart extends ObservableReactComponent<LineChartProps> {
         d3.select(this._lineChartRef.current).select('svg').remove();
         d3.select(this._lineChartRef.current).select('.tooltip').remove();
 
-        var { xMin, xMax, yMin, yMax } = rangeVals;
+        let { xMin, xMax, yMin, yMax } = rangeVals;
         if (xMin === undefined || xMax === undefined || yMin === undefined || yMax === undefined) {
             return;
         }
 
         // adding svg
-        const margin = this._props.margin;
+        const { margin } = this._props;
         const svg = (this._lineChartSvg = d3
             .select(this._lineChartRef.current)
             .append('svg')
@@ -258,15 +260,15 @@ export class LineChart extends ObservableReactComponent<LineChartProps> {
             .append('g')
             .attr('transform', `translate(${margin.left}, ${margin.top})`));
 
-        var validSecondData;
+        let validSecondData;
         if (this._props.axes.length > 2) {
             // for when there are 2 lines on the chart
-            var next = this._tableData.map(record => ({ x: Number(record[this._props.axes[0]]), y: Number(record[this._props.axes[2]]) })).sort((a, b) => (a.x < b.x ? -1 : 1));
+            const next = this._tableData.map(record => ({ x: Number(record[this._props.axes[0]]), y: Number(record[this._props.axes[2]]) })).sort((a, b) => (a.x < b.x ? -1 : 1));
             validSecondData = next.filter(d => {
                 if (!d.x || isNaN(d.x) || !d.y || isNaN(d.y)) return false;
                 return true;
             });
-            var secondDataRange = minMaxRange([validSecondData]);
+            const secondDataRange = minMaxRange([validSecondData]);
             if (secondDataRange.xMax! > xMax) xMax = secondDataRange.xMax;
             if (secondDataRange.yMax! > yMax) yMax = secondDataRange.yMax;
             if (secondDataRange.xMin! < xMin) xMin = secondDataRange.xMin;
@@ -290,45 +292,31 @@ export class LineChart extends ObservableReactComponent<LineChartProps> {
             svg.append('path').attr('stroke', 'red');
 
             // legend
-            var color = d3.scaleOrdinal().range(['black', 'blue']).domain([this._props.axes[1], this._props.axes[2]]);
+            const color: any = d3.scaleOrdinal().range(['black', 'blue']).domain([this._props.axes[1], this._props.axes[2]]);
             svg.selectAll('mydots')
                 .data([this._props.axes[1], this._props.axes[2]])
                 .enter()
                 .append('circle')
                 .attr('cx', 5)
-                .attr('cy', function (d, i) {
-                    return -30 + i * 15;
-                })
+                .attr('cy', (d, i) => -30 + i * 15)
                 .attr('r', 7)
-                .style('fill', function (d) {
-                    return color(d);
-                });
+                .style('fill', d => color(d));
             svg.selectAll('mylabels')
                 .data([this._props.axes[1], this._props.axes[2]])
                 .enter()
                 .append('text')
                 .attr('x', 25)
-                .attr('y', function (d, i) {
-                    return -30 + i * 15;
-                })
-                .style('fill', function (d) {
-                    return color(d);
-                })
-                .text(function (d) {
-                    return d;
-                })
+                .attr('y', (d, i) => -30 + i * 15)
+                .style('fill', d => color(d))
+                .text(d => d)
                 .attr('text-anchor', 'left')
                 .style('alignment-baseline', 'middle');
         }
 
         // get valid data points
         const data = dataSet[0];
-        var validData = data.filter(d => {
-            Object.keys(data[0]).map(key => {
-                if (!d[key] || isNaN(d[key])) return false;
-            });
-            return true;
-        });
+        const keys = Object.keys(data[0]);
+        const validData = data.filter(d => !keys.some(key => isNaN(d[key])));
 
         // draw the plot line
         drawLine(svg.append('path'), validData, lineGen, false);
@@ -355,7 +343,7 @@ export class LineChart extends ObservableReactComponent<LineChartProps> {
             const x0 = bisect(data, xScale.invert(xPos - 5)); // shift x by -5 so that you can reach points on the left-side axis
             const d0 = data[x0];
             // find .circle-d1 with data-x = d0.x and data-y = d0.y
-            const selected = svg.selectAll('.datapoint').filter((d: any) => d['data-x'] === d0.x && d['data-y'] === d0.y);
+            svg.selectAll('.datapoint').filter((d: any) => d['data-x'] === d0.x && d['data-y'] === d0.y);
             this.setCurrSelected(d0.x, d0.y);
             this.updateTooltip(higlightFocusPt, xScale, d0, yScale, tooltip);
         });
@@ -378,7 +366,7 @@ export class LineChart extends ObservableReactComponent<LineChartProps> {
             .style('text-anchor', 'middle')
             .text(this._props.axes[0]);
         svg.append('text')
-            .attr('transform', 'rotate(-90)' + ' ' + 'translate( 0, ' + -10 + ')')
+            .attr('transform', 'rotate(-90) translate(0, -10)')
             .attr('x', -(height / 2))
             .attr('y', -30)
             .attr('height', 20)
@@ -404,57 +392,60 @@ export class LineChart extends ObservableReactComponent<LineChartProps> {
     }
 
     render() {
-        var titleAccessor: any = 'dataViz_lineChart_title';
-        if (this._props.axes.length == 2) titleAccessor = titleAccessor + this._props.axes[0] + '-' + this._props.axes[1];
-        else if (this._props.axes.length > 0) titleAccessor = titleAccessor + this._props.axes[0];
+        let titleAccessor: any = 'dataViz_lineChart_title';
+        if (this._props.axes.length === 2) titleAccessor = titleAccessor + this._props.axes[0] + '-' + this._props.axes[1];
+        else if (this._props.axes.length > 0) titleAccessor += this._props.axes[0];
         if (!this._props.layoutDoc[titleAccessor]) this._props.layoutDoc[titleAccessor] = this.defaultGraphTitle;
         const selectedPt = this._currSelected ? `{ ${this._props.axes[0]}: ${this._currSelected.x} ${this._props.axes[1]}: ${this._currSelected.y} }` : 'none';
-        var selectedTitle = '';
+        let selectedTitle = '';
         if (this._currSelected && this._props.titleCol) {
             selectedTitle += '\n' + this._props.titleCol + ': ';
             this._tableData.forEach(each => {
-                var mapThisEntry = false;
-                if (this._currSelected.x == each[this._props.axes[0]] && this._currSelected.y == each[this._props.axes[1]]) mapThisEntry = true;
-                else if (this._currSelected.y == each[this._props.axes[0]] && this._currSelected.x == each[this._props.axes[1]]) mapThisEntry = true;
+                let mapThisEntry = false;
+                if (this._currSelected.x === each[this._props.axes[0]] && this._currSelected.y === each[this._props.axes[1]]) mapThisEntry = true;
+                else if (this._currSelected.y === each[this._props.axes[0]] && this._currSelected.x === each[this._props.axes[1]]) mapThisEntry = true;
                 if (mapThisEntry) selectedTitle += each[this._props.titleCol] + ', ';
             });
             selectedTitle = selectedTitle.slice(0, -1).slice(0, -1);
         }
-        if (this._lineChartData.length > 0 || !this.parentViz || this.parentViz.length == 0) {
+        if (this._lineChartData.length > 0 || !this.parentViz || this.parentViz.length === 0) {
             return this._props.axes.length >= 2 && /\d/.test(this._props.records[0][this._props.axes[0]]) && /\d/.test(this._props.records[0][this._props.axes[1]]) ? (
                 <div className="chart-container" style={{ width: this._props.width + this._props.margin.right }}>
                     <div className="graph-title">
                         <EditableText
                             val={StrCast(this._props.layoutDoc[titleAccessor])}
                             setVal={undoable(
-                                action(val => (this._props.layoutDoc[titleAccessor] = val as string)),
+                                action(val => {
+                                    this._props.layoutDoc[titleAccessor] = val as string;
+                                }),
                                 'Change Graph Title'
                             )}
-                            color={'black'}
+                            color="black"
                             size={Size.LARGE}
                             fillWidth
                         />
                     </div>
                     <div ref={this._lineChartRef} />
-                    {selectedPt != 'none' ? (
-                        <div className={'selected-data'}>
+                    {selectedPt !== 'none' ? (
+                        <div className="selected-data">
                             {`Selected: ${selectedPt}`}
                             {`${selectedTitle}`}
                             <Button
-                                onClick={e => {
+                                onClick={() => {
                                     this._props.vizBox.sidebarBtnDown;
                                     this._props.vizBox.sidebarAddDocument;
-                                }}></Button>
+                                }}
+                            />
                         </div>
                     ) : null}
                 </div>
             ) : (
-                <span className="chart-container"> {'first use table view to select two numerical axes to plot'}</span>
-            );
-        } else
-            return (
-                // when it is a brushed table and the incoming table doesn't have any rows selected
-                <div className="chart-container">Selected rows of data from the incoming DataVizBox to display.</div>
+                <span className="chart-container"> first use table view to select two numerical axes to plot</span>
             );
+        }
+        return (
+            // when it is a brushed table and the incoming table doesn't have any rows selected
+            <div className="chart-container">Selected rows of data from the incoming DataVizBox to display.</div>
+        );
     }
 }
diff --git a/src/client/views/nodes/DataVizBox/components/TableBox.tsx b/src/client/views/nodes/DataVizBox/components/TableBox.tsx
index 15959c61d..55c221046 100644
--- a/src/client/views/nodes/DataVizBox/components/TableBox.tsx
+++ b/src/client/views/nodes/DataVizBox/components/TableBox.tsx
@@ -1,3 +1,5 @@
+/* eslint-disable jsx-a11y/no-noninteractive-tabindex */
+/* eslint-disable jsx-a11y/no-static-element-interactions */
 import { Button, Type } from 'browndash-components';
 import { IReactionDisposer, action, computed, makeObservable, observable, reaction } from 'mobx';
 import { observer } from 'mobx-react';
@@ -13,7 +15,9 @@ import { ObservableReactComponent } from '../../../ObservableReactComponent';
 import { DocumentView } from '../../DocumentView';
 import { DataVizView } from '../DataVizBox';
 import './Chart.scss';
+
 const { DATA_VIZ_TABLE_ROW_HEIGHT } = require('../../../global/globalCssVariables.module.scss'); // prettier-ignore
+
 interface TableBoxProps {
     Document: Doc;
     layoutDoc: Doc;
@@ -71,7 +75,7 @@ export class TableBox extends ObservableReactComponent<TableBoxProps> {
     }
 
     @computed get columns() {
-        return this._tableData.length ? Array.from(Object.keys(this._tableData[0])).filter(header => header != '' && header != undefined) : [];
+        return this._tableData.length ? Array.from(Object.keys(this._tableData[0])).filter(header => header !== '' && header !== undefined) : [];
     }
 
     // updates the 'dataViz_selectedRows' and 'dataViz_highlightedRows' fields to no longer include rows that aren't in the table
@@ -108,13 +112,11 @@ export class TableBox extends ObservableReactComponent<TableBoxProps> {
             if (highlited?.includes(rowId)) highlited.splice(highlited.indexOf(rowId), 1);
             else highlited?.push(rowId);
             if (!selected?.includes(rowId)) selected?.push(rowId);
-        } else {
+        } else if (selected?.includes(rowId)) {
             // selecting a row
-            if (selected?.includes(rowId)) {
-                if (highlited?.includes(rowId)) highlited.splice(highlited.indexOf(rowId), 1);
-                selected.splice(selected.indexOf(rowId), 1);
-            } else selected?.push(rowId);
-        }
+            if (highlited?.includes(rowId)) highlited.splice(highlited.indexOf(rowId), 1);
+            selected.splice(selected.indexOf(rowId), 1);
+        } else selected?.push(rowId);
         e.stopPropagation();
     };
 
@@ -124,7 +126,7 @@ export class TableBox extends ObservableReactComponent<TableBoxProps> {
         setupMoveUpEvents(
             {},
             e,
-            e => {
+            moveEv => {
                 // dragging off a column to create a brushed DataVizBox
                 const sourceAnchorCreator = () => this._props.docView?.()!.Document!;
                 const targetCreator = (annotationOn: Doc | undefined) => {
@@ -138,13 +140,13 @@ export class TableBox extends ObservableReactComponent<TableBoxProps> {
                     embedding.pieSliceColors = Field.Copy(this._props.layoutDoc.pieSliceColors);
                     return embedding;
                 };
-                if (this._props.docView?.() && !ClientUtils.isClick(e.clientX, e.clientY, downX, downY, Date.now())) {
-                    DragManager.StartAnchorAnnoDrag(e.target instanceof HTMLElement ? [e.target] : [], new DragManager.AnchorAnnoDragData(this._props.docView()!, sourceAnchorCreator, targetCreator), downX, downY, {
-                        dragComplete: e => {
-                            if (!e.aborted && e.annoDragData && e.annoDragData.linkSourceDoc && e.annoDragData.dropDocument && e.linkDocument) {
-                                e.linkDocument.link_displayLine = true;
-                                e.linkDocument.link_matchEmbeddings = true;
-                                e.linkDocument.link_displayArrow = true;
+                if (this._props.docView?.() && !ClientUtils.isClick(moveEv.clientX, moveEv.clientY, downX, downY, Date.now())) {
+                    DragManager.StartAnchorAnnoDrag(moveEv.target instanceof HTMLElement ? [moveEv.target] : [], new DragManager.AnchorAnnoDragData(this._props.docView()!, sourceAnchorCreator, targetCreator), downX, downY, {
+                        dragComplete: completeEv => {
+                            if (!completeEv.aborted && completeEv.annoDragData && completeEv.annoDragData.linkSourceDoc && completeEv.annoDragData.dropDocument && completeEv.linkDocument) {
+                                completeEv.linkDocument.link_displayLine = true;
+                                completeEv.linkDocument.link_matchEmbeddings = true;
+                                completeEv.linkDocument.link_displayArrow = true;
                                 // e.annoDragData.linkSourceDoc.followLinkToggle = e.annoDragData.dropDocument.annotationOn === this._props.Document;
                                 // e.annoDragData.linkSourceDoc.followLinkZoom = false;
                             }
@@ -155,9 +157,9 @@ export class TableBox extends ObservableReactComponent<TableBoxProps> {
                 return false;
             },
             emptyFunction,
-            action(e => {
-                if (e.shiftKey) {
-                    if (this._props.titleCol == col) this._props.titleCol = '';
+            action(clickEv => {
+                if (clickEv.shiftKey) {
+                    if (this._props.titleCol === col) this._props.titleCol = '';
                     else this._props.titleCol = col;
                     this._props.selectTitleCol(this._props.titleCol);
                 } else {
@@ -185,8 +187,22 @@ export class TableBox extends ObservableReactComponent<TableBoxProps> {
                         }
                     }}>
                     <div className="selectAll-buttons">
-                        <Button onClick={action(() => (this._props.layoutDoc.dataViz_selectedRows = new List<number>(this._tableDataIds)))} text="Select All" type={Type.SEC} color={'black'} />
-                        <Button onClick={action(() => (this._props.layoutDoc.dataViz_selectedRows = new List<number>()))} text="Deselect All" type={Type.SEC} color={'black'} />
+                        <Button
+                            onClick={action(() => {
+                                this._props.layoutDoc.dataViz_selectedRows = new List<number>(this._tableDataIds);
+                            })}
+                            text="Select All"
+                            type={Type.SEC}
+                            color="black"
+                        />
+                        <Button
+                            onClick={action(() => {
+                                this._props.layoutDoc.dataViz_selectedRows = new List<number>();
+                            })}
+                            text="Deselect All"
+                            type={Type.SEC}
+                            color="black"
+                        />
                     </div>
                     <div
                         className={`tableBox-container ${this.columns[0]}`}
@@ -225,7 +241,7 @@ export class TableBox extends ObservableReactComponent<TableBoxProps> {
                                                         ? 'darkgreen'
                                                         : this._props.axes.length > 2 && this._props.axes.lastElement() === col
                                                           ? 'darkred'
-                                                          : this._props.axes.lastElement() === col || (this._props.axes.length > 2 && this._props.axes[1] == col)
+                                                          : this._props.axes.lastElement() === col || (this._props.axes.length > 2 && this._props.axes[1] === col)
                                                             ? 'darkblue'
                                                             : undefined,
                                                 background:
@@ -233,7 +249,7 @@ export class TableBox extends ObservableReactComponent<TableBoxProps> {
                                                         ? '#E3fbdb'
                                                         : this._props.axes.length > 2 && this._props.axes.lastElement() === col
                                                           ? '#Fbdbdb'
-                                                          : this._props.axes.lastElement() === col || (this._props.axes.length > 2 && this._props.axes[1] == col)
+                                                          : this._props.axes.lastElement() === col || (this._props.axes.length > 2 && this._props.axes[1] === col)
                                                             ? '#c6ebf7'
                                                             : undefined,
                                                 // blue: #ADD8E6
@@ -260,11 +276,11 @@ export class TableBox extends ObservableReactComponent<TableBoxProps> {
                                                 background: NumListCast(this._props.layoutDoc.dataViz_highlitedRows).includes(rowId) ? 'lightYellow' : NumListCast(this._props.layoutDoc.dataViz_selectedRows).includes(rowId) ? 'lightgrey' : '',
                                             }}>
                                             {this.columns.map(col => {
-                                                var colSelected = false;
-                                                if (this._props.axes.length > 2) colSelected = this._props.axes[0] == col || this._props.axes[1] == col || this._props.axes[2] == col;
-                                                else if (this._props.axes.length > 1) colSelected = this._props.axes[0] == col || this._props.axes[1] == col;
-                                                else if (this._props.axes.length > 0) colSelected = this._props.axes[0] == col;
-                                                if (this._props.titleCol == col) colSelected = true;
+                                                let colSelected = false;
+                                                if (this._props.axes.length > 2) colSelected = this._props.axes[0] === col || this._props.axes[1] === col || this._props.axes[2] === col;
+                                                else if (this._props.axes.length > 1) colSelected = this._props.axes[0] === col || this._props.axes[1] === col;
+                                                else if (this._props.axes.length > 0) colSelected = this._props.axes[0] === col;
+                                                if (this._props.titleCol === col) colSelected = true;
                                                 return (
                                                     <td key={this.columns.indexOf(col)} style={{ border: colSelected ? '3px solid black' : '1px solid black', fontWeight: colSelected ? 'bolder' : 'normal' }}>
                                                         <div className="tableBox-cell">{this._props.records[rowId][col]}</div>
@@ -279,10 +295,10 @@ export class TableBox extends ObservableReactComponent<TableBoxProps> {
                     </div>
                 </div>
             );
-        } else
-            return (
-                // when it is a brushed table and the incoming table doesn't have any rows selected
-                <div className="chart-container">Selected rows of data from the incoming DataVizBox to display.</div>
-            );
+        }
+        return (
+            // when it is a brushed table and the incoming table doesn't have any rows selected
+            <div className="chart-container">Selected rows of data from the incoming DataVizBox to display.</div>
+        );
     }
 }
diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx
index 518158a7f..832e18b68 100644
--- a/src/client/views/nodes/DocumentContentsView.tsx
+++ b/src/client/views/nodes/DocumentContentsView.tsx
@@ -1,3 +1,4 @@
+/* eslint-disable react/require-default-props */
 import { computed, makeObservable } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
@@ -18,7 +19,7 @@ import { CollectionView } from '../collections/CollectionView';
 import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView';
 import { CollectionSchemaView } from '../collections/collectionSchema/CollectionSchemaView';
 import { SchemaRowBox } from '../collections/collectionSchema/SchemaRowBox';
-import { PresElementBox } from '../nodes/trails/PresElementBox';
+import { PresElementBox } from './trails/PresElementBox';
 import { SearchBox } from '../search/SearchBox';
 import { DashWebRTCVideo } from '../webcam/DashWebRTCVideo';
 import { AudioBox } from './AudioBox';
@@ -71,8 +72,8 @@ interface HTMLtagProps {
     children?: JSX.Element[];
 }
 
-//"<HTMLdiv borderRadius='100px' onClick={this.bannerColor=this.bannerColor==='red'?'green':'red'} overflow='hidden' position='absolute' width='100%' height='100%' transform='rotate({2*this.x+this.y}deg)'> <ImageBox {...props} fieldKey={'data'}/> <HTMLspan width='200px' top='0' height='35px' textAlign='center' paddingTop='10px' transform='translate(-40px, 45px) rotate(-45deg)' position='absolute' color='{this.bannerColor===`green`?`light`:`dark`}blue' backgroundColor='{this.bannerColor===`green`?`dark`:`light`}blue'> {this.title}</HTMLspan></HTMLdiv>"
-//"<HTMLdiv borderRadius='100px' overflow='hidden' position='absolute' width='100%' height='100%'
+// "<HTMLdiv borderRadius='100px' onClick={this.bannerColor=this.bannerColor==='red'?'green':'red'} overflow='hidden' position='absolute' width='100%' height='100%' transform='rotate({2*this.x+this.y}deg)'> <ImageBox {...props} fieldKey={'data'}/> <HTMLspan width='200px' top='0' height='35px' textAlign='center' paddingTop='10px' transform='translate(-40px, 45px) rotate(-45deg)' position='absolute' color='{this.bannerColor===`green`?`light`:`dark`}blue' backgroundColor='{this.bannerColor===`green`?`dark`:`light`}blue'> {this.title}</HTMLspan></HTMLdiv>"
+// "<HTMLdiv borderRadius='100px' overflow='hidden' position='absolute' width='100%' height='100%'
 //          transform='rotate({2*this.x+this.y}deg)'
 //          onClick = { this.bannerColor = this.bannerColor === 'red' ? 'green' : 'red' } >
 //    <ImageBox {...props} fieldKey={'data'}/>
@@ -85,7 +86,7 @@ interface HTMLtagProps {
 //  </HTMLdiv>"
 @observer
 export class HTMLtag extends React.Component<HTMLtagProps> {
-    click = (e: React.MouseEvent) => {
+    click = () => {
         const clickScript = (this.props as any).onClick as Opt<ScriptField>;
         clickScript?.script.run({ this: this.props.Document, self: this.props.Document, scale: this.props.scaling });
     };
@@ -96,11 +97,10 @@ export class HTMLtag extends React.Component<HTMLtagProps> {
     render() {
         const style: { [key: string]: any } = {};
         const divKeys = OmitKeys(this.props, ['children', 'dragStarting', 'dragEnding', 'htmltag', 'scaling', 'Document', 'key', 'onInput', 'onClick', '__proto__']).omit;
-        const replacer = (match: any, expr: string, offset: any, string: any) => {
+        const replacer = (match: any, expr: string) =>
             // bcz: this executes a script to convert a property expression string:  { script }  into a value
-            return (ScriptField.MakeFunction(expr, { self: Doc.name, this: Doc.name, scale: 'number' })?.script.run({ self: this.props.Document, this: this.props.Document, scale: this.props.scaling }).result as string) || '';
-        };
-        Object.keys(divKeys).map((prop: string) => {
+            (ScriptField.MakeFunction(expr, { self: Doc.name, this: Doc.name, scale: 'number' })?.script.run({ self: this.props.Document, this: this.props.Document, scale: this.props.scaling }).result as string) || '';
+        Object.keys(divKeys).forEach((prop: string) => {
             const p = (this.props as any)[prop] as string;
             style[prop] = p?.replace(/{([^.'][^}']+)}/g, replacer);
         });
@@ -158,7 +158,7 @@ export class DocumentContentsView extends ObservableReactComponent<DocumentConte
             'dontCenter',
             'DataTransition',
             'contextMenuItems',
-            //'onClick', // don't need to omit this since it will be set
+            // 'onClick', // don't need to omit this since it will be set
             'onDoubleClickScript',
             'onPointerDownScript',
             'onPointerUpScript',
@@ -187,21 +187,16 @@ export class DocumentContentsView extends ObservableReactComponent<DocumentConte
         let layoutFrame = this.layout;
 
         // replace code content with a script  >{content}<   as in  <HTMLdiv>{this.title}</HTMLdiv>
-        const replacer = (match: any, prefix: string, expr: string, postfix: string, offset: any, string: any) => {
-            return prefix + ((ScriptField.MakeFunction(expr, { self: Doc.name, this: Doc.name })?.script.run({ this: this._props.Document }).result as string) || '') + postfix;
-        };
+        const replacer = (match: any, prefix: string, expr: string, postfix: string) => prefix + ((ScriptField.MakeFunction(expr, { self: Doc.name, this: Doc.name })?.script.run({ this: this._props.Document }).result as string) || '') + postfix;
         layoutFrame = layoutFrame.replace(/(>[^{]*)[^=]\{([^.'][^<}]+)\}([^}]*<)/g, replacer);
 
         // replace HTML<tag> with corresponding HTML tag as in:  <HTMLdiv> becomes  <HTMLtag Document={props.Document} htmltag='div'>
-        const replacer2 = (match: any, p1: string, offset: any, string: any) => {
-            return `<HTMLtag Document={props.Document} scaling='${this._props.NativeDimScaling?.() || 1}' htmltag='${p1}'`;
-        };
+        const replacer2 = (match: any, p1: string) => `<HTMLtag Document={props.Document} scaling='${this._props.NativeDimScaling?.() || 1}' htmltag='${p1}'`;
         layoutFrame = layoutFrame.replace(/<HTML([a-zA-Z0-9_-]+)/g, replacer2);
 
         // replace /HTML<tag> with </HTMLdiv>  as in:  </HTMLdiv> becomes  </HTMLtag>
-        const replacer3 = (match: any, p1: string, offset: any, string: any) => {
-            return `</HTMLtag`;
-        };
+        const replacer3 = (/* match: any, p1: string, offset: any, string: any */) => `</HTMLtag`;
+
         layoutFrame = layoutFrame.replace(/<\/HTML([a-zA-Z0-9_-]+)/g, replacer3);
 
         // add onClick function to props
@@ -271,7 +266,7 @@ export class DocumentContentsView extends ObservableReactComponent<DocumentConte
                 }}
                 bindings={bindings}
                 jsx={layoutFrame}
-                showWarnings={true}
+                showWarnings
                 onError={(test: any) => {
                     console.log('DocumentContentsView:' + test, bindings, layoutFrame);
                 }}
diff --git a/src/client/views/nodes/DocumentLinksButton.tsx b/src/client/views/nodes/DocumentLinksButton.tsx
index d378082f8..a029b3761 100644
--- a/src/client/views/nodes/DocumentLinksButton.tsx
+++ b/src/client/views/nodes/DocumentLinksButton.tsx
@@ -1,3 +1,5 @@
+/* eslint-disable jsx-a11y/no-static-element-interactions */
+/* eslint-disable jsx-a11y/click-events-have-key-events */
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { Tooltip } from '@mui/material';
 import { action, computed, makeObservable, observable, runInAction } from 'mobx';
@@ -26,20 +28,22 @@ interface DocumentLinksButtonProps {
     AlwaysOn?: boolean;
     InMenu?: boolean;
     OnHover?: boolean;
-    StartLink?: boolean; //whether the link HAS been started (i.e. now needs to be completed)
+    StartLink?: boolean; // whether the link HAS been started (i.e. now needs to be completed)
     ShowCount?: boolean;
     scaling?: () => number; // how uch doc is scaled so that link buttons can invert it
     hideCount?: () => boolean;
 }
 
 export class DocButtonState {
-    @observable public StartLink: Doc | undefined = undefined; //origin's Doc, if defined
+    @observable public StartLink: Doc | undefined = undefined; // origin's Doc, if defined
     @observable public StartLinkView: DocumentView | undefined = undefined;
     @observable public AnnotationId: string | undefined = undefined;
     @observable public AnnotationUri: string | undefined = undefined;
     @observable public LinkEditorDocView: DocumentView | undefined = undefined;
+    // eslint-disable-next-line no-use-before-define
     public static _instance: DocButtonState | undefined;
     public static get Instance() {
+        // eslint-disable-next-line no-return-assign
         return DocButtonState._instance ?? (DocButtonState._instance = new DocButtonState());
     }
     constructor() {
@@ -50,7 +54,7 @@ export class DocButtonState {
 export class DocumentLinksButton extends ObservableReactComponent<DocumentLinksButtonProps> {
     private _linkButton = React.createRef<HTMLDivElement>();
     public static get StartLink() { return DocButtonState.Instance.StartLink; } // prettier-ignore
-    public static set StartLink(value) { runInAction(() => (DocButtonState.Instance.StartLink = value)); } // prettier-ignore
+    public static set StartLink(value) { runInAction(() => {DocButtonState.Instance.StartLink = value}); } // prettier-ignore
     @observable public static StartLinkView: DocumentView | undefined = undefined;
     @observable public static AnnotationId: string | undefined = undefined;
     @observable public static AnnotationUri: string | undefined = undefined;
@@ -93,7 +97,9 @@ export class DocumentLinksButton extends ObservableReactComponent<DocumentLinksB
             }),
             undefined,
             undefined,
-            action(() => (DocButtonState.Instance.LinkEditorDocView = this._props.View))
+            action(() => {
+                DocButtonState.Instance.LinkEditorDocView = this._props.View;
+            })
         );
     };
 
@@ -105,7 +111,7 @@ export class DocumentLinksButton extends ObservableReactComponent<DocumentLinksB
             emptyFunction,
             action((e, doubleTap) => {
                 if (doubleTap && this._props.InMenu && this._props.StartLink) {
-                    //action(() => Doc.BrushDoc(this._props.View.Document));
+                    // action(() => Doc.BrushDoc(this._props.View.Document));
                     if (DocumentLinksButton.StartLink === this._props.View.Document) {
                         DocumentLinksButton.StartLink = undefined;
                         DocumentLinksButton.StartLinkView = undefined;
@@ -119,7 +125,7 @@ export class DocumentLinksButton extends ObservableReactComponent<DocumentLinksB
     };
 
     @undoBatch
-    onLinkClick = (e: React.MouseEvent): void => {
+    onLinkClick = (): void => {
         if (this._props.InMenu && this._props.StartLink) {
             DocumentLinksButton.AnnotationId = undefined;
             DocumentLinksButton.AnnotationUri = undefined;
@@ -127,7 +133,7 @@ export class DocumentLinksButton extends ObservableReactComponent<DocumentLinksB
                 DocumentLinksButton.StartLink = undefined;
                 DocumentLinksButton.StartLinkView = undefined;
             } else {
-                //if this LinkButton's Document is undefined
+                // if this LinkButton's Document is undefined
                 DocumentLinksButton.StartLink = this._props.View.Document;
                 DocumentLinksButton.StartLinkView = this._props.View;
             }
@@ -154,7 +160,9 @@ export class DocumentLinksButton extends ObservableReactComponent<DocumentLinksB
                 DocumentLinksButton.AnnotationUri = undefined;
                 // !this._props.StartLink
             } else if (startLink !== endLink) {
+                // eslint-disable-next-line no-param-reassign
                 endLink = endLinkView?.ComponentView?.getAnchor?.(true, pinProps) || endLink;
+                // eslint-disable-next-line no-param-reassign
                 startLink = DocumentLinksButton.StartLinkView?.ComponentView?.getAnchor?.(true) || startLink;
                 const linkDoc = DocUtils.MakeLink(startLink, endLink, { link_relationship: DocumentLinksButton.AnnotationId ? 'hypothes.is annotation' : undefined });
 
@@ -193,7 +201,9 @@ export class DocumentLinksButton extends ObservableReactComponent<DocumentLinksB
                     }
 
                     setTimeout(
-                        action(() => (TaskCompletionBox.taskCompleted = false)),
+                        action(() => {
+                            TaskCompletionBox.taskCompleted = false;
+                        }),
                         2500
                     );
                 }
@@ -243,13 +253,13 @@ export class DocumentLinksButton extends ObservableReactComponent<DocumentLinksB
             showLinkCount(this._props.OnHover, this._props.Bottom)
         ) : (
             <div className="documentLinksButton-menu">
-                {this._props.StartLink ? ( //if link has been started from current node, then set behavior of link button to deactivate linking when clicked again
+                {this._props.StartLink ? ( // if link has been started from current node, then set behavior of link button to deactivate linking when clicked again
                     <div className={`documentLinksButton ${isActive ? `startLink` : ``}`} ref={this._linkButton} onPointerDown={isActive ? StopEvent : this.onLinkButtonDown} onClick={isActive ? this.clearLinks : this.onLinkClick}>
                         <FontAwesomeIcon className="documentdecorations-icon" icon="link" />
                     </div>
                 ) : null}
-                {!this._props.StartLink && DocumentLinksButton.StartLink !== this._props.View.Document ? ( //if the origin node is not this node
-                    <div className={'documentLinksButton-endLink'} ref={this._linkButton} onPointerDown={DocumentLinksButton.StartLink && this.completeLink}>
+                {!this._props.StartLink && DocumentLinksButton.StartLink !== this._props.View.Document ? ( // if the origin node is not this node
+                    <div className="documentLinksButton-endLink" ref={this._linkButton} onPointerDown={DocumentLinksButton.StartLink && this.completeLink}>
                         <FontAwesomeIcon className="documentdecorations-icon" icon="link" />
                     </div>
                 ) : null}
@@ -263,7 +273,7 @@ export class DocumentLinksButton extends ObservableReactComponent<DocumentLinksB
         const buttonTitle = 'Tap to view links; double tap to open link collection';
         const title = this._props.ShowCount ? buttonTitle : menuTitle;
 
-        //render circular tooltip if it isn't set to invisible and show the number of doc links the node has, and render inner-menu link button for starting/stopping links if currently in menu
+        // render circular tooltip if it isn't set to invisible and show the number of doc links the node has, and render inner-menu link button for starting/stopping links if currently in menu
         return !Array.from(this.filteredLinks).length && !this._props.AlwaysOn ? null : (
             <div
                 className="documentLinksButton-wrapper"
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 62a3e2467..5962cd09f 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -118,7 +118,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
      * This function is filled in by MainView to allow non-viewBox views to add Docs as tabs without
      * needing to know about/reference MainView
      */
-    public static addDocTabFunc: (doc: Doc, location: OpenWhere) => boolean = returnFalse;
+    public static addDocTabFunc: (doc: Doc | Doc[], location: OpenWhere) => boolean = returnFalse;
 
     private _disposers: { [name: string]: IReactionDisposer } = {};
     private _doubleClickTimeout: NodeJS.Timeout | undefined;
diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx
index 14454ff61..5c91d5aca 100644
--- a/src/client/views/nodes/FieldView.tsx
+++ b/src/client/views/nodes/FieldView.tsx
@@ -1,3 +1,5 @@
+/* eslint-disable react/no-unused-prop-types */
+/* eslint-disable react/require-default-props */
 import { computed } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
@@ -31,6 +33,7 @@ export interface FocusViewOptions {
     easeFunc?: 'linear' | 'ease'; // transition method for scrolling
 }
 export type FocusFuncType = (doc: Doc, options: FocusViewOptions) => Opt<number>;
+// eslint-disable-next-line no-use-before-define
 export type StyleProviderFuncType = (doc: Opt<Doc>, props: Opt<FieldViewProps>, property: string) => any;
 //
 // these properties get assigned through the render() method of the DocumentView when it creates this node.
@@ -73,13 +76,14 @@ export interface FieldViewSharedProps {
     onPointerDownScript?: () => ScriptField;
     onPointerUpScript?: () => ScriptField;
     onBrowseClickScript?: () => ScriptField | undefined;
+    // eslint-disable-next-line no-use-before-define
     onKey?: (e: React.KeyboardEvent, fieldProps: FieldViewProps) => boolean | undefined;
     layout_fitWidth?: (doc: Doc) => boolean | undefined;
     searchFilterDocs: () => Doc[];
     layout_showTitle?: () => string;
     whenChildContentsActiveChanged: (isActive: boolean) => void;
     rootSelected?: () => boolean; // whether the root of a template has been selected
-    addDocTab: (doc: Doc, where: OpenWhere) => boolean;
+    addDocTab: (doc: Doc | Doc[], where: OpenWhere) => boolean;
     filterAddDocument?: (doc: Doc[]) => boolean; // allows a document that renders a Collection view to filter or modify any documents added to the collection (see PresBox for an example)
     addDocument?: (doc: Doc | Doc[], annotationKey?: string) => boolean;
     removeDocument?: (doc: Doc | Doc[], annotationKey?: string) => boolean;
@@ -120,7 +124,7 @@ export interface FieldViewProps extends FieldViewSharedProps {
 @observer
 export class FieldView extends React.Component<FieldViewProps> {
     public static LayoutString(fieldType: { name: string }, fieldStr: string) {
-        return `<${fieldType.name} {...props} fieldKey={'${fieldStr}'}/>`; //e.g., "<ImageBox {...props} fieldKey={'data'} />"
+        return `<${fieldType.name} {...props} fieldKey={'${fieldStr}'}/>`; // e.g., "<ImageBox {...props} fieldKey={'data'} />"
     }
     @computed get fieldval() {
         return this.props.Document[this.props.fieldKey];
@@ -135,6 +139,6 @@ export class FieldView extends React.Component<FieldViewProps> {
         if (field instanceof List) return <div> {field.map(f => Field.toString(f)).join(', ')} </div>;
         if (field instanceof WebField) return <p>{Field.toString(field.url.href)}</p>;
         if (!(field instanceof Promise)) return <p>{Field.toString(field)}</p>;
-        return <p> {'Waiting for server...'} </p>;
+        return <p> Waiting for server... </p>;
     }
 }
diff --git a/src/client/views/nodes/FontIconBox/FontIconBox.tsx b/src/client/views/nodes/FontIconBox/FontIconBox.tsx
index 70fc63115..79738c452 100644
--- a/src/client/views/nodes/FontIconBox/FontIconBox.tsx
+++ b/src/client/views/nodes/FontIconBox/FontIconBox.tsx
@@ -1,13 +1,14 @@
+/* eslint-disable react/jsx-props-no-spreading */
 import { IconProp } from '@fortawesome/fontawesome-svg-core';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { Button, ColorPicker, Dropdown, DropdownType, EditableText, IconButton, IListItemProps, MultiToggle, NumberDropdown, NumberDropdownType, Popup, Size, Toggle, ToggleType, Type } from 'browndash-components';
+import { Button, ColorPicker, Dropdown, DropdownType, IconButton, IListItemProps, MultiToggle, NumberDropdown, NumberDropdownType, Popup, Size, Toggle, ToggleType, Type } from 'browndash-components';
 import { action, computed, makeObservable, observable } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
+import { ClientUtils, returnTrue, setupMoveUpEvents } from '../../../../ClientUtils';
 import { Doc, DocListCast, StrListCast } from '../../../../fields/Doc';
 import { BoolCast, DocCast, NumCast, ScriptCast, StrCast } from '../../../../fields/Types';
-import { emptyFunction, Utils } from '../../../../Utils';
-import { ClientUtils, returnTrue, setupMoveUpEvents } from '../../../../ClientUtils';
+import { emptyFunction } from '../../../../Utils';
 import { CollectionViewType, DocumentType } from '../../../documents/DocumentTypes';
 import { SelectionManager } from '../../../util/SelectionManager';
 import { SettingsManager } from '../../../util/SettingsManager';
@@ -34,7 +35,7 @@ export enum ButtonType {
     NumberSliderButton = 'numSliderBtn',
     NumberDropdownButton = 'numDropdownBtn',
     NumberInlineButton = 'numInlineBtn',
-    EditableText = 'editableText',
+    EditText = 'editableText',
 }
 
 export interface ButtonProps extends FieldViewProps {
@@ -83,7 +84,7 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() {
         if (iconFalse) {
             icon = StrCast(this.dataDoc[this.fieldKey ?? 'iconFalse'] ?? this.dataDoc.icon, 'user') as any;
             if (icon) return <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={icon} color={color} />;
-            else return null;
+            return null;
         }
         icon = StrCast(this.dataDoc[this.fieldKey ?? 'icon'] ?? this.dataDoc.icon, 'user') as any;
         return !icon ? null : icon === 'pres-trail' ? TrailsIcon(color) : <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={icon} color={color} />;
@@ -109,7 +110,7 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() {
      * - Color button
      * - Dropdown list
      * - Number button
-     **/
+     * */
 
     _batch: UndoManager.Batch | undefined = undefined;
     /**
@@ -118,17 +119,12 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() {
     @computed get numberDropdown() {
         let type: NumberDropdownType;
         switch (this.type) {
-            case ButtonType.NumberDropdownButton:
-                type = 'dropdown';
-                break;
-            case ButtonType.NumberInlineButton:
-                type = 'input';
-                break;
+            case ButtonType.NumberDropdownButton:  type = 'dropdown'; break;
+            case ButtonType.NumberInlineButton:    type = 'input';  break;
             case ButtonType.NumberSliderButton:
-            default:
-                type = 'slider';
+            default:                               type = 'slider';
                 break;
-        }
+        } // prettier-ignore
         const numScript = (value?: number) => ScriptCast(this.Document.script).script.run({ this: this.Document, self: this.Document, value, _readOnly_: value === undefined });
         const color = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Color);
         // Script for checking the outcome of the toggle
@@ -155,12 +151,10 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() {
         setupMoveUpEvents(
             this,
             e,
-            (e: PointerEvent) => {
-                return ScriptCast(this.Document.onDragScript)?.script.run({ this: this.Document, self: this.Document, value: { doc: value, e } }).result;
-            },
+            () => ScriptCast(this.Document.onDragScript)?.script.run({ this: this.Document, self: this.Document, value: { doc: value, e } }).result,
             emptyFunction,
             emptyFunction
-        );
+        ); // prettier-ignore
         return false;
     };
 
@@ -189,7 +183,7 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() {
                     }
                     return (
                         <Popup
-                            icon={<FontAwesomeIcon size={'1x'} icon={icon} />}
+                            icon={<FontAwesomeIcon size="1x" icon={icon} />}
                             text={text}
                             type={Type.TERT}
                             color={SettingsManager.userColor}
@@ -275,8 +269,8 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() {
         // Determine the type of toggle button
         const tooltip: string = StrCast(this.Document.toolTip);
 
-        const script = ScriptCast(this.Document.onClick);
-        const toggleStatus = script ? script.script.run({ this: this.Document, self: this.Document, value: undefined, _readOnly_: true }).result : false;
+        // const script = ScriptCast(this.Document.onClick);
+        // const toggleStatus = script ? script.script.run({ this: this.Document, self: this.Document, value: undefined, _readOnly_: true }).result : false;
         // Colors
         const color = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Color);
         const items = DocListCast(this.dataDoc.data);
@@ -313,7 +307,7 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() {
         const toggleStatus = script?.script.run({ this: this.Document, self: this.Document, value: undefined, _readOnly_: true }).result ?? false;
         // Colors
         const color = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Color);
-        const backgroundColor = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BackgroundColor);
+        // const backgroundColor = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BackgroundColor);
 
         return (
             <Toggle
@@ -323,7 +317,7 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() {
                 toggleStatus={toggleStatus}
                 text={buttonText}
                 color={color}
-                //background={SettingsManager.userBackgroundColor}
+                // background={SettingsManager.userBackgroundColor}
                 icon={this.Icon(color)!}
                 label={this.label}
                 onPointerDown={e =>
@@ -332,10 +326,10 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() {
                         e,
                         returnTrue,
                         emptyFunction,
-                        action((e, doubleTap) => {
+                        action((clickEv, doubleTap) => {
                             (!doubleTap || !double) && script?.script.run({ this: this.Document, self: this.Document, value: !toggleStatus, _readOnly_: false });
                             doubleTap && double?.script.run({ this: this.Document, self: this.Document, value: !toggleStatus, _readOnly_: false });
-                            this._hackToRecompute = this._hackToRecompute + 1;
+                            this._hackToRecompute += 1;
                         })
                     )
                 }
@@ -348,27 +342,22 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() {
      */
     @computed get defaultButton() {
         const color = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Color);
-        const backgroundColor = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BackgroundColor);
         const tooltip: string = StrCast(this.Document.toolTip);
 
         return <IconButton tooltip={tooltip} icon={this.Icon(color)!} label={this.label} />;
     }
 
     @computed get editableText() {
-        // Script for running the toggle
         const script = ScriptCast(this.Document.script);
-        // Function to run the script
         const checkResult = script?.script.run({ this: this.Document, self: this.Document, value: '', _readOnly_: true }).result;
 
-        const setValue = (value: string, shiftDown?: boolean): boolean => script?.script.run({ this: this.Document, self: this.Document, value, _readOnly_: false }).result;
-
-        return <EditableText editing={false} setEditing={(editing: boolean) => {}} />;
+        const setValue = (value: string): boolean => script?.script.run({ this: this.Document, self: this.Document, value, _readOnly_: false }).result;
 
         return (
             <div className="menuButton editableText">
-                <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={'lock'} />
+                <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon="lock" />
                 <div style={{ width: 'calc(100% - .875em)', paddingLeft: '4px' }}>
-                    <EditableView GetValue={() => script?.script.run({ this: this.Document, self: this.Document, value: '', _readOnly_: true }).result} SetValue={setValue} oneLine={true} contents={checkResult} />
+                    <EditableView GetValue={() => script?.script.run({ this: this.Document, self: this.Document, value: '', _readOnly_: true }).result} SetValue={setValue} oneLine contents={checkResult} />
                 </div>
             </div>
         );
@@ -384,7 +373,7 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() {
             case ButtonType.NumberDropdownButton: 
             case ButtonType.NumberInlineButton:
             case ButtonType.NumberSliderButton: return this.numberDropdown;
-            case ButtonType.EditableText:       return this.editableText; 
+            case ButtonType.EditText:           return this.editableText; 
             case ButtonType.DropdownList:       return this.dropdownListButton;
             case ButtonType.ColorButton:        return this.colorButton; 
             case ButtonType.MultiToggleButton:  return this.multiToggleButton;
@@ -395,6 +384,7 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() {
                         background={SettingsManager.userBackgroundColor} text={StrCast(this.dataDoc.buttonText)}/>;
             case ButtonType.MenuButton: return <IconButton {...btnProps} color={color} 
                         background={SettingsManager.userBackgroundColor} size={Size.LARGE} tooltipPlacement='right' onPointerDown={scriptFunc} />; 
+            default: 
         }
         return this.defaultButton;
     };
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 231300a65..90b4a6740 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -22,7 +22,7 @@ import { Networking } from '../../Network';
 import { DocumentManager } from '../../util/DocumentManager';
 import { DragManager } from '../../util/DragManager';
 import { undoBatch } from '../../util/UndoManager';
-import { ContextMenu } from '../../views/ContextMenu';
+import { ContextMenu } from '../ContextMenu';
 import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView';
 import { ContextMenuProps } from '../ContextMenuItem';
 import { PinProps, ViewBoxAnnotatableComponent, ViewBoxInterface } from '../DocComponent';
@@ -428,9 +428,9 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl
             setupMoveUpEvents(
                 this,
                 e,
-                action(e => {
+                action(moveEv => {
                     MarqueeAnnotator.clearAnnotations(this._savedAnnotations);
-                    this._marqueeref.current?.onInitiateSelection([e.clientX, e.clientY]);
+                    this._marqueeref.current?.onInitiateSelection([moveEv.clientX, moveEv.clientY]);
                     return true;
                 }),
                 returnFalse,
@@ -476,6 +476,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl
                 }}>
                 <CollectionFreeFormView
                     ref={this._ffref}
+                    // eslint-disable-next-line react/jsx-props-no-spreading
                     {...this._props}
                     setContentViewBox={emptyFunction}
                     NativeWidth={returnZero}
diff --git a/src/client/views/nodes/KeyValuePair.tsx b/src/client/views/nodes/KeyValuePair.tsx
index c3afc198d..f96dd2b76 100644
--- a/src/client/views/nodes/KeyValuePair.tsx
+++ b/src/client/views/nodes/KeyValuePair.tsx
@@ -1,3 +1,4 @@
+/* eslint-disable jsx-a11y/control-has-associated-label */
 import { Tooltip } from '@mui/material';
 import { action, makeObservable, observable } from 'mobx';
 import { observer } from 'mobx-react';
@@ -63,7 +64,7 @@ export class KeyValuePair extends ObservableReactComponent<KeyValuePairProps> {
     render() {
         // let fieldKey = Object.keys(props.Document).indexOf(props.fieldKey) !== -1 ? props.fieldKey : "(" + props.fieldKey + ")";
         let protoCount = 0;
-        let doc = this._props.doc;
+        let { doc } = this._props;
         while (doc) {
             if (Object.keys(doc).includes(this._props.keyName)) {
                 break;
@@ -77,10 +78,18 @@ export class KeyValuePair extends ObservableReactComponent<KeyValuePairProps> {
         const hover = { transition: '0.3s ease opacity', opacity: this.isPointerOver || this.isChecked ? 1 : 0 };
 
         return (
-            <tr className={this._props.rowStyle} onPointerEnter={action(() => (this.isPointerOver = true))} onPointerLeave={action(() => (this.isPointerOver = false))}>
+            <tr
+                className={this._props.rowStyle}
+                onPointerEnter={action(() => {
+                    this.isPointerOver = true;
+                })}
+                onPointerLeave={action(() => {
+                    this.isPointerOver = false;
+                })}>
                 <td className="keyValuePair-td-key" style={{ width: `${this._props.keyWidth}%` }}>
                     <div className="keyValuePair-td-key-container">
                         <button
+                            type="button"
                             style={hover}
                             className="keyValuePair-td-key-delete"
                             onClick={undoBatch(() => {
diff --git a/src/client/views/nodes/LabelBox.tsx b/src/client/views/nodes/LabelBox.tsx
index d1c8c62ed..80ece7cc8 100644
--- a/src/client/views/nodes/LabelBox.tsx
+++ b/src/client/views/nodes/LabelBox.tsx
@@ -13,6 +13,7 @@ import { ContextMenuProps } from '../ContextMenuItem';
 import { PinProps, ViewBoxBaseComponent } from '../DocComponent';
 import { StyleProp } from '../StyleProvider';
 import { FieldView, FieldViewProps } from './FieldView';
+// eslint-disable-next-line import/extensions
 import BigText from './LabelBigText';
 import './LabelBox.scss';
 import { PresBox } from './trails';
@@ -23,7 +24,7 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps>() {
         return FieldView.LayoutString(LabelBox, fieldKey);
     }
     public static LayoutStringWithTitle(fieldStr: string, label?: string) {
-        return !label ? LabelBox.LayoutString(fieldStr) : `<LabelBox fieldKey={'${fieldStr}'} label={'${label}'} {...props} />`; //e.g., "<ImageBox {...props} fieldKey={"data} />"
+        return !label ? LabelBox.LayoutString(fieldStr) : `<LabelBox fieldKey={'${fieldStr}'} label={'${label}'} {...props} />`; // e.g., "<ImageBox {...props} fieldKey={"data} />"
     }
     private dropDisposer?: DragManager.DragDropDisposer;
     private _timeout: any;
@@ -54,14 +55,16 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps>() {
     get paramsDoc() {
         return Doc.AreProtosEqual(this.layoutDoc, this.dataDoc) ? this.dataDoc : this.layoutDoc;
     }
-    specificContextMenu = (e: React.MouseEvent): void => {
+    specificContextMenu = (): void => {
         const funcs: ContextMenuProps[] = [];
         !Doc.noviceMode &&
             funcs.push({
                 description: 'Clear Script Params',
                 event: () => {
                     const params = Cast(this.paramsDoc['onClick-paramFieldKeys'], listSpec('string'), []);
-                    params?.map(p => (this.paramsDoc[p] = undefined));
+                    params?.forEach(p => {
+                        this.paramsDoc[p] = undefined;
+                    });
                 },
                 icon: 'trash',
             });
@@ -71,7 +74,7 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps>() {
 
     @undoBatch
     drop = (e: Event, de: DragManager.DropEvent) => {
-        const docDragData = de.complete.docDragData;
+        const { docDragData } = de.complete;
         const params = Cast(this.paramsDoc['onClick-paramFieldKeys'], listSpec('string'), []);
         const missingParams = params?.filter(p => !this.paramsDoc[p]);
         if (docDragData && missingParams?.includes((e.target as any).textContent)) {
@@ -131,7 +134,10 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps>() {
         };
         this._timeout = undefined;
         if (!r) return params;
-        if (!r.offsetHeight || !r.offsetWidth) return (this._timeout = setTimeout(() => this.fitTextToBox(r)));
+        if (!r.offsetHeight || !r.offsetWidth) {
+            this._timeout = setTimeout(() => this.fitTextToBox(r));
+            return this._timeout;
+        }
         const parent = r.parentNode;
         const parentStyle = parent.style;
         parentStyle.display = '';
@@ -154,8 +160,13 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps>() {
         return (
             <div
                 className="labelBox-outerDiv"
-                onMouseLeave={action(() => (this._mouseOver = false))}
-                onMouseOver={action(() => (this._mouseOver = true))}
+                onMouseLeave={action(() => {
+                    this._mouseOver = false;
+                })}
+                // eslint-disable-next-line jsx-a11y/mouse-events-have-key-events
+                onMouseOver={action(() => {
+                    this._mouseOver = true;
+                })}
                 ref={this.createDropTarget}
                 onContextMenu={this.specificContextMenu}
                 style={{ boxShadow: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BoxShadow) }}>
diff --git a/src/client/views/nodes/LinkAnchorBox.tsx b/src/client/views/nodes/LinkAnchorBox.tsx
index 0155defb7..bff6d53da 100644
--- a/src/client/views/nodes/LinkAnchorBox.tsx
+++ b/src/client/views/nodes/LinkAnchorBox.tsx
@@ -15,7 +15,9 @@ import { StyleProp } from '../StyleProvider';
 import { FieldView, FieldViewProps } from './FieldView';
 import './LinkAnchorBox.scss';
 import { LinkInfo } from './LinkDocPreview';
+
 const { MEDIUM_GRAY } = require('../global/globalCssVariables.module.scss'); // prettier-ignore
+
 @observer
 export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps>() {
     public static LayoutString(fieldKey: string) {
@@ -48,7 +50,7 @@ export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps>() {
                 else this._props.select(false);
             });
     };
-    onPointerMove = action((e: PointerEvent, down: number[], delta: number[]) => {
+    onPointerMove = action((e: PointerEvent) => {
         const cdiv = this._ref?.current?.parentElement;
         if (!this._isOpen && cdiv) {
             const bounds = cdiv.getBoundingClientRect();
@@ -68,7 +70,7 @@ export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps>() {
         return false;
     });
 
-    specificContextMenu = (e: React.MouseEvent): void => {};
+    specificContextMenu = (): void => {};
 
     render() {
         TraceMobx();
diff --git a/src/client/views/nodes/MapBox/MapBox.tsx b/src/client/views/nodes/MapBox/MapBox.tsx
index 7855f8fe8..822485b8d 100644
--- a/src/client/views/nodes/MapBox/MapBox.tsx
+++ b/src/client/views/nodes/MapBox/MapBox.tsx
@@ -55,7 +55,6 @@ import { MarkerIcons } from './MarkerIcons';
 
 const MAPBOX_ACCESS_TOKEN = 'pk.eyJ1IjoiemF1bHRhdmFuZ2FyIiwiYSI6ImNscHgwNDd1MDA3MXIydm92ODdianp6cGYifQ.WFAqbhwxtMHOWSPtu0l2uQ';
 const MAPBOX_FORWARD_GEOCODE_BASE_URL = 'https://api.mapbox.com/geocoding/v5/mapbox.places/';
-
 const MAPBOX_REVERSE_GEOCODE_BASE_URL = 'https://api.mapbox.com/geocoding/v5/mapbox.places/';
 
 type PopupInfo = {
@@ -112,13 +111,13 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
     };
 
     // this list contains pushpins and configs
-    @computed get allAnnotations() { return DocListCast(this.dataDoc[this.annotationKey]); } //prettier-ignore
-    @computed get allSidebarDocs() { return DocListCast(this.dataDoc[this.SidebarKey]); } //prettier-ignore
-    @computed get allPushpins() { return this.allAnnotations.filter(anno => anno.type === DocumentType.PUSHPIN); } //prettier-ignore
-    @computed get allRoutes() { return this.allAnnotations.filter(anno => anno.type === DocumentType.MAPROUTE); } //prettier-ignore
-    @computed get SidebarShown() { return this.layoutDoc._layout_showSidebar ? true : false; } //prettier-ignore
-    @computed get sidebarWidthPercent() { return StrCast(this.layoutDoc._layout_sidebarWidthPercent, '0%'); } //prettier-ignore
-    @computed get SidebarKey() {  return this.fieldKey + '_sidebar'; } //prettier-ignore
+    @computed get allAnnotations() { return DocListCast(this.dataDoc[this.annotationKey]); } // prettier-ignore
+    @computed get allSidebarDocs() { return DocListCast(this.dataDoc[this.SidebarKey]); } // prettier-ignore
+    @computed get allPushpins() { return this.allAnnotations.filter(anno => anno.type === DocumentType.PUSHPIN); } // prettier-ignore
+    @computed get allRoutes() { return this.allAnnotations.filter(anno => anno.type === DocumentType.MAPROUTE); } // prettier-ignore
+    @computed get SidebarShown() { return !!this.layoutDoc._layout_showSidebar; } // prettier-ignore
+    @computed get sidebarWidthPercent() { return StrCast(this.layoutDoc._layout_sidebarWidthPercent, '0%'); } // prettier-ignore
+    @computed get SidebarKey() {  return this.fieldKey + '_sidebar'; } // prettier-ignore
     @computed get sidebarColor() {
         return StrCast(this.layoutDoc.sidebar_color, StrCast(this.layoutDoc[this._props.fieldKey + '_backgroundColor'], '#e4e4e4'));
     }
@@ -259,7 +258,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
                     }
                 });
             }
-        }); //add to annotation list
+        }); // add to annotation list
 
         return this.addDocument(doc, sidebarKey); // add to sidebar list
     };
@@ -326,7 +325,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
                     backgroundColor: this.SidebarShown ? Colors.MEDIUM_BLUE : Colors.BLACK,
                 }}
                 onPointerDown={this.sidebarBtnDown}>
-                <FontAwesomeIcon style={{ color: Colors.WHITE }} icon={'comment-alt'} size="sm" />
+                <FontAwesomeIcon style={{ color: Colors.WHITE }} icon="comment-alt" size="sm" />
             </div>
         );
     }
@@ -390,7 +389,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
     sidebarDown = (e: React.PointerEvent) => {
         setupMoveUpEvents(this, e, this.sidebarMove, emptyFunction, () => setTimeout(this.toggleSidebar), true);
     };
-    sidebarMove = (e: PointerEvent, down: number[], delta: number[]) => {
+    sidebarMove = (e: PointerEvent) => {
         const bounds = this._ref.current!.getBoundingClientRect();
         this.layoutDoc._layout_sidebarWidthPercent = '' + 100 * Math.max(0, 1 - (e.clientX - bounds.left) / bounds.width) + '%';
         this.layoutDoc._layout_showSidebar = this.layoutDoc._layout_sidebarWidthPercent !== '0%';
@@ -654,7 +653,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
             console.error(features);
             if (features && features.length > 0 && features[0].properties && features[0].geometry) {
                 const geometry = features[0].geometry as LineString;
-                const routeTitle: string = features[0].properties['routeTitle'];
+                const { routeTitle } = features[0].properties;
                 const routeDoc: Doc | undefined = this.allRoutes.find(routeDoc => routeDoc.title === routeTitle);
                 this.deselectPinOrRoute(); // TODO: Also deselect route if selected
                 if (routeDoc) {
@@ -699,7 +698,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
      */
     handleMapDblClick = async (e: MapLayerMouseEvent) => {
         e.preventDefault();
-        const lngLat: LngLat = e.lngLat;
+        const { lngLat }: LngLat = e;
         const longitude: number = lngLat.lng;
         const latitude: number = lngLat.lat;
 
diff --git a/src/client/views/nodes/MapboxMapBox/MapboxContainer.tsx b/src/client/views/nodes/MapboxMapBox/MapboxContainer.tsx
index e857ef722..189e2105d 100644
--- a/src/client/views/nodes/MapboxMapBox/MapboxContainer.tsx
+++ b/src/client/views/nodes/MapboxMapBox/MapboxContainer.tsx
@@ -6,7 +6,7 @@ import * as React from 'react';
 import { MapProvider, Map as MapboxMap } from 'react-map-gl';
 import { ClientUtils, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnOne, setupMoveUpEvents } from '../../../../ClientUtils';
 import { emptyFunction } from '../../../../Utils';
-import { Doc, DocListCast, LinkedTo, Opt } from '../../../../fields/Doc';
+import { Doc, DocListCast, Field, LinkedTo, Opt } from '../../../../fields/Doc';
 import { DocCss, Highlight } from '../../../../fields/DocSymbols';
 import { Id } from '../../../../fields/FieldSymbols';
 import { DocCast, NumCast, StrCast } from '../../../../fields/Types';
@@ -15,7 +15,6 @@ import { DocUtils, Docs } from '../../../documents/Documents';
 import { DocumentManager } from '../../../util/DocumentManager';
 import { DragManager } from '../../../util/DragManager';
 import { LinkManager } from '../../../util/LinkManager';
-import { SnappingManager } from '../../../util/SnappingManager';
 import { Transform } from '../../../util/Transform';
 import { UndoManager, undoable } from '../../../util/UndoManager';
 import { PinProps, ViewBoxAnnotatableComponent } from '../../DocComponent';
@@ -25,9 +24,9 @@ import { Colors } from '../../global/globalEnums';
 import { DocumentView } from '../DocumentView';
 import { FieldView, FieldViewProps, FocusViewOptions } from '../FieldView';
 import { MapAnchorMenu } from '../MapBox/MapAnchorMenu';
+import '../MapBox/MapBox.scss';
 import { FormattedTextBox } from '../formattedText/FormattedTextBox';
 import { PresBox } from '../trails';
-import './MapBox.scss';
 
 /**
  * MapBox architecture:
@@ -43,7 +42,6 @@ import './MapBox.scss';
  */
 
 const mapboxApiKey = 'pk.eyJ1IjoiemF1bHRhdmFuZ2FyIiwiYSI6ImNsbnc2eHJpbTA1ZTUyam85aGx4Z2FhbGwifQ.2Kqw9mk-9wAAg9kmHmKzcg';
-const bingApiKey = process.env.BING_MAPS; // if you're running local, get a Bing Maps api key here: https://www.bingmapsportal.com/  and then add it to the .env file in the Dash-Web root directory as: _CLIENT_BING_MAPS=<your apikey>
 
 /**
  * Consider integrating later: allows for drawing, circling, making shapes on map
@@ -89,7 +87,7 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent<FieldViewProps>
         return this.allAnnotations.filter(anno => anno.type === DocumentType.PUSHPIN);
     }
     @computed get SidebarShown() {
-        return this.layoutDoc._layout_showSidebar ? true : false;
+        return !!this.layoutDoc._layout_showSidebar;
     }
     @computed get sidebarWidthPercent() {
         return StrCast(this.layoutDoc._layout_sidebarWidthPercent, '0%');
@@ -139,14 +137,18 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent<FieldViewProps>
                     }
                 });
             }
-        }); //add to annotation list
+        }); // add to annotation list
 
         return this.addDocument(doc, sidebarKey); // add to sidebar list
     };
 
     removeMapDocument = (doc: Doc | Doc[], annotationKey?: string) => {
         const docs = doc instanceof Doc ? [doc] : doc;
-        this.allAnnotations.filter(anno => docs.includes(DocCast(anno.mapPin))).forEach(anno => (anno.mapPin = undefined));
+        this.allAnnotations
+            .filter(anno => docs.includes(DocCast(anno.mapPin)))
+            .forEach(anno => {
+                anno.mapPin = undefined;
+            });
         return this.removeDocument(doc, annotationKey, undefined);
     };
 
@@ -206,7 +208,7 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent<FieldViewProps>
                     backgroundColor: this.SidebarShown ? Colors.MEDIUM_BLUE : Colors.BLACK,
                 }}
                 onPointerDown={this.sidebarBtnDown}>
-                <FontAwesomeIcon style={{ color: Colors.WHITE }} icon={'comment-alt'} size="sm" />
+                <FontAwesomeIcon style={{ color: Colors.WHITE }} icon="comment-alt" size="sm" />
             </div>
         );
     }
@@ -605,7 +607,7 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent<FieldViewProps>
         this._bingMap.current.entities.remove(this.map_docToPinMap.get(pin));
         this.map_docToPinMap.delete(pin);
         const newpin = new this.MicrosoftMaps.Pushpin(new this.MicrosoftMaps.Location(pin.latitude, pin.longitude), color ? { color } : {});
-        this.MicrosoftMaps.Events.addHandler(newpin, 'click', (e: any) => this.pushpinClicked(pin));
+        this.MicrosoftMaps.Events.addHandler(newpin, 'click', () => this.pushpinClicked(pin));
         this._bingMap.current.entities.push(newpin);
         this.map_docToPinMap.set(pin, newpin);
     };
@@ -637,7 +639,7 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent<FieldViewProps>
             () => this.allAnnotations.map(doc => doc[Highlight]),
             () => {
                 const allConfigPins = this.allAnnotations.map(doc => ({ doc, pushpin: DocCast(doc.mapPin) })).filter(pair => pair.pushpin);
-                allConfigPins.forEach(({ doc, pushpin }) => {
+                allConfigPins.forEach(({ pushpin }) => {
                     if (!pushpin[Highlight] && this.map_pinHighlighted.get(pushpin)) {
                         this.recolorPin(pushpin);
                         this.map_pinHighlighted.delete(pushpin);
@@ -736,7 +738,6 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent<FieldViewProps>
             return null;
         }
 
-        const renderAnnotations = (childFilters?: () => string[]) => null;
         return (
             <div className="mapBox" ref={this._ref}>
                 <div
@@ -746,15 +747,11 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent<FieldViewProps>
                         e.button === 0 && !e.ctrlKey && e.stopPropagation();
                     }}
                     style={{ width: `calc(100% - ${this.sidebarWidthPercent})`, pointerEvents: this.pointerEvents() }}>
-                    <div style={{ mixBlendMode: 'multiply' }}>{renderAnnotations(this.transparentFilter)}</div>
-                    {renderAnnotations(this.opaqueFilter)}
-                    {SnappingManager.IsDragging ? null : renderAnnotations()}
-
                     <div className="mapBox-searchbar">
                         <EditableText
                             // editing
                             setVal={(newText: string | number) => typeof newText === 'string' && this.searchbarOnEdit(newText)}
-                            onEnter={e => this.bingSearch()}
+                            onEnter={() => this.bingSearch()}
                             placeholder={this.bingSearchBarContents || 'enter city/zip/...'}
                             textAlign="center"
                         />
@@ -792,7 +789,7 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent<FieldViewProps>
                             ? null
                             : this.allAnnotations
                                   .filter(anno => !anno.layout_unrendered)
-                                  .map((pushpin, i) => (
+                                  .map(pushpin => (
                                       <DocumentView
                                           key={pushpin[Id]}
                                           // eslint-disable-next-line react/jsx-props-no-spreading
diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx
index 8140c0ca7..cc897aaef 100644
--- a/src/client/views/nodes/PDFBox.tsx
+++ b/src/client/views/nodes/PDFBox.tsx
@@ -1,3 +1,5 @@
+/* eslint-disable jsx-a11y/no-static-element-interactions */
+/* eslint-disable jsx-a11y/control-has-associated-label */
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { action, computed, IReactionDisposer, makeObservable, observable, reaction, runInAction } from 'mobx';
 import { observer } from 'mobx-react';
@@ -26,7 +28,7 @@ import { ContextMenu } from '../ContextMenu';
 import { ContextMenuProps } from '../ContextMenuItem';
 import { PinProps, ViewBoxAnnotatableComponent, ViewBoxInterface } from '../DocComponent';
 import { Colors } from '../global/globalEnums';
-import { CreateImage } from '../nodes/WebBoxRenderer';
+import { CreateImage } from './WebBoxRenderer';
 import { PDFViewer } from '../pdf/PDFViewer';
 import { SidebarAnnos } from '../SidebarAnnos';
 import { DocumentView, OpenWhere } from './DocumentView';
@@ -67,8 +69,16 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
         const nh = Doc.NativeHeight(this.Document, this.dataDoc) || 1200;
         !this.Document._layout_fitWidth && (this.Document._height = NumCast(this.Document._width) * (nh / nw));
         if (this.pdfUrl) {
-            if (PDFBox.pdfcache.get(this.pdfUrl.url.href)) runInAction(() => (this._pdf = PDFBox.pdfcache.get(this.pdfUrl!.url.href)));
-            else if (PDFBox.pdfpromise.get(this.pdfUrl.url.href)) PDFBox.pdfpromise.get(this.pdfUrl.url.href)?.then(action((pdf: any) => (this._pdf = pdf)));
+            if (PDFBox.pdfcache.get(this.pdfUrl.url.href))
+                runInAction(() => {
+                    this._pdf = PDFBox.pdfcache.get(this.pdfUrl!.url.href);
+                });
+            else if (PDFBox.pdfpromise.get(this.pdfUrl.url.href))
+                PDFBox.pdfpromise.get(this.pdfUrl.url.href)?.then(
+                    action((pdf: any) => {
+                        this._pdf = pdf;
+                    })
+                );
         }
     }
 
@@ -86,7 +96,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
         if (oldDiv instanceof HTMLCanvasElement) {
             const canvas = oldDiv;
             const img = document.createElement('img'); // create a Image Element
-            img.src = canvas.toDataURL(); //image sourcez
+            img.src = canvas.toDataURL(); // image sourcez
             img.style.width = canvas.style.width;
             img.style.height = canvas.style.height;
             const newCan = newDiv as HTMLCanvasElement;
@@ -97,7 +107,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
     };
 
     crop = (region: Doc | undefined, addCrop?: boolean) => {
-        if (!region) return;
+        if (!region) return undefined;
         const cropping = Doc.MakeCopy(region, true);
         const regionData = region[DocData];
         regionData.lockedPosition = true;
@@ -112,11 +122,11 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
         this.replaceCanvases(docViewContent, newDiv);
         const htmlString = this._pdfViewer?._mainCont.current && new XMLSerializer().serializeToString(newDiv);
 
-        const anchx = NumCast(cropping.x);
-        const anchy = NumCast(cropping.y);
+        // const anchx = NumCast(cropping.x);
+        // const anchy = NumCast(cropping.y);
         const anchw = NumCast(cropping._width) * (this._props.NativeDimScaling?.() || 1);
         const anchh = NumCast(cropping._height) * (this._props.NativeDimScaling?.() || 1);
-        const viewScale = 1;
+        // const viewScale = 1;
         cropping.title = 'crop: ' + this.Document.title;
         cropping.x = NumCast(this.Document.x) + NumCast(this.layoutDoc._width);
         cropping.y = NumCast(this.Document.y);
@@ -157,7 +167,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
                     )
                 );
             })
-            .catch(function (error: any) {
+            .catch((error: any) => {
                 console.error('oops, something went wrong!', error);
             });
 
@@ -245,7 +255,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
         }
         const docAnchor = () =>
             Docs.Create.ConfigDocument({
-                title: StrCast(this.Document.title + '@' + NumCast(this.layoutDoc._layout_scrollTop)?.toFixed(0)),
+                title: StrCast(this.Document.title + '@' + (NumCast(this.layoutDoc._layout_scrollTop) ?? 0).toFixed(0)),
                 annotationOn: this.Document,
             });
         const visibleAnchor = this._pdfViewer?._getAnchor?.(this._pdfViewer.savedAnnotations(), true);
@@ -288,7 +298,9 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
         this.Document._layout_curPage = Math.min(NumCast(this.dataDoc[this._props.fieldKey + '_numPages']), (NumCast(this.Document._layout_curPage) || 1) + 1);
         return true;
     };
-    public gotoPage = (p: number) => (this.Document._layout_curPage = p);
+    public gotoPage = (p: number) => {
+        this.Document._layout_curPage = p;
+    };
 
     @undoBatch
     onKeyDown = action((e: KeyboardEvent) => {
@@ -300,6 +312,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
             case 'PageUp':
                 processed = this.backPage();
                 break;
+            default:
         }
         if (processed) {
             e.stopImmediatePropagation();
@@ -315,7 +328,9 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
             this._initialScrollTarget = undefined;
         }
     };
-    searchStringChanged = (e: React.ChangeEvent<HTMLInputElement>) => (this._searchString = e.currentTarget.value);
+    searchStringChanged = (e: React.ChangeEvent<HTMLInputElement>) => {
+        this._searchString = e.currentTarget.value;
+    };
 
     // adding external documents; to sidebar key
     // if (doc.Geolocation) this.addDocument(doc, this.fieldkey+"_annotation")
@@ -371,11 +386,11 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
     settingsPanel() {
         const pageBtns = (
             <>
-                <button className="pdfBox-backBtn" key="back" title="Page Back" onPointerDown={e => e.stopPropagation()} onClick={this.backPage}>
-                    <FontAwesomeIcon style={{ color: 'white' }} icon={'arrow-left'} size="sm" />
+                <button type="button" className="pdfBox-backBtn" key="back" title="Page Back" onPointerDown={e => e.stopPropagation()} onClick={this.backPage}>
+                    <FontAwesomeIcon style={{ color: 'white' }} icon="arrow-left" size="sm" />
                 </button>
-                <button className="pdfBox-fwdBtn" key="fwd" title="Page Forward" onPointerDown={e => e.stopPropagation()} onClick={this.forwardPage}>
-                    <FontAwesomeIcon style={{ color: 'white' }} icon={'arrow-right'} size="sm" />
+                <button type="button" className="pdfBox-fwdBtn" key="fwd" title="Page Forward" onPointerDown={e => e.stopPropagation()} onClick={this.forwardPage}>
+                    <FontAwesomeIcon style={{ color: 'white' }} icon="arrow-right" size="sm" />
                 </button>
             </>
         );
@@ -388,7 +403,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
                 onPointerDown={e => e.stopPropagation()}
                 style={{ display: this._props.isContentActive() ? 'flex' : 'none' }}>
                 <div className="pdfBox-overlayCont" onPointerDown={e => e.stopPropagation()} style={{ left: `${this._searching ? 0 : 100}%` }}>
-                    <button className="pdfBox-overlayButton" title={searchTitle} />
+                    <button type="button" className="pdfBox-overlayButton" title={searchTitle} />
                     <input
                         className="pdfBox-searchBar"
                         placeholder="Search"
@@ -399,17 +414,18 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
                             e.keyCode === KeyCodes.ENTER && this.search(this._searchString, e.shiftKey);
                         }}
                     />
-                    <button className="pdfBox-search" title="Search" onClick={e => this.search(this._searchString, e.shiftKey)}>
+                    <button type="button" className="pdfBox-search" title="Search" onClick={e => this.search(this._searchString, e.shiftKey)}>
                         <FontAwesomeIcon icon="search" size="sm" />
                     </button>
-                    <button className="pdfBox-prevIcon" title="Previous Annotation" onClick={this.prevAnnotation}>
-                        <FontAwesomeIcon icon={'arrow-up'} size="lg" />
+                    <button type="button" className="pdfBox-prevIcon" title="Previous Annotation" onClick={this.prevAnnotation}>
+                        <FontAwesomeIcon icon="arrow-up" size="lg" />
                     </button>
-                    <button className="pdfBox-nextIcon" title="Next Annotation" onClick={this.nextAnnotation}>
-                        <FontAwesomeIcon icon={'arrow-down'} size="lg" />
+                    <button type="button" className="pdfBox-nextIcon" title="Next Annotation" onClick={this.nextAnnotation}>
+                        <FontAwesomeIcon icon="arrow-down" size="lg" />
                     </button>
                 </div>
                 <button
+                    type="button"
                     className="pdfBox-overlayButton"
                     title={searchTitle}
                     onClick={action(() => {
@@ -426,9 +442,13 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
                     <input
                         value={curPage}
                         style={{ width: `${curPage > 99 ? 4 : 3}ch`, pointerEvents: 'all' }}
-                        onChange={e => (this.Document._layout_curPage = Number(e.currentTarget.value))}
+                        onChange={e => {
+                            this.Document._layout_curPage = Number(e.currentTarget.value);
+                        }}
                         onKeyDown={e => e.stopPropagation()}
-                        onClick={action(() => (this._pageControls = !this._pageControls))}
+                        onClick={action(() => {
+                            this._pageControls = !this._pageControls;
+                        })}
                     />
                     {this._pageControls ? pageBtns : null}
                 </div>
@@ -443,14 +463,16 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
         return PDFBox.sidebarResizerWidth + nativeDiff * (this._props.NativeDimScaling?.() || 1);
     };
     @undoBatch
-    toggleSidebarType = () => (this.dataDoc[this.SidebarKey + '_type_collection'] = this.dataDoc[this.SidebarKey + '_type_collection'] === CollectionViewType.Freeform ? CollectionViewType.Stacking : CollectionViewType.Freeform);
-    specificContextMenu = (e: React.MouseEvent): void => {
+    toggleSidebarType = () => {
+        this.dataDoc[this.SidebarKey + '_type_collection'] = this.dataDoc[this.SidebarKey + '_type_collection'] === CollectionViewType.Freeform ? CollectionViewType.Stacking : CollectionViewType.Freeform;
+    };
+    specificContextMenu = (): void => {
         const cm = ContextMenu.Instance;
         const options = cm.findByDescription('Options...');
         const optionItems: ContextMenuProps[] = options && 'subitems' in options ? options.subitems : [];
         !Doc.noviceMode && optionItems.push({ description: 'Toggle Sidebar Type', event: this.toggleSidebarType, icon: 'expand-arrows-alt' });
         !Doc.noviceMode && optionItems.push({ description: 'update icon', event: () => this.pdfUrl && this.updateIcon(), icon: 'expand-arrows-alt' });
-        //optionItems.push({ description: "Toggle Sidebar ", event: () => this.toggleSidebar(), icon: "expand-arrows-alt" });
+        // optionItems.push({ description: "Toggle Sidebar ", event: () => this.toggleSidebar(), icon: "expand-arrows-alt" });
         !options && ContextMenu.Instance.addItem({ description: 'Options...', subitems: optionItems, icon: 'asterisk' });
         const help = cm.findByDescription('Help...');
         const helpItems: ContextMenuProps[] = help && 'subitems' in help ? help.subitems : [];
@@ -472,7 +494,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
     anchorMenuClick = () => this._sidebarRef.current?.anchorMenuClick;
     @observable _showSidebar = false;
     @computed get SidebarShown() {
-        return this._showSidebar || this.layoutDoc._show_sidebar ? true : false;
+        return !!(this._showSidebar || this.layoutDoc._show_sidebar);
     }
     @computed get sidebarHandle() {
         return (
@@ -486,7 +508,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
                     backgroundColor: this.SidebarShown ? Colors.MEDIUM_BLUE : Colors.BLACK,
                 }}
                 onPointerDown={e => this.sidebarBtnDown(e, true)}>
-                <FontAwesomeIcon style={{ color: Colors.WHITE }} icon={'comment-alt'} size="sm" />
+                <FontAwesomeIcon style={{ color: Colors.WHITE }} icon="comment-alt" size="sm" />
             </div>
         );
     }
@@ -517,6 +539,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
             return ComponentTag === CollectionStackingView ? (
                 <SidebarAnnos
                     ref={this._sidebarRef}
+                    // eslint-disable-next-line react/jsx-props-no-spreading
                     {...this._props}
                     Document={this.Document}
                     layoutDoc={this.layoutDoc}
@@ -532,6 +555,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
             ) : (
                 <div onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, () => SelectionManager.SelectView(this.DocumentView?.()!, false), true)}>
                     <ComponentTag
+                        // eslint-disable-next-line react/jsx-props-no-spreading
                         {...this._props}
                         setContentViewBox={emptyFunction} // override setContentView to do nothing
                         NativeWidth={this.sidebarNativeWidthFunc}
@@ -542,7 +566,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
                         yPadding={0}
                         viewField={this.SidebarKey}
                         isAnnotationOverlay={false}
-                        originTopLeft={true}
+                        originTopLeft
                         isAnyChildContentActive={this.isAnyChildContentActive}
                         select={emptyFunction}
                         whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
@@ -551,7 +575,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
                         addDocument={this.sidebarAddDocument}
                         ScreenToLocalTransform={this.sidebarScreenToLocal}
                         renderDepth={this._props.renderDepth + 1}
-                        noSidebar={true}
+                        noSidebar
                         fieldKey={this.SidebarKey}
                     />
                 </div>
@@ -585,6 +609,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
                         top: 0,
                     }}>
                     <PDFViewer
+                        // eslint-disable-next-line react/jsx-props-no-spreading
                         {...this._props}
                         pdfBox={this}
                         sidebarAddDoc={this.sidebarAddDocument}
@@ -617,10 +642,19 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
         const pdfView = !this._pdf ? null : this.renderPdfView;
         const href = this.pdfUrl?.url.href;
         if (!pdfView && href) {
-            if (PDFBox.pdfcache.get(href)) setTimeout(action(() => (this._pdf = PDFBox.pdfcache.get(href))));
+            if (PDFBox.pdfcache.get(href))
+                setTimeout(
+                    action(() => {
+                        this._pdf = PDFBox.pdfcache.get(href);
+                    })
+                );
             else {
                 if (!PDFBox.pdfpromise.get(href)) PDFBox.pdfpromise.set(href, Pdfjs.getDocument(href).promise);
-                PDFBox.pdfpromise.get(href)?.then(action((pdf: any) => PDFBox.pdfcache.set(href, (this._pdf = pdf))));
+                PDFBox.pdfpromise.get(href)?.then(
+                    action((pdf: any) => {
+                        PDFBox.pdfcache.set(href, (this._pdf = pdf));
+                    })
+                );
             }
         }
         return pdfView ?? this.renderTitleBox;
diff --git a/src/client/views/nodes/RecordingBox/RecordingBox.tsx b/src/client/views/nodes/RecordingBox/RecordingBox.tsx
index 40199cce1..7d123d90c 100644
--- a/src/client/views/nodes/RecordingBox/RecordingBox.tsx
+++ b/src/client/views/nodes/RecordingBox/RecordingBox.tsx
@@ -47,7 +47,9 @@ export class RecordingBox extends ViewBoxBaseComponent<FieldViewProps>() {
     @observable videoDuration: number | undefined = undefined;
 
     @action
-    setVideoDuration = (duration: number) => (this.videoDuration = duration);
+    setVideoDuration = (duration: number) => {
+        this.videoDuration = duration;
+    };
 
     @action
     setResult = (info: Upload.AccessPathInfo, presentation?: Presentation) => {
@@ -69,15 +71,15 @@ export class RecordingBox extends ViewBoxBaseComponent<FieldViewProps>() {
     public static WorkspaceStopRecording() {
         const remDoc = RecordingBox.screengrabber?.Document;
         if (remDoc) {
-            //if recordingbox is true; when we press the stop button. changed vals temporarily to see if changes happening
+            // if recordingbox is true; when we press the stop button. changed vals temporarily to see if changes happening
             RecordingBox.screengrabber?.Pause?.();
             setTimeout(() => {
                 RecordingBox.screengrabber?.Finish?.();
-                remDoc.overlayX = 70; //was 100
+                remDoc.overlayX = 70; // was 100
                 remDoc.overlayY = 590;
                 RecordingBox.screengrabber = undefined;
             }, 100);
-            //could break if recording takes too long to turn into videobox. If so, either increase time on setTimeout below or find diff place to do this
+            // could break if recording takes too long to turn into videobox. If so, either increase time on setTimeout below or find diff place to do this
             setTimeout(() => Doc.RemFromMyOverlay(remDoc), 1000);
             Doc.UserDoc().workspaceRecordingState = mediaState.Paused;
             Doc.AddDocToList(Doc.UserDoc(), 'workspaceRecordings', remDoc);
@@ -103,10 +105,10 @@ export class RecordingBox extends ViewBoxBaseComponent<FieldViewProps>() {
                       _width: 205,
                       _height: 115,
                   });
-        screengrabber.overlayX = 70; //was -400
-        screengrabber.overlayY = 590; //was 0
+        screengrabber.overlayX = 70; // was -400
+        screengrabber.overlayY = 590; // was 0
         screengrabber[DocData][Doc.LayoutFieldKey(screengrabber) + '_trackScreen'] = true;
-        Doc.AddToMyOverlay(screengrabber); //just adds doc to overlay
+        Doc.AddToMyOverlay(screengrabber); // just adds doc to overlay
         DocumentManager.Instance.AddViewRenderedCb(screengrabber, docView => {
             RecordingBox.screengrabber = docView.ComponentView as RecordingBox;
             RecordingBox.screengrabber.Record?.();
@@ -137,7 +139,7 @@ export class RecordingBox extends ViewBoxBaseComponent<FieldViewProps>() {
      */
     @undoBatch
     public static addRecToWorkspace(value: RecordingBox) {
-        let ffView = Array.from(DocumentManager.Instance.DocumentViews).find(view => view.ComponentView instanceof CollectionFreeFormView);
+        const ffView = Array.from(DocumentManager.Instance.DocumentViews).find(view => view.ComponentView instanceof CollectionFreeFormView);
         (ffView?.ComponentView as CollectionFreeFormView)._props.addDocument?.(value.Document);
         Doc.RemoveDocFromList(Doc.UserDoc(), 'workspaceRecordings', value.Document);
         Doc.RemFromMyOverlay(value.Document);
@@ -204,51 +206,66 @@ export class RecordingBox extends ViewBoxBaseComponent<FieldViewProps>() {
             </div>
         );
     }
+    // eslint-disable-next-line no-use-before-define
     static screengrabber: RecordingBox | undefined;
 }
 
+// eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function stopWorkspaceRecording() {
     RecordingBox.WorkspaceStopRecording();
 });
 
+// eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function stopWorkspaceReplaying(value: Doc) {
     RecordingBox.stopWorkspaceReplaying(value);
 });
+// eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function removeWorkspaceReplaying(value: Doc) {
     RecordingBox.removeWorkspaceReplaying(value);
 });
 
+// eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function getCurrentRecording() {
     return Doc.UserDoc().currentRecording;
 });
+// eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function getWorkspaceRecordings() {
     return new List<any>(['Record Workspace', `Record Webcam`, ...DocListCast(Doc.UserDoc().workspaceRecordings)]);
 });
+// eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function isWorkspaceRecording() {
     return Doc.UserDoc().workspaceRecordingState === mediaState.Recording;
 });
+// eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function isWorkspaceReplaying() {
     return Doc.UserDoc().workspaceReplayingState;
 });
+// eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function replayWorkspace(value: Doc | string, _readOnly_: boolean) {
     if (_readOnly_) return DocCast(Doc.UserDoc().currentRecording) ?? 'Record Workspace';
     if (typeof value === 'string') RecordingBox.WorkspaceStartRecording(value);
     else RecordingBox.replayWorkspace(value);
+    return undefined;
 });
-ScriptingGlobals.add(function pauseWorkspaceReplaying(value: Doc, _readOnly_: boolean) {
+// eslint-disable-next-line prefer-arrow-callback
+ScriptingGlobals.add(function pauseWorkspaceReplaying(value: Doc) {
     RecordingBox.pauseWorkspaceReplaying(value);
 });
-ScriptingGlobals.add(function resumeWorkspaceReplaying(value: Doc, _readOnly_: boolean) {
+// eslint-disable-next-line prefer-arrow-callback
+ScriptingGlobals.add(function resumeWorkspaceReplaying(value: Doc) {
     RecordingBox.resumeWorkspaceReplaying(value);
 });
 
+// eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function startRecordingDrag(value: { doc: Doc | string; e: React.PointerEvent }) {
     if (DocCast(value.doc)) {
         DragManager.StartDocumentDrag([value.e.target as HTMLElement], new DragManager.DocumentDragData([DocCast(value.doc)], dropActionType.embed), value.e.clientX, value.e.clientY);
         value.e.preventDefault();
         return true;
     }
+    return undefined;
 });
+// eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function renderDropdown() {
     if (!Doc.UserDoc().workspaceRecordings || DocListCast(Doc.UserDoc().workspaceRecordings).length === 0) {
         return true;
diff --git a/src/client/views/nodes/ScreenshotBox.tsx b/src/client/views/nodes/ScreenshotBox.tsx
index e29e47514..882f6ba9e 100644
--- a/src/client/views/nodes/ScreenshotBox.tsx
+++ b/src/client/views/nodes/ScreenshotBox.tsx
@@ -1,3 +1,4 @@
+/* eslint-disable jsx-a11y/media-has-caption */
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import * as React from 'react';
 // import { Canvas } from '@react-three/fiber';
@@ -163,7 +164,7 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
         ind !== -1 && DocUtils.ActiveRecordings.splice(ind, 1);
     }
 
-    specificContextMenu = (e: React.MouseEvent): void => {
+    specificContextMenu = (): void => {
         const subitems = [{ description: 'Screen Capture', event: this.toggleRecording, icon: 'expand-arrows-alt' as any }];
         ContextMenu.Instance.addItem({ description: 'Options...', subitems, icon: 'video' });
     };
@@ -171,7 +172,7 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
     @computed get content() {
         return (
             <video
-                className={'videoBox-content'}
+                className="videoBox-content"
                 key="video"
                 ref={r => {
                     this._videoRef = r;
@@ -184,7 +185,7 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
                 autoPlay={this._screenCapture}
                 style={{ width: this._screenCapture ? '100%' : undefined, height: this._screenCapture ? '100%' : undefined }}
                 onCanPlay={this.videoLoad}
-                controls={true}
+                controls
                 onClick={e => e.preventDefault()}>
                 <source type="video/mp4" />
                 Not supported.
@@ -221,23 +222,23 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
     toggleRecording = async () => {
         if (!this._screenCapture) {
             this._audioRec = new MediaRecorder(await navigator.mediaDevices.getUserMedia({ audio: true }));
-            const aud_chunks: any = [];
-            this._audioRec.ondataavailable = (e: any) => aud_chunks.push(e.data);
-            this._audioRec.onstop = async (e: any) => {
-                const [{ result }] = await Networking.UploadFilesToServer(aud_chunks.map((file: any) => ({ file })));
+            const audChunks: any = [];
+            this._audioRec.ondataavailable = (e: any) => audChunks.push(e.data);
+            this._audioRec.onstop = async () => {
+                const [{ result }] = await Networking.UploadFilesToServer(audChunks.map((file: any) => ({ file })));
                 if (!(result instanceof Error)) {
                     this.dataDoc[this._props.fieldKey + '_audio'] = new AudioField(result.accessPaths.agnostic.client);
                 }
             };
             this._videoRef!.srcObject = await (navigator.mediaDevices as any).getDisplayMedia({ video: true });
             this._videoRec = new MediaRecorder(this._videoRef!.srcObject);
-            const vid_chunks: any = [];
+            const vidChunks: any = [];
             this._videoRec.onstart = () => {
                 if (this.dataDoc[this._props.fieldKey + '_trackScreen']) TrackMovements.Instance.start();
                 this.dataDoc[this._props.fieldKey + '_recordingStart'] = new DateField(new Date());
             };
-            this._videoRec.ondataavailable = (e: any) => vid_chunks.push(e.data);
-            this._videoRec.onstop = async (e: any) => {
+            this._videoRec.ondataavailable = (e: any) => vidChunks.push(e.data);
+            this._videoRec.onstop = async () => {
                 const presentation = TrackMovements.Instance.yieldPresentation();
                 if (presentation?.movements) {
                     const presCopy = { ...presentation };
@@ -245,7 +246,7 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
                     this.dataDoc[this.fieldKey + '_presentation'] = JSON.stringify(presCopy);
                 }
                 TrackMovements.Instance.finish();
-                const file = new File(vid_chunks, `${this.Document[Id]}.mkv`, { type: vid_chunks[0].type, lastModified: Date.now() });
+                const file = new File(vidChunks, `${this.Document[Id]}.mkv`, { type: vidChunks[0].type, lastModified: Date.now() });
                 const [{ result }] = await Networking.UploadFilesToServer({ file });
                 this.dataDoc[this.fieldKey + '_duration'] = (new Date().getTime() - this.recordingStart!) / 1000;
                 if (!(result instanceof Error)) {
@@ -298,6 +299,7 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
                 <div className="videoBox-viewer">
                     <div style={{ position: 'relative', height: this.videoPanelHeight() }}>
                         <CollectionFreeFormView
+                            // eslint-disable-next-line react/jsx-props-no-spreading
                             {...this._props}
                             setContentViewBox={emptyFunction}
                             NativeWidth={returnZero}
@@ -306,7 +308,7 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
                             PanelWidth={this._props.PanelWidth}
                             focus={this._props.focus}
                             isSelected={this._props.isSelected}
-                            isAnnotationOverlay={true}
+                            isAnnotationOverlay
                             select={emptyFunction}
                             isContentActive={returnFalse}
                             NativeDimScaling={returnOne}
@@ -325,9 +327,10 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
                     <div style={{ background: SettingsManager.userColor, position: 'relative', height: this.formattedPanelHeight() }}>
                         {!(this.dataDoc[this.fieldKey + '_dictation'] instanceof Doc) ? null : (
                             <FormattedTextBox
+                                // eslint-disable-next-line react/jsx-props-no-spreading
                                 {...this._props}
                                 Document={DocCast(this.dataDoc[this.fieldKey + '_dictation'])}
-                                fieldKey={'text'}
+                                fieldKey="text"
                                 PanelHeight={this.formattedPanelHeight}
                                 select={emptyFunction}
                                 isContentActive={emptyFunction}
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index 60141b2a6..5b3f37993 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -1,3 +1,4 @@
+/* eslint-disable jsx-a11y/media-has-caption */
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { action, computed, IReactionDisposer, makeObservable, observable, ObservableMap, reaction, runInAction } from 'mobx';
 import { observer } from 'mobx-react';
@@ -153,11 +154,14 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl
                     clearTimeout(this._controlsFadeTimer);
                     this._scrubbing = true;
                     this._controlsFadeTimer = setTimeout(
-                        action(() => (this._scrubbing = false)),
+                        action(() => {
+                            this._scrubbing = false;
+                        }),
                         500
                     );
                     e.stopPropagation();
                     break;
+                default:
             }
         }
     };
@@ -204,7 +208,9 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl
         else {
             this._keepCurrentlyPlaying = true;
             this.pause();
-            setTimeout(() => (this._keepCurrentlyPlaying = false));
+            setTimeout(() => {
+                this._keepCurrentlyPlaying = false;
+            });
         }
     };
 
@@ -247,7 +253,9 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl
             clearTimeout(this._controlsFadeTimer);
             this._controlsVisible = true;
             this._controlsFadeTimer = setTimeout(
-                action(() => (this._controlsVisible = false)),
+                action(() => {
+                    this._controlsVisible = false;
+                }),
                 3000
             );
         }
@@ -281,7 +289,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl
         const canvas = document.createElement('canvas');
         canvas.width = 640;
         canvas.height = (640 * Doc.NativeHeight(this.layoutDoc)) / (Doc.NativeWidth(this.layoutDoc) || 1);
-        const ctx = canvas.getContext('2d'); //draw image to canvas. scale to target dimensions
+        const ctx = canvas.getContext('2d'); // draw image to canvas. scale to target dimensions
         if (ctx) {
             this._videoRef && ctx.drawImage(this._videoRef, 0, 0, canvas.width, canvas.height);
         }
@@ -298,7 +306,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl
             this._props.addDocument?.(b);
             DocUtils.MakeLink(b, this.Document, { link_relationship: 'video snapshot' });
         } else {
-            //convert to desired file format
+            // convert to desired file format
             const dataUrl = canvas.toDataURL('image/png'); // can also use 'image/png'
             // if you want to preview the captured image,
             const retitled = StrCast(this.Document.title).replace(/[ -\.:]/g, '');
@@ -335,7 +343,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl
         Doc.SetNativeWidth(imageSnapshot[DocData], Doc.NativeWidth(this.layoutDoc));
         Doc.SetNativeHeight(imageSnapshot[DocData], Doc.NativeHeight(this.layoutDoc));
         this._props.addDocument?.(imageSnapshot);
-        const link = DocUtils.MakeLink(imageSnapshot, this.getAnchor(true), { link_relationship: 'video snapshot' });
+        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);  // 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));
     };
@@ -346,7 +354,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl
         if (!addAsAnnotation && marquee) marquee.backgroundColor = 'transparent';
         const anchor =
             addAsAnnotation && marquee
-                ? CollectionStackedTimeline.createAnchor(this.Document, this.dataDoc, this.annotationKey, timecode ? timecode : undefined, undefined, marquee, addAsAnnotation) || this.Document
+                ? CollectionStackedTimeline.createAnchor(this.Document, this.dataDoc, this.annotationKey, timecode || undefined, undefined, marquee, addAsAnnotation) || this.Document
                 : Docs.Create.ConfigDocument({ title: '#' + timecode, _timecodeToShow: timecode, annotationOn: this.Document });
         PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), temporal: true, pannable: true } }, this.Document);
         return anchor;
@@ -376,12 +384,14 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl
         if (this._stackedTimeline?.makeDocUnfiltered(doc)) {
             if (this.heightPercent === 100) {
                 // do we want to always open up the timeline when followin a link? kind of clunky visually
-                //this.layoutDoc._layout_timelineHeightPercent = VideoBox.heightPercent;
+                // this.layoutDoc._layout_timelineHeightPercent = VideoBox.heightPercent;
                 options.didMove = true;
             }
             return this._stackedTimeline.getView(doc, options);
         }
-        return new Promise<Opt<DocumentView>>(res => DocumentManager.Instance.AddViewRenderedCb(doc, dv => res(dv)));
+        return new Promise<Opt<DocumentView>>(res => {
+            DocumentManager.Instance.AddViewRenderedCb(doc, dv => res(dv));
+        });
     };
 
     // extracts video thumbnails and saves them as field of doc
@@ -391,7 +401,9 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl
         const thumbnailPromises: Promise<any>[] = [];
         const video = document.createElement('video');
 
-        video.onloadedmetadata = () => (video.currentTime = 0);
+        video.onloadedmetadata = () => {
+            video.currentTime = 0;
+        };
 
         video.onseeked = () => {
             const canvas = document.createElement('canvas');
@@ -405,7 +417,9 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl
             if (newTime < video.duration) {
                 video.currentTime = newTime;
             } else {
-                Promise.all(thumbnailPromises).then(thumbnails => (this.dataDoc[this.fieldKey + '_thumbnails'] = new List<string>(thumbnails)));
+                Promise.all(thumbnailPromises).then(thumbnails => {
+                    this.dataDoc[this.fieldKey + '_thumbnails'] = new List<string>(thumbnails);
+                });
             }
         };
 
@@ -424,11 +438,13 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl
             this._disposers.reactionDisposer?.();
             this._disposers.reactionDisposer = reaction(
                 () => NumCast(this.layoutDoc._layout_currentTimecode),
-                time => !this._playing && (vref.currentTime = time),
+                time => {
+                    !this._playing && (vref.currentTime = time);
+                },
                 { fireImmediately: true }
             );
 
-            (!this.dataDoc[this.fieldKey + '_thumbnails'] || StrListCast(this.dataDoc[this.fieldKey + '_thumbnails']).length != VideoBox.numThumbnails) && this.getVideoThumbnails();
+            (!this.dataDoc[this.fieldKey + '_thumbnails'] || StrListCast(this.dataDoc[this.fieldKey + '_thumbnails']).length !== VideoBox.numThumbnails) && this.getVideoThumbnails();
         }
     };
 
@@ -437,7 +453,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl
     setContentRef = (cref: HTMLDivElement | null) => {
         this._contentRef = cref;
         if (cref) {
-            cref.onfullscreenchange = action(e => {
+            cref.onfullscreenchange = action(() => {
                 this._fullScreen = document.fullscreenElement === cref;
                 this._controlsVisible = true;
                 this._scrubbing = false;
@@ -452,7 +468,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl
     };
 
     // context menu
-    specificContextMenu = (e: React.MouseEvent): void => {
+    specificContextMenu = (): void => {
         const field = Cast(this.dataDoc[this._props.fieldKey], VideoField);
         if (field) {
             const url = field.url.href;
@@ -463,7 +479,9 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl
                 subitems.push({
                     description: 'Screen Capture',
                     event: async () => {
-                        runInAction(() => (this._screenCapture = !this._screenCapture));
+                        runInAction(() => {
+                            this._screenCapture = !this._screenCapture;
+                        });
                         this._videoRef!.srcObject = !this._screenCapture ? undefined : await (navigator.mediaDevices as any).getDisplayMedia({ video: true });
                     },
                     icon: 'expand-arrows-alt',
@@ -471,7 +489,9 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl
             subitems.push({ description: (this.layoutDoc.dontAutoFollowLinks ? '' : "Don't") + ' follow links when encountered', event: () => (this.layoutDoc.dontAutoFollowLinks = !this.layoutDoc.dontAutoFollowLinks), icon: 'expand-arrows-alt' });
             subitems.push({
                 description: (this.layoutDoc.dontAutoPlayFollowedLinks ? '' : "Don't") + ' play when link is selected',
-                event: () => (this.layoutDoc.dontAutoPlayFollowedLinks = !this.layoutDoc.dontAutoPlayFollowedLinks),
+                event: () => {
+                    this.layoutDoc.dontAutoPlayFollowedLinks = !this.layoutDoc.dontAutoPlayFollowedLinks;
+                },
                 icon: 'expand-arrows-alt',
             });
             subitems.push({ description: (this.layoutDoc.autoPlayAnchors ? "Don't auto play" : 'Auto play') + ' anchors onClick', event: () => (this.layoutDoc.autoPlayAnchors = !this.layoutDoc.autoPlayAnchors), icon: 'expand-arrows-alt' });
@@ -505,7 +525,9 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl
     };
 
     // ref for updating time
-    setAudioRef = (e: HTMLAudioElement | null) => (this._audioPlayer = e);
+    setAudioRef = (e: HTMLAudioElement | null) => {
+        this._audioPlayer = e;
+    };
 
     // renders the video and audio
     @computed get content() {
@@ -587,7 +609,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl
         setupMoveUpEvents(
             this,
             e,
-            action(encodeURIComponent => {
+            action(() => {
                 this._clicking = false;
                 if (this._props.isContentActive()) {
                     // const local = this.ScreenToLocalTransform().scale(this._props.scaling?.() || 1).transformPoint(e.clientX, e.clientY);
@@ -601,7 +623,9 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl
             () => {
                 this.layoutDoc._layout_timelineHeightPercent = this.heightPercent !== 100 ? 100 : VideoBox.heightPercent;
                 setTimeout(
-                    action(() => (this._clicking = false)),
+                    action(() => {
+                        this._clicking = false;
+                    }),
                     500
                 );
             },
@@ -636,7 +660,9 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl
     addDocWithTimecode(doc: Doc | Doc[]): boolean {
         const docs = doc instanceof Doc ? [doc] : doc;
         const curTime = NumCast(this.layoutDoc._layout_currentTimecode);
-        docs.forEach(doc => (doc._timecodeToHide = (doc._timecodeToShow = curTime) + 1));
+        docs.forEach(doc => {
+            doc._timecodeToHide = (doc._timecodeToShow = curTime) + 1;
+        });
         return this.addDocument(doc);
     }
 
@@ -732,11 +758,9 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl
     // stretches vertically or horizontally depending on video orientation so video fits full screen
     fullScreenSize() {
         if (this._videoRef && this._videoRef.videoHeight / this._videoRef.videoWidth > 1) {
-            //prettier-ignore
-            return ({ height: '100%' });
+            return { height: '100%' };
         }
-        //prettier-ignore
-        return ({ width: '100%' });
+        return ({ width: '100%' }); // prettier-ignore
     }
 
     // for zoom slider, sets timeline waveform zoom
@@ -778,7 +802,10 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl
         this._props.select(true);
     };
 
-    timelineWhenChildContentsActiveChanged = action((isActive: boolean) => this._props.whenChildContentsActiveChanged((this._isAnyChildContentActive = isActive)));
+    timelineWhenChildContentsActiveChanged = action((isActive: boolean) => {
+        this._isAnyChildContentActive = isActive;
+        this._props.whenChildContentsActiveChanged(isActive);
+    });
 
     timelineScreenToLocal = () =>
         this._props
@@ -786,7 +813,9 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl
             .scale(this.scaling())
             .translate(0, (-this.heightPercent / 100) * this._props.PanelHeight());
 
-    setPlayheadTime = (time: number) => (this.player!.currentTime = this.layoutDoc._layout_currentTimecode = time);
+    setPlayheadTime = (time: number) => {
+        this.player!.currentTime = this.layoutDoc._layout_currentTimecode = time;
+    };
 
     timelineHeight = () => (this._props.PanelHeight() * (100 - this.heightPercent)) / 100;
 
@@ -849,7 +878,10 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl
         return (
             <div className="videoBox-stackPanel" style={{ transition: this.transition, height: `${100 - this.heightPercent}%`, display: this.heightPercent === 100 ? 'none' : '' }}>
                 <CollectionStackedTimeline
-                    ref={action((r: any) => (this._stackedTimeline = r))}
+                    ref={action((r: any) => {
+                        this._stackedTimeline = r;
+                    })}
+                    // eslint-disable-next-line react/jsx-props-no-spreading
                     {...this._props}
                     dataFieldKey={this.fieldKey}
                     fieldKey={this.annotationKey}
@@ -887,7 +919,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl
     }
 
     crop = (region: Doc | undefined, addCrop?: boolean) => {
-        if (!region) return;
+        if (!region) return undefined;
         const cropping = Doc.MakeCopy(region, true);
         const regionData = region[DocData];
         regionData.backgroundColor = 'transparent';
@@ -916,8 +948,8 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl
         croppingProto.type = DocumentType.VID;
         croppingProto.layout = VideoBox.LayoutString('data');
         croppingProto.data = ObjectField.MakeCopy(this.dataDoc[this.fieldKey] as ObjectField);
-        croppingProto['data_nativeWidth'] = anchw;
-        croppingProto['data_nativeHeight'] = anchh;
+        croppingProto.data_nativeWidth = anchw;
+        croppingProto.data_nativeHeight = anchh;
         croppingProto.videoCrop = true;
         croppingProto.layout_currentTimecode = this.layoutDoc._layout_currentTimecode;
         croppingProto.freeform_scale = viewScale;
@@ -959,14 +991,15 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl
                             left: (this._props.PanelWidth() - this.panelWidth()) / 2,
                         }}>
                         <CollectionFreeFormView
+                            // eslint-disable-next-line react/jsx-props-no-spreading
                             {...this._props}
                             setContentViewBox={emptyFunction}
                             NativeWidth={returnZero}
                             NativeHeight={returnZero}
                             renderDepth={this._props.renderDepth + 1}
                             fieldKey={this.annotationKey}
-                            isAnnotationOverlay={true}
-                            annotationLayerHostsContent={true}
+                            isAnnotationOverlay
+                            annotationLayerHostsContent
                             PanelWidth={this._props.PanelWidth}
                             PanelHeight={this._props.PanelHeight}
                             isAnyChildContentActive={returnFalse}
@@ -1050,12 +1083,12 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl
                     </div>
                 )}
 
-                <div className="videobox-button" title={'full screen'} onPointerDown={this.onFullDown}>
+                <div className="videobox-button" title="full screen" onPointerDown={this.onFullDown}>
                     <FontAwesomeIcon icon="expand" />
                 </div>
 
                 {!this._fullScreen && width > 300 && (
-                    <div className="videobox-button" title={'show timeline'} onPointerDown={this.onTimelineHdlDown}>
+                    <div className="videobox-button" title="show timeline" onPointerDown={this.onTimelineHdlDown}>
                         <FontAwesomeIcon icon="eye" />
                     </div>
                 )}
diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx
index 446e83dd3..fc2e4bf61 100644
--- a/src/client/views/nodes/WebBox.tsx
+++ b/src/client/views/nodes/WebBox.tsx
@@ -1,3 +1,5 @@
+/* eslint-disable jsx-a11y/control-has-associated-label */
+/* eslint-disable jsx-a11y/no-static-element-interactions */
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { htmlToText } from 'html-to-text';
 import { action, computed, IReactionDisposer, makeObservable, observable, ObservableMap, reaction, runInAction } from 'mobx';
@@ -15,7 +17,7 @@ import { listSpec } from '../../../fields/Schema';
 import { Cast, NumCast, StrCast, WebCast } from '../../../fields/Types';
 import { ImageField, WebField } from '../../../fields/URLField';
 import { TraceMobx } from '../../../fields/util';
-import { emptyFunction, stringHash, Utils } from '../../../Utils';
+import { emptyFunction, stringHash } from '../../../Utils';
 import { Docs, DocUtils } from '../../documents/Documents';
 import { DocumentManager } from '../../util/DocumentManager';
 import { ScriptingGlobals } from '../../util/ScriptingGlobals';
@@ -39,8 +41,9 @@ import { FieldView, FieldViewProps, FocusViewOptions } from './FieldView';
 import { LinkInfo } from './LinkDocPreview';
 import { PresBox } from './trails';
 import './WebBox.scss';
+
 const { CreateImage } = require('./WebBoxRenderer');
-const _global = (window /* browser */ || global) /* node */ as any;
+
 @observer
 export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implements ViewBoxInterface {
     public static LayoutString(fieldKey: string) {
@@ -142,19 +145,19 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
         const scrollTop = NumCast(this.layoutDoc._layout_scrollTop);
         const nativeWidth = NumCast(this.layoutDoc.nativeWidth);
         const nativeHeight = (nativeWidth * this._props.PanelHeight()) / this._props.PanelWidth();
-        var htmlString = this._iframe.contentDocument && new XMLSerializer().serializeToString(this._iframe.contentDocument);
+        let htmlString = this._iframe.contentDocument && new XMLSerializer().serializeToString(this._iframe.contentDocument);
         if (!htmlString) {
             htmlString = await (await fetch(ClientUtils.CorsProxy(this.webField!.href))).text();
         }
         this.layoutDoc.thumb = undefined;
         this.Document.thumbLockout = true; // lock to prevent multiple thumb updates.
         CreateImage(this._webUrl.endsWith('/') ? this._webUrl.substring(0, this._webUrl.length - 1) : this._webUrl, this._iframe.contentDocument?.styleSheets ?? [], htmlString, nativeWidth, nativeHeight, scrollTop)
-            .then((data_url: any) => {
-                if (data_url.includes('<!DOCTYPE')) {
+            .then((dataUrl: any) => {
+                if (dataUrl.includes('<!DOCTYPE')) {
                     console.log('BAD DATA IN THUMB CREATION');
                     return;
                 }
-                Utils.convertDataUri(data_url, this.layoutDoc[Id] + '-icon' + new Date().getTime(), true, this.layoutDoc[Id] + '-icon').then(returnedfilename =>
+                ClientUtils.convertDataUri(dataUrl, this.layoutDoc[Id] + '-icon' + new Date().getTime(), true, this.layoutDoc[Id] + '-icon').then(returnedfilename =>
                     setTimeout(
                         action(() => {
                             this.Document.thumbLockout = false;
@@ -167,7 +170,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
                     )
                 );
             })
-            .catch(function (error: any) {
+            .catch((error: any) => {
                 console.error('oops, something went wrong!', error);
             });
     };
@@ -188,7 +191,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
         });
         this._disposers.urlchange = reaction(
             () => WebCast(this.dataDoc.data),
-            url => this.submitURL(false, false)
+            () => this.submitURL(false, false)
         );
         this._disposers.titling = reaction(
             () => StrCast(this.Document.title),
@@ -200,8 +203,8 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
 
         this._disposers.layout_autoHeight = reaction(
             () => this.layoutDoc._layout_autoHeight,
-            layout_autoHeight => {
-                if (layout_autoHeight) {
+            layoutAutoHeight => {
+                if (layoutAutoHeight) {
                     this.layoutDoc._nativeHeight = NumCast(this.Document[this._props.fieldKey + '_nativeHeight']);
                     this._props.setHeight?.(NumCast(this.Document[this._props.fieldKey + '_nativeHeight']) * (this._props.NativeDimScaling?.() || 1));
                 }
@@ -221,7 +224,9 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
             } // else it's an HTMLfield
         } else if (this.webField && !this.dataDoc.text) {
             WebRequest.get(ClientUtils.CorsProxy(this.webField.href)) //
-                .then(result => result && (this.dataDoc.text = htmlToText(result.content)));
+                .then(result => {
+                    result && (this.dataDoc.text = htmlToText(result.content));
+                });
         }
 
         this._disposers.scrollReaction = reaction(
@@ -297,21 +302,26 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
                     const focusTime = options.zoomTime ?? 500;
                     this.goTo(scrollTo, focusTime, options.easeFunc);
                     return focusTime;
-                } else {
-                    this._initialScroll = scrollTo;
                 }
+                this._initialScroll = scrollTo;
             }
         }
+        return undefined;
     };
 
     @action
-    getView = (doc: Doc, options: FocusViewOptions) => {
-        if (Doc.AreProtosEqual(doc, this.Document)) return new Promise<Opt<DocumentView>>(res => res(this.DocumentView?.()));
+    getView = (doc: Doc /* , options: FocusViewOptions */) => {
+        if (Doc.AreProtosEqual(doc, this.Document))
+            return new Promise<Opt<DocumentView>>(res => {
+                res(this.DocumentView?.());
+            });
         if (this.Document.layout_fieldKey === 'layout_icon') this.DocumentView?.().iconify();
         const webUrl = WebCast(doc.config_data)?.url;
         if (this._url && webUrl && webUrl.href !== this._url) this.setData(webUrl.href);
         if (this._sidebarRef?.current?.makeDocUnfiltered(doc) && !this.SidebarShown) this.toggleSidebar(false);
-        return new Promise<Opt<DocumentView>>(res => DocumentManager.Instance.AddViewRenderedCb(doc, dv => res(dv)));
+        return new Promise<Opt<DocumentView>>(res => {
+            DocumentManager.Instance.AddViewRenderedCb(doc, dv => res(dv));
+        });
     };
 
     sidebarAddDocTab = (doc: Doc, where: OpenWhere) => {
@@ -322,14 +332,16 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
         return this._props.addDocTab(doc, where);
     };
     getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => {
-        let ele: Opt<HTMLDivElement> = undefined;
+        let ele: Opt<HTMLDivElement>;
         try {
             const contents = this._iframe?.contentWindow?.getSelection()?.getRangeAt(0).cloneContents();
             if (contents) {
                 ele = document.createElement('div');
                 ele.append(contents);
             }
-        } catch (e) {}
+        } catch (e) {
+            /* empty */
+        }
         const visibleAnchor = this._getAnchor(this._savedAnnotations, true);
         const anchor =
             visibleAnchor ??
@@ -338,7 +350,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
                 y: NumCast(this.layoutDoc._layout_scrollTop),
                 annotationOn: this.Document,
             });
-        PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), scrollable: pinProps?.pinData ? true : false, pannable: true } }, this.Document);
+        PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), scrollable: !!pinProps?.pinData, pannable: true } }, this.Document);
         anchor.text = ele?.textContent ?? '';
         anchor.text_html = ele?.innerHTML ?? this._selectionText;
         addAsAnnotation && this.addDocumentWrapper(anchor);
@@ -395,7 +407,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
                 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.specificContextMenu();
                     this.DocumentView?.().onContextMenu(undefined, theclick[0], theclick[1]);
                 });
             }
@@ -470,6 +482,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
             const sheets = document.head.appendChild(style);
             return (sheets as any).sheet;
         }
+        return undefined;
     }
     addWebStyleSheetRule(sheet: any, selector: any, css: any, selectorPrefix = '.') {
         const propText =
@@ -484,7 +497,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
     _iframetimeout: any = undefined;
     @observable _warning = 0;
     @action
-    iframeLoaded = (e: any) => {
+    iframeLoaded = () => {
         const iframe = this._iframe;
         if (this._initialScroll !== undefined) {
             this.setScrollPos(this._initialScroll);
@@ -640,12 +653,17 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
                 this._scrollHeight = 0;
                 if (this._webUrl === this._url) {
                     this._webUrl = curUrl;
-                    setTimeout(action(() => (this._webUrl = this._url)));
+                    setTimeout(
+                        action(() => {
+                            this._webUrl = this._url;
+                        })
+                    );
                 } else {
                     this._webUrl = this._url;
                 }
                 return true;
             }
+            return undefined;
         });
         return false;
     };
@@ -663,12 +681,17 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
                 this._scrollHeight = 0;
                 if (this._webUrl === this._url) {
                     this._webUrl = curUrl;
-                    setTimeout(action(() => (this._webUrl = this._url)));
+                    setTimeout(
+                        action(() => {
+                            this._webUrl = this._url;
+                        })
+                    );
                 } else {
                     this._webUrl = this._url;
                 }
                 return true;
             }
+            return undefined;
         });
         return false;
     };
@@ -700,7 +723,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
         const html = dataTransfer.getData('text/html');
         const uri = dataTransfer.getData('text/uri-list');
         const url = uri || html || this._url || '';
-        const newurl = url.startsWith(window.location.origin) ? url.replace(window.location.origin, this._url?.match(/http[s]?:\/\/[^\/]*/)?.[0] || '') : url;
+        const newurl = url.startsWith(window.location.origin) ? url.replace(window.location.origin, this._url?.match(/http[s]?:\/\/[^/]*/)?.[0] || '') : url;
         this.setData(newurl);
         e.stopPropagation();
     };
@@ -723,19 +746,31 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
         e.stopPropagation();
     };
 
-    specificContextMenu = (e: React.MouseEvent | PointerEvent): void => {
+    specificContextMenu = (): void => {
         const cm = ContextMenu.Instance;
         const funcs: ContextMenuProps[] = [];
         if (!cm.findByDescription('Options...')) {
             !Doc.noviceMode &&
-                funcs.push({ description: (this.layoutDoc[this.fieldKey + '_useCors'] ? "Don't Use" : 'Use') + ' Cors', event: () => (this.layoutDoc[this.fieldKey + '_useCors'] = !this.layoutDoc[this.fieldKey + '_useCors']), icon: 'snowflake' });
+                funcs.push({
+                    description: (this.layoutDoc[this.fieldKey + '_useCors'] ? "Don't Use" : 'Use') + ' Cors',
+                    event: () => {
+                        this.layoutDoc[this.fieldKey + '_useCors'] = !this.layoutDoc[this.fieldKey + '_useCors'];
+                    },
+                    icon: 'snowflake',
+                });
             funcs.push({
                 description: (this.dataDoc[this.fieldKey + '_allowScripts'] ? 'Prevent' : 'Allow') + ' Scripts',
                 event: () => {
                     this.dataDoc[this.fieldKey + '_allowScripts'] = !this.dataDoc[this.fieldKey + '_allowScripts'];
                     if (this._iframe) {
-                        runInAction(() => (this._hackHide = true));
-                        setTimeout(action(() => (this._hackHide = false)));
+                        runInAction(() => {
+                            this._hackHide = true;
+                        });
+                        setTimeout(
+                            action(() => {
+                                this._hackHide = false;
+                            })
+                        );
                     }
                 },
                 icon: 'snowflake',
@@ -773,7 +808,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
             setupMoveUpEvents(
                 this,
                 e,
-                action(e => {
+                action(() => {
                     MarqueeAnnotator.clearAnnotations(this._savedAnnotations);
                     return true;
                 }),
@@ -797,7 +832,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
     @observable lighttext = false;
 
     @computed get urlContent() {
-        if (this.ScreenToLocalBoxXf().Scale > 25) return <div></div>;
+        if (this.ScreenToLocalBoxXf().Scale > 25) return <div />;
         setTimeout(
             action(() => {
                 if (this._initialScroll === undefined && !this._webPageHasBeenRendered) {
@@ -826,12 +861,15 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
         if (field instanceof WebField) {
             const url = this.layoutDoc[this.fieldKey + '_useCors'] ? ClientUtils.CorsProxy(this._webUrl) : this._webUrl;
             const scripts = this.dataDoc[this.fieldKey + '_allowScripts'] || this._webUrl.includes('wikipedia.org') || this._webUrl.includes('google.com') || this._webUrl.startsWith('https://bing');
-            //if (!scripts) console.log('No scripts for: ' + url);
+            // if (!scripts) console.log('No scripts for: ' + url);
             return (
                 <iframe
+                    title="web iframe"
                     key={this._warning}
                     className="webBox-iframe"
-                    ref={action((r: HTMLIFrameElement | null) => (this._iframe = r))}
+                    ref={action((r: HTMLIFrameElement | null) => {
+                        this._iframe = r;
+                    })}
                     style={{ pointerEvents: SnappingManager.IsResizing ? 'none' : undefined }}
                     src={url}
                     onLoad={this.iframeLoaded}
@@ -842,11 +880,23 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
                 />
             );
         }
-        return <iframe className="webBox-iframe" ref={action((r: HTMLIFrameElement | null) => (this._iframe = r))} src={'https://crossorigin.me/https://cs.brown.edu'} />;
+        return (
+            <iframe
+                title="web frame"
+                className="webBox-iframe"
+                ref={action((r: HTMLIFrameElement | null) => {
+                    this._iframe = r;
+                })}
+                src="https://crossorigin.me/https://cs.brown.edu"
+            />
+        );
     }
 
     addDocumentWrapper = (doc: Doc | Doc[], annotationKey?: string) => {
-        this._url && (doc instanceof Doc ? [doc] : doc).forEach(doc => (doc.config_data = new WebField(this._url)));
+        this._url &&
+            (doc instanceof Doc ? [doc] : doc).forEach(doc => {
+                doc.config_data = new WebField(this._url);
+            });
         return this.addDocument(doc, annotationKey);
     };
 
@@ -900,14 +950,14 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
                     backgroundColor: this.SidebarShown ? Colors.MEDIUM_BLUE : Colors.BLACK,
                 }}
                 onPointerDown={e => this.sidebarBtnDown(e, true)}>
-                <FontAwesomeIcon style={{ color: Colors.WHITE }} icon={'comment-alt'} size="sm" />
+                <FontAwesomeIcon style={{ color: Colors.WHITE }} icon="comment-alt" size="sm" />
             </div>
         );
     }
     @observable _previewNativeWidth: Opt<number> = undefined;
     @observable _previewWidth: Opt<number> = undefined;
     toggleSidebar = action((preview: boolean = false) => {
-        var nativeWidth = NumCast(this.layoutDoc[this.fieldKey + '_nativeWidth']);
+        let nativeWidth = NumCast(this.layoutDoc[this.fieldKey + '_nativeWidth']);
         if (!nativeWidth) {
             const defaultNativeWidth = NumCast(this.Document.nativeWidth, this.dataDoc[this.fieldKey] instanceof WebField ? 850 : NumCast(this.Document._width));
             Doc.SetNativeWidth(this.dataDoc, Doc.NativeWidth(this.dataDoc) || defaultNativeWidth);
@@ -946,7 +996,9 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
     };
     _innerCollectionView: CollectionFreeFormView | undefined;
     zoomScaling = () => this._innerCollectionView?.zoomScaling() ?? 1;
-    setInnerContent = (component: ViewBoxInterface) => (this._innerCollectionView = component as CollectionFreeFormView);
+    setInnerContent = (component: ViewBoxInterface) => {
+        this._innerCollectionView = component as CollectionFreeFormView;
+    };
 
     @computed get content() {
         const interactive = this._props.isContentActive() && this._props.pointerEvents?.() !== 'none' && Doc.ActiveTool === InkTool.None;
@@ -977,24 +1029,26 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
                 {this.inlineTextAnnotations
                     .sort((a, b) => NumCast(a.y) - NumCast(b.y))
                     .map(anno => (
+                        // eslint-disable-next-line react/jsx-props-no-spreading
                         <Annotation {...this._props} fieldKey={this.annotationKey} pointerEvents={this.pointerEvents} dataDoc={this.dataDoc} anno={anno} key={`${anno[Id]}-annotation`} />
                     ))}
             </div>
         );
     }
     @computed get SidebarShown() {
-        return this._showSidebar || this.layoutDoc._layout_showSidebar ? true : false;
+        return !!(this._showSidebar || this.layoutDoc._layout_showSidebar);
     }
     renderAnnotations = (childFilters: () => string[]) => (
         <CollectionFreeFormView
+            // eslint-disable-next-line react/jsx-props-no-spreading
             {...this._props}
             setContentViewBox={this.setInnerContent}
             NativeWidth={returnZero}
             NativeHeight={returnZero}
             originTopLeft={false}
-            isAnnotationOverlayScrollable={true}
+            isAnnotationOverlayScrollable
             renderDepth={this._props.renderDepth + 1}
-            isAnnotationOverlay={true}
+            isAnnotationOverlay
             fieldKey={this.annotationKey}
             setPreviewCursor={this.setPreviewCursor}
             PanelWidth={this.panelWidth}
@@ -1037,7 +1091,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
                 }}
                 // when active, block wheel events from propagating since they're handled by the iframe
                 onWheel={this.onZoomWheel}
-                onScroll={e => this.setDashScrollTop(this._outerRef.current?.scrollTop || 0)}
+                onScroll={() => this.setDashScrollTop(this._outerRef.current?.scrollTop || 0)}
                 onPointerDown={this.onMarqueeDown}>
                 <div className="webBox-innerContent" style={{ height: (this._webPageHasBeenRendered && this._scrollHeight > this._props.PanelHeight() && this._scrollHeight) || '100%', pointerEvents }}>
                     {this.content}
@@ -1053,7 +1107,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
         return (
             <div className="webBox-ui" onPointerDown={e => e.stopPropagation()} style={{ display: this._props.isContentActive() ? 'flex' : 'none' }}>
                 <div className="webBox-overlayCont" onPointerDown={e => e.stopPropagation()} style={{ left: `${this._searching ? 0 : 100}%` }}>
-                    <button className="webBox-overlayButton" title={'search'} />
+                    <button type="button" className="webBox-overlayButton" title="search" />
                     <input
                         className="webBox-searchBar"
                         placeholder="Search"
@@ -1064,13 +1118,14 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
                             e.stopPropagation();
                         }}
                     />
-                    <button className="webBox-search" title="Search" onClick={e => this.search(this._searchString, e.shiftKey)}>
+                    <button type="button" className="webBox-search" title="Search" onClick={e => this.search(this._searchString, e.shiftKey)}>
                         <FontAwesomeIcon icon="search" size="sm" />
                     </button>
                 </div>
                 <button
+                    type="button"
                     className="webBox-overlayButton"
-                    title={'search'}
+                    title="search"
                     onClick={action(() => {
                         this._searching = !this._searching;
                         this.search('', false, true);
@@ -1083,8 +1138,12 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
             </div>
         );
     }
-    searchStringChanged = (e: React.ChangeEvent<HTMLInputElement>) => (this._searchString = e.currentTarget.value);
-    setPreviewCursor = (func?: (x: number, y: number, drag: boolean, hide: boolean, doc: Opt<Doc>) => void) => (this._setPreviewCursor = func);
+    searchStringChanged = (e: React.ChangeEvent<HTMLInputElement>) => {
+        this._searchString = e.currentTarget.value;
+    };
+    setPreviewCursor = (func?: (x: number, y: number, drag: boolean, hide: boolean, doc: Opt<Doc>) => void) => {
+        this._setPreviewCursor = func;
+    };
     panelWidth = () => this._props.PanelWidth() / (this._props.NativeDimScaling?.() || 1) - this.sidebarWidth() + WebBox.sidebarResizerWidth;
     panelHeight = () => this._props.PanelHeight() / (this._props.NativeDimScaling?.() || 1);
     scrollXf = () => this.ScreenToLocalBoxXf().translate(0, NumCast(this.layoutDoc._layout_scrollTop));
@@ -1157,6 +1216,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
                 <div style={{ position: 'absolute', height: '100%', right: 0, top: 0, width: `calc(100 * ${this.sidebarWidth() / this._props.PanelWidth()}%` }}>
                     <SidebarAnnos
                         ref={this._sidebarRef}
+                        // eslint-disable-next-line react/jsx-props-no-spreading
                         {...this._props}
                         whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
                         fieldKey={this.fieldKey + '_' + this._urlHash}
@@ -1177,6 +1237,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
         );
     }
 }
+// eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function urlHash(url: string) {
     return stringHash(url);
 });
diff --git a/src/client/views/nodes/formattedText/DashFieldView.tsx b/src/client/views/nodes/formattedText/DashFieldView.tsx
index eaa8fffaa..dc388b22a 100644
--- a/src/client/views/nodes/formattedText/DashFieldView.tsx
+++ b/src/client/views/nodes/formattedText/DashFieldView.tsx
@@ -1,3 +1,6 @@
+/* eslint-disable jsx-a11y/no-static-element-interactions */
+/* eslint-disable jsx-a11y/click-events-have-key-events */
+/* eslint-disable jsx-a11y/control-has-associated-label */
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { Tooltip } from '@mui/material';
 import { action, computed, IReactionDisposer, makeObservable, observable, reaction, runInAction } from 'mobx';
@@ -24,90 +27,69 @@ import { OpenWhere } from '../DocumentView';
 import './DashFieldView.scss';
 import { FormattedTextBox } from './FormattedTextBox';
 
-export class DashFieldView {
-    dom: HTMLDivElement; // container for label and value
-    root: any;
-    node: any;
-    tbox: FormattedTextBox;
-    getpos: any;
-    @observable _nodeSelected = false;
-    NodeSelected = () => this._nodeSelected;
+@observer
+export class DashFieldViewMenu extends AntimodeMenu<AntimodeMenuProps> {
+    // eslint-disable-next-line no-use-before-define
+    static Instance: DashFieldViewMenu;
+    static createFieldView: (e: React.MouseEvent) => void = emptyFunction;
+    static toggleFieldHide: () => void = emptyFunction;
+    static toggleValueHide: () => void = emptyFunction;
+    constructor(props: any) {
+        super(props);
+        DashFieldViewMenu.Instance = this;
+    }
 
-    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.getpos = getPos;
-        this.dom = document.createElement('div');
-        this.dom.style.width = node.attrs.width;
-        this.dom.style.height = node.attrs.height;
-        this.dom.style.position = 'relative';
-        this.dom.style.display = 'inline-block';
-        const tBox = this.tbox;
-        this.dom.onkeypress = function (e: KeyboardEvent) {
-            e.stopPropagation();
-        };
-        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 = self.getpos() + 1; 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);
-                }
-            }
-        };
-        this.dom.onkeyup = function (e: any) {
-            e.stopPropagation();
-        };
-        this.dom.onmousedown = function (e: any) {
-            e.stopPropagation();
-        };
+    showFields = (e: React.MouseEvent) => {
+        DashFieldViewMenu.createFieldView(e);
+        DashFieldViewMenu.Instance.fadeOut(true);
+    };
+    toggleFieldHide = () => {
+        DashFieldViewMenu.toggleFieldHide();
+        DashFieldViewMenu.Instance.fadeOut(true);
+    };
+    toggleValueHide = () => {
+        DashFieldViewMenu.toggleValueHide();
+        DashFieldViewMenu.Instance.fadeOut(true);
+    };
 
-        this.root = ReactDOM.createRoot(this.dom);
-        this.root.render(
-            <DashFieldViewInternal
-                node={node}
-                unclickable={this.unclickable}
-                getPos={getPos}
-                fieldKey={node.attrs.fieldKey}
-                docId={node.attrs.docId}
-                width={node.attrs.width}
-                height={node.attrs.height}
-                hideKey={node.attrs.hideKey}
-                hideValue={node.attrs.hideValue}
-                editable={node.attrs.editable}
-                nodeSelected={this.NodeSelected}
-                tbox={tbox}
-            />
+    @observable _fieldKey = '';
+
+    @action
+    public show = (x: number, y: number, fieldKey: string) => {
+        this._fieldKey = fieldKey;
+        this.jumpTo(x, y, true);
+        const hideMenu = () => {
+            this.fadeOut(true);
+            document.removeEventListener('pointerdown', hideMenu, true);
+        };
+        document.addEventListener('pointerdown', hideMenu, true);
+    };
+    render() {
+        return this.getElement(
+            <>
+                <Tooltip key="trash" title={<div className="dash-tooltip">{`Show Pivot Viewer for '${this._fieldKey}'`}</div>}>
+                    <button type="button" className="antimodeMenu-button" onPointerDown={this.showFields}>
+                        <FontAwesomeIcon icon="eye" size="sm" />
+                    </button>
+                </Tooltip>
+                {this._fieldKey.startsWith('#') ? null : (
+                    <Tooltip key="key" title={<div className="dash-tooltip">Toggle view of field key</div>}>
+                        <button type="button" className="antimodeMenu-button" onPointerDown={this.toggleFieldHide}>
+                            <FontAwesomeIcon icon="bullseye" size="sm" />
+                        </button>
+                    </Tooltip>
+                )}
+                {this._fieldKey.startsWith('#') ? null : (
+                    <Tooltip key="val" title={<div className="dash-tooltip">Toggle view of field value</div>}>
+                        <button type="button" className="antimodeMenu-button" onPointerDown={this.toggleValueHide}>
+                            <FontAwesomeIcon icon="hashtag" size="sm" />
+                        </button>
+                    </Tooltip>
+                )}
+            </>
         );
     }
-    destroy() {
-        setTimeout(() => {
-            try {
-                this.root.unmount();
-            } catch {}
-        });
-    }
-    deselectNode() {
-        runInAction(() => (this._nodeSelected = false));
-        this.dom.classList.remove('ProseMirror-selectednode');
-    }
-    selectNode() {
-        setTimeout(() => runInAction(() => (this._nodeSelected = true)), 100);
-        this.dom.classList.add('ProseMirror-selectednode');
-    }
 }
-
 interface IDashFieldViewInternal {
     fieldKey: string;
     docId: string;
@@ -137,7 +119,9 @@ export class DashFieldViewInternal extends ObservableReactComponent<IDashFieldVi
         makeObservable(this);
         this._fieldKey = this._props.fieldKey;
         this._textBoxDoc = this._props.tbox.Document;
-        const setDoc = action((doc: Doc) => (this._dashDoc = doc));
+        const setDoc = action((doc: Doc) => {
+            this._dashDoc = doc;
+        });
 
         if (this._props.docId) {
             DocServer.GetRefField(this._props.docId).then(dashDoc => dashDoc instanceof Doc && setDoc(dashDoc));
@@ -172,7 +156,11 @@ export class DashFieldViewInternal extends ObservableReactComponent<IDashFieldVi
     // set the display of the field's value (checkbox for booleans, span of text for strings)
     @computed get fieldValueContent() {
         return !this._dashDoc ? null : (
-            <div onClick={action(e => (this._expanded = !this._props.editable ? !this._expanded : true))} style={{ fontSize: 'smaller', width: !this._hideKey && this._expanded ? this.columnWidth() : undefined }}>
+            <div
+                onClick={action(() => {
+                    this._expanded = !this._props.editable ? !this._expanded : true;
+                })}
+                style={{ fontSize: 'smaller', width: !this._hideKey && this._expanded ? this.columnWidth() : undefined }}>
                 <SchemaTableCell
                     Document={this._dashDoc}
                     col={0}
@@ -187,20 +175,20 @@ export class DashFieldViewInternal extends ObservableReactComponent<IDashFieldVi
                     padding={0}
                     getFinfo={emptyFunction}
                     setColumnValues={returnFalse}
-                    allowCRs={true}
+                    allowCRs
                     oneLine={!this._expanded && !this._props.nodeSelected()}
                     finishEdit={this.finishEdit}
                     transform={Transform.Identity}
                     menuTarget={null}
-                    autoFocus={true}
+                    autoFocus
                     rootSelected={this._props.tbox._props.rootSelected}
                 />
             </div>
         );
     }
 
-    createPivotForField = (e: React.MouseEvent) => {
-        let container = this._props.tbox.DocumentView?.().containerViewPath?.().lastElement();
+    createPivotForField = () => {
+        const container = this._props.tbox.DocumentView?.().containerViewPath?.().lastElement();
         if (container) {
             const embedding = Doc.MakeEmbedding(container.Document);
             embedding._type_collection = CollectionViewType.Time;
@@ -219,7 +207,7 @@ export class DashFieldViewInternal extends ObservableReactComponent<IDashFieldVi
     toggleFieldHide = undoable(
         action(() => {
             const editor = this._props.tbox.EditorView!;
-            editor.dispatch(editor.state.tr.setNodeMarkup(this._props.getPos(), this._props.node.type, { ...this._props.node.attrs, hideKey: this._props.node.attrs.hideValue ? false : !this._props.node.attrs.hideKey ? true : false }));
+            editor.dispatch(editor.state.tr.setNodeMarkup(this._props.getPos(), this._props.node.type, { ...this._props.node.attrs, hideKey: this._props.node.attrs.hideValue ? false : !this._props.node.attrs.hideKey }));
         }),
         'hideKey'
     );
@@ -227,7 +215,7 @@ export class DashFieldViewInternal extends ObservableReactComponent<IDashFieldVi
     toggleValueHide = undoable(
         action(() => {
             const editor = this._props.tbox.EditorView!;
-            editor.dispatch(editor.state.tr.setNodeMarkup(this._props.getPos(), this._props.node.type, { ...this._props.node.attrs, hideValue: this._props.node.attrs.hideKey ? false : !this._props.node.attrs.hideValue ? true : false }));
+            editor.dispatch(editor.state.tr.setNodeMarkup(this._props.getPos(), this._props.node.type, { ...this._props.node.attrs, hideValue: this._props.node.attrs.hideKey ? false : !this._props.node.attrs.hideValue }));
         }),
         'hideValue'
     );
@@ -243,11 +231,11 @@ export class DashFieldViewInternal extends ObservableReactComponent<IDashFieldVi
     // clicking on the label creates a pivot view collection of all documents
     // in the same collection.  The pivot field is the fieldKey of this label
     onPointerDownLabelSpan = (e: React.PointerEvent) => {
-        setupMoveUpEvents(this, e, returnFalse, returnFalse, e => {
+        setupMoveUpEvents(this, e, returnFalse, returnFalse, moveEv => {
             DashFieldViewMenu.createFieldView = this.createPivotForField;
             DashFieldViewMenu.toggleFieldHide = this.toggleFieldHide;
             DashFieldViewMenu.toggleValueHide = this.toggleValueHide;
-            DashFieldViewMenu.Instance.show(e.clientX, e.clientY + 16, this._fieldKey);
+            DashFieldViewMenu.Instance.show(moveEv.clientX, moveEv.clientY + 16, this._fieldKey);
             const editor = this._props.tbox.EditorView!;
             setTimeout(() => editor.dispatch(editor.state.tr.setSelection(new NodeSelection(editor.state.doc.resolve(this._props.getPos())))), 100);
         });
@@ -277,7 +265,7 @@ export class DashFieldViewInternal extends ObservableReactComponent<IDashFieldVi
                 }}>
                 {this._hideKey ? null : (
                     <span className="dashFieldView-labelSpan" title="click to see related tags" onPointerDown={this.onPointerDownLabelSpan}>
-                        {(Doc.AreProtosEqual(DocCast(this._textBoxDoc.rootDocument) ?? this._textBoxDoc, DocCast(this._dashDoc?.rootDocument) ?? this._dashDoc) ? '' : this._dashDoc?.title + ':') + this._fieldKey}
+                        {(Doc.AreProtosEqual(DocCast(this._textBoxDoc.rootDocument) ?? this._textBoxDoc, DocCast(this._dashDoc?.rootDocument) ?? this._dashDoc) ? '' : (this._dashDoc?.title ?? '') + ':') + this._fieldKey}
                     </span>
                 )}
                 {this._props.fieldKey.startsWith('#') || this._hideValue ? null : this.fieldValueContent}
@@ -293,65 +281,93 @@ export class DashFieldViewInternal extends ObservableReactComponent<IDashFieldVi
         );
     }
 }
-@observer
-export class DashFieldViewMenu extends AntimodeMenu<AntimodeMenuProps> {
-    static Instance: DashFieldViewMenu;
-    static createFieldView: (e: React.MouseEvent) => void = emptyFunction;
-    static toggleFieldHide: () => void = emptyFunction;
-    static toggleValueHide: () => void = emptyFunction;
-    constructor(props: any) {
-        super(props);
-        DashFieldViewMenu.Instance = this;
-    }
-
-    showFields = (e: React.MouseEvent) => {
-        DashFieldViewMenu.createFieldView(e);
-        DashFieldViewMenu.Instance.fadeOut(true);
-    };
-    toggleFieldHide = (e: React.MouseEvent) => {
-        DashFieldViewMenu.toggleFieldHide();
-        DashFieldViewMenu.Instance.fadeOut(true);
-    };
-    toggleValueHide = (e: React.MouseEvent) => {
-        DashFieldViewMenu.toggleValueHide();
-        DashFieldViewMenu.Instance.fadeOut(true);
-    };
-
-    @observable _fieldKey = '';
+export class DashFieldView {
+    dom: HTMLDivElement; // container for label and value
+    root: any;
+    node: any;
+    tbox: FormattedTextBox;
+    getpos: any;
+    @observable _nodeSelected = false;
+    NodeSelected = () => this._nodeSelected;
 
-    @action
-    public show = (x: number, y: number, fieldKey: string) => {
-        this._fieldKey = fieldKey;
-        this.jumpTo(x, y, true);
-        const hideMenu = () => {
-            this.fadeOut(true);
-            document.removeEventListener('pointerdown', hideMenu, true);
+    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.getpos = getPos;
+        this.dom = document.createElement('div');
+        this.dom.style.width = node.attrs.width;
+        this.dom.style.height = node.attrs.height;
+        this.dom.style.position = 'relative';
+        this.dom.style.display = 'inline-block';
+        this.dom.onkeypress = function (e: KeyboardEvent) {
+            e.stopPropagation();
         };
-        document.addEventListener('pointerdown', hideMenu, true);
-    };
-    render() {
-        return this.getElement(
-            <>
-                <Tooltip key="trash" title={<div className="dash-tooltip">{`Show Pivot Viewer for '${this._fieldKey}'`}</div>}>
-                    <button className="antimodeMenu-button" onPointerDown={this.showFields}>
-                        <FontAwesomeIcon icon="eye" size="sm" />
-                    </button>
-                </Tooltip>
-                {this._fieldKey.startsWith('#') ? null : (
-                    <Tooltip key="key" title={<div className="dash-tooltip">Toggle view of field key</div>}>
-                        <button className="antimodeMenu-button" onPointerDown={this.toggleFieldHide}>
-                            <FontAwesomeIcon icon="bullseye" size="sm" />
-                        </button>
-                    </Tooltip>
-                )}
-                {this._fieldKey.startsWith('#') ? null : (
-                    <Tooltip key="val" title={<div className="dash-tooltip">Toggle view of field value</div>}>
-                        <button className="antimodeMenu-button" onPointerDown={this.toggleValueHide}>
-                            <FontAwesomeIcon icon="hashtag" size="sm" />
-                        </button>
-                    </Tooltip>
-                )}
-            </>
+        this.dom.onkeydown = function (e: KeyboardEvent) {
+            e.stopPropagation();
+            if (e.key === 'Tab') {
+                e.preventDefault();
+                const editor = tbox.EditorView;
+                if (editor) {
+                    const { state } = editor;
+                    for (let i = self.getpos() + 1; 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;
+                        }
+                    }
+                }
+            }
+        };
+        this.dom.onkeyup = function (e: any) {
+            e.stopPropagation();
+        };
+        this.dom.onmousedown = function (e: any) {
+            e.stopPropagation();
+        };
+
+        this.root = ReactDOM.createRoot(this.dom);
+        this.root.render(
+            <DashFieldViewInternal
+                node={node}
+                unclickable={this.unclickable}
+                getPos={getPos}
+                fieldKey={node.attrs.fieldKey}
+                docId={node.attrs.docId}
+                width={node.attrs.width}
+                height={node.attrs.height}
+                hideKey={node.attrs.hideKey}
+                hideValue={node.attrs.hideValue}
+                editable={node.attrs.editable}
+                nodeSelected={this.NodeSelected}
+                tbox={tbox}
+            />
         );
     }
+    destroy() {
+        setTimeout(() => {
+            try {
+                this.root.unmount();
+            } catch {
+                /* empty */
+            }
+        });
+    }
+    deselectNode() {
+        runInAction(() => {
+            this._nodeSelected = false;
+        });
+        this.dom.classList.remove('ProseMirror-selectednode');
+    }
+    selectNode() {
+        setTimeout(
+            action(() => {
+                this._nodeSelected = true;
+            }),
+            100
+        );
+        this.dom.classList.add('ProseMirror-selectednode');
+    }
 }
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.scss b/src/client/views/nodes/formattedText/FormattedTextBox.scss
index 5b2b558fc..3a1a72910 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.scss
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.scss
@@ -350,7 +350,8 @@ footnote::before {
     span {
         font-family: inherit;
         background-color: inherit;
-        display: inline; // needs to be inline for search highlighting to appear // contents; // fixes problem where extra space is added around <ol> lists when inside a prosemirror span
+        display: inline; // needs to be inline for search highlighting to appear
+        // display: contents; // BUT needs to be 'contents' to avoid Chrome bug where extra space is added above and <ol> lists when inside a prosemirror span
     }
 
     blockquote {
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index a82f025f9..99a2f4ab9 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -1531,7 +1531,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
             ...(Doc.UserDoc().fontColor !== 'transparent' && Doc.UserDoc().fontColor ? [schema.mark(schema.marks.pFontColor, { color: StrCast(Doc.UserDoc().fontColor) })] : []),
             ...(Doc.UserDoc().fontStyle === 'italics' ? [schema.mark(schema.marks.em)] : []),
             ...(Doc.UserDoc().textDecoration === 'underline' ? [schema.mark(schema.marks.underline)] : []),
-            ...(Doc.UserDoc().fontFamily ? [schema.mark(schema.marks.pFontFamily, { family: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.FontFamily) })] : []),
+            ...(Doc.UserDoc().fontFamily ? [schema.mark(schema.marks.pFontFamily, { fontFamily: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.FontFamily) })] : []),
             ...(Doc.UserDoc().fontSize ? [schema.mark(schema.marks.pFontSize, { fontSize: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.FontSize) })] : []),
             ...(Doc.UserDoc().fontWeight === 'bold' ? [schema.mark(schema.marks.strong)] : []),
             ...[schema.marks.user_mark.create({ userid: ClientUtils.CurrentUserEmail(), modified: Math.floor(Date.now() / 1000) })],
diff --git a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts
index c3a5a2c86..073ed91c3 100644
--- a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts
+++ b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts
@@ -23,7 +23,7 @@ export const updateBullets = (tx2: Transaction, schema: Schema, assignedMapStyle
     tx2.doc.descendants((node: any, offset: any /* , index: any */) => {
         if ((from === undefined || to === undefined || (from <= offset + node.nodeSize && to >= offset)) && (node.type === schema.nodes.ordered_list || node.type === schema.nodes.list_item)) {
             const { path } = tx2.doc.resolve(offset) as any;
-            let depth = Array.from(path).reduce((p: number, c: any) => p + (c.hasOwnProperty('type') && c.type === schema.nodes.ordered_list ? 1 : 0), 0);
+            let depth = Array.from(path).reduce((p: number, c: any) => p + (c.type === schema.nodes.ordered_list ? 1 : 0), 0);
             if (node.type === schema.nodes.ordered_list) {
                 if (depth === 0 && !assignedMapStyle) mapStyle = node.attrs.mapStyle;
                 depth++;
@@ -34,18 +34,19 @@ export const updateBullets = (tx2: Transaction, schema: Schema, assignedMapStyle
     return tx2;
 };
 
-export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKeys?: KeyMap): KeyMap {
+export function buildKeymap<S extends Schema<any>>(schema: S, props: any): KeyMap {
     const keys: { [key: string]: any } = {};
 
     function bind(key: string, cmd: any) {
-        if (mapKeys) {
-            const mapped = mapKeys[key];
-            if (mapped === false) return;
-            if (mapped) key = mapped;
-        }
         keys[key] = cmd;
     }
 
+    function onKey(): boolean | undefined {
+        // bcz: this is pretty hacky -- prosemirror doesn't send us the keyboard event, but the 'event' variable is in scope.. so we access it anyway
+        // eslint-disable-next-line no-restricted-globals
+        return props.onKey?.(event, props);
+    }
+
     const canEdit = (state: any) => {
         switch (GetEffectiveAcl(props.TemplateDataDocument)) {
             case AclAugment:
@@ -84,12 +85,12 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey
     // Commands for lists
     bind('Ctrl-i', (state: EditorState, dispatch: (tx: Transaction) => void) => canEdit(state) && wrapInList(schema.nodes.ordered_list)(state as any, dispatch as any));
 
-    bind('Ctrl-Tab', () => (props.onKey?.(event, props) ? true : true));
-    bind('Alt-Tab', () => (props.onKey?.(event, props) ? true : true));
-    bind('Meta-Tab', () => (props.onKey?.(event, props) ? true : true));
-    bind('Meta-Enter', () => (props.onKey?.(event, props) ? true : true));
+    bind('Ctrl-Tab', () => onKey() || true);
+    bind('Alt-Tab', () => onKey() || true);
+    bind('Meta-Tab', () => onKey() || true);
+    bind('Meta-Enter', () => onKey() || true);
     bind('Tab', (state: EditorState, dispatch: (tx: Transaction) => void) => {
-        if (props.onKey?.(event, props)) return true;
+        if (onKey()) return true;
         if (!canEdit(state)) return true;
         const ref = state.selection;
         const range = ref.$from.blockRange(ref.$to);
@@ -119,10 +120,11 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey
                 console.log('bullet promote fail');
             }
         }
+        return undefined;
     });
 
     bind('Shift-Tab', (state: EditorState, dispatch: (tx: Transaction) => void) => {
-        if (props.onKey?.(event, props)) return true;
+        if (onKey()) return true;
         if (!canEdit(state)) return true;
         const marks = state.storedMarks || (state.selection.$to.parentOffset && state.selection.$from.marks());
 
@@ -136,10 +138,11 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey
         ) {
             console.log('bullet demote fail');
         }
+        return undefined;
     });
 
     // Command to create a new Tab with a PDF of all the command shortcuts
-    bind('Mod-/', (state: EditorState, dispatch: (tx: Transaction) => void) => {
+    bind('Mod-/', () => {
         const newDoc = Docs.Create.PdfDocument(ClientUtils.prepend('/assets/cheat-sheet.pdf'), { _width: 300, _height: 300 });
         props.addDocTab(newDoc, OpenWhere.addRight);
     });
@@ -171,13 +174,13 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey
         SelectionManager.DeselectAll();
     });
 
-    bind('Alt-Enter', () => (props.onKey?.(event, props) ? true : true));
-    bind('Ctrl-Enter', () => (props.onKey?.(event, props) ? true : true));
+    bind('Alt-Enter', () => onKey() || true);
+    bind('Ctrl-Enter', () => onKey() || true);
     bind('Cmd-a', (state: EditorState, dispatch: (tx: Transaction) => void) => {
         dispatch(state.tr.setSelection(new TextSelection(state.doc.resolve(1), state.doc.resolve(state.doc.content.size - 1))));
         return true;
     });
-    bind('Cmd-?', (state: EditorState, dispatch: (tx: Transaction) => void) => {
+    bind('Cmd-?', () => {
         RTFMarkup.Instance.open();
         return true;
     });
@@ -200,7 +203,7 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey
             const node = resolved.nodeAfter;
             const sm = state.storedMarks || undefined;
             if (node) {
-                tr.replaceRangeWith(state.selection.from, state.selection.from, schema.nodes.paragraph.create({ align: 'right' })).setStoredMarks([...node.marks, ...(sm ? sm : [])]);
+                tr.replaceRangeWith(state.selection.from, state.selection.from, schema.nodes.paragraph.create({ align: 'right' })).setStoredMarks([...node.marks, ...(sm || [])]);
             }
         }
         dispatch(tr);
@@ -215,7 +218,7 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey
             const node = resolved.nodeAfter;
             const sm = state.storedMarks || undefined;
             if (node) {
-                tr.replaceRangeWith(state.selection.from, state.selection.from, schema.nodes.paragraph.create({ align: 'center' })).setStoredMarks([...node.marks, ...(sm ? sm : [])]);
+                tr.replaceRangeWith(state.selection.from, state.selection.from, schema.nodes.paragraph.create({ align: 'center' })).setStoredMarks([...node.marks, ...(sm || [])]);
             }
         }
         dispatch(tr);
@@ -230,7 +233,7 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey
             const node = resolved.nodeAfter;
             const sm = state.storedMarks || undefined;
             if (node) {
-                tr.replaceRangeWith(state.selection.from, state.selection.from, schema.nodes.paragraph.create({ align: 'left' })).setStoredMarks([...node.marks, ...(sm ? sm : [])]);
+                tr.replaceRangeWith(state.selection.from, state.selection.from, schema.nodes.paragraph.create({ align: 'left' })).setStoredMarks([...node.marks, ...(sm || [])]);
             }
         }
         dispatch(tr);
@@ -262,7 +265,7 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey
 
     // backspace = chainCommands(deleteSelection, joinBackward, selectNodeBackward);
     const backspace = (state: EditorState, dispatch: (tx: Transaction) => void, view: EditorView) => {
-        if (props.onKey?.(event, props)) return true;
+        if (onKey()) return true;
         if (!canEdit(state)) return true;
 
         if (
@@ -296,7 +299,7 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey
     // command to break line
 
     const enter = (state: EditorState, dispatch: (tx: Transaction) => void, view: EditorView, once = true) => {
-        if (props.onKey?.(event, props)) return true;
+        if (onKey()) return true;
         if (!canEdit(state)) return true;
 
         const trange = state.selection.$from.blockRange(state.selection.$to);
@@ -361,8 +364,8 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey
     bind('Enter', enter);
 
     // Command to create a blank space
-    bind('Space', (state: EditorState, dispatch: (tx: Transaction) => void) => {
-        if (props.TemplateDataDocument && GetEffectiveAcl(props.TemplateDataDocument) != AclEdit && GetEffectiveAcl(props.TemplateDataDocument) != AclAugment && GetEffectiveAcl(props.TemplateDataDocument) != AclAdmin) return true;
+    bind('Space', () => {
+        if (props.TemplateDataDocument && ![AclAdmin, AclAugment, AclEdit].includes(GetEffectiveAcl(props.TemplateDataDocument))) return true;
         return false;
     });
 
diff --git a/src/client/views/nodes/formattedText/RichTextMenu.tsx b/src/client/views/nodes/formattedText/RichTextMenu.tsx
index ec9c1a15d..6108383c2 100644
--- a/src/client/views/nodes/formattedText/RichTextMenu.tsx
+++ b/src/client/views/nodes/formattedText/RichTextMenu.tsx
@@ -23,10 +23,12 @@ import { FormattedTextBox } from './FormattedTextBox';
 import { updateBullets } from './ProsemirrorExampleTransfer';
 import './RichTextMenu.scss';
 import { schema } from './schema_rts';
+
 const { toggleMark } = require('prosemirror-commands');
 
 @observer
 export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
+    // eslint-disable-next-line no-use-before-define
     static _instance: { menu: RichTextMenu | undefined } = observable({ menu: undefined });
     static get Instance() {
         return RichTextMenu._instance?.menu;
@@ -116,7 +118,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
     componentDidMount() {
         this._disposer = reaction(
             () => SelectionManager.Views.slice(),
-            views => this.updateMenu(undefined, undefined, undefined, undefined)
+            () => this.updateMenu(undefined, undefined, undefined, undefined)
         );
     }
     componentWillUnmount() {
@@ -139,10 +141,10 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
 
         this.setActiveMarkButtons(this.getActiveMarksOnSelection());
         const active = this.getActiveFontStylesOnSelection();
-        const activeFamilies = active.activeFamilies;
-        const activeSizes = active.activeSizes;
-        const activeColors = active.activeColors;
-        const activeHighlights = active.activeHighlights;
+        const { activeFamilies } = active;
+        const { activeSizes } = active;
+        const { activeColors } = active;
+        const { activeHighlights } = active;
         const refDoc = SelectionManager.Views.lastElement()?.layoutDoc ?? Doc.UserDoc();
         const refField = (pfx => (pfx ? pfx + '_' : ''))(SelectionManager.Views.lastElement()?.LayoutFieldKey);
 
@@ -174,8 +176,8 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
                 dispatch(updateBullets(markup, state.schema));
             } else {
                 const state = this.view?.state;
-                const tr = this.view?.state.tr;
-                if (tr && state) {
+                if (state) {
+                    const { tr } = state;
                     if (dontToggle) {
                         tr.addMark(state.selection.from, state.selection.to, mark);
                         dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(state.selection.from), tr.doc.resolve(state.selection.to)))); // bcz: need to redo the selection because ctrl-a selections disappear otherwise
@@ -190,7 +192,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
     // finds font sizes and families in selection
     getActiveAlignment() {
         if (this.view && this.TextView?._props.rootSelected?.()) {
-            const path = (this.view.state.selection.$from as any).path;
+            const { path } = this.view.state.selection.$from as any;
             for (let i = path.length - 3; i < path.length && i >= 0; i -= 3) {
                 if (path[i]?.type === this.view.state.schema.nodes.paragraph || path[i]?.type === this.view.state.schema.nodes.heading) {
                     return path[i].attrs.align || 'left';
@@ -222,24 +224,25 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
         const activeColors = new Set<string>();
         const activeHighlights = new Set<string>();
         if (this.view && this.TextView?._props.rootSelected?.()) {
-            const state = this.view.state;
+            const { state } = this.view;
             const pos = this.view.state.selection.$from;
-            var marks: Mark[] = [...(state.storedMarks ?? [])];
+            let marks: Mark[] = [...(state.storedMarks ?? [])];
             if (state.storedMarks !== null) {
+                /* empty */
             } else if (state.selection.empty) {
                 for (let i = 0; i <= pos.depth; i++) {
                     marks = [...Array.from(pos.node(i).marks), ...this.view.state.selection.$anchor.marks(), ...marks];
                 }
             } else {
-                state.doc.nodesBetween(state.selection.from, state.selection.to, (node, pos, parent, index) => {
+                state.doc.nodesBetween(state.selection.from, state.selection.to, (node /* , pos, parent, index */) => {
                     node.marks?.filter(mark => !mark.isInSet(marks)).map(mark => marks.push(mark));
                 });
             }
             marks.forEach(m => {
-                m.type === state.schema.marks.pFontFamily && activeFamilies.add(m.attrs.family);
-                m.type === state.schema.marks.pFontColor && activeColors.add(m.attrs.color);
+                m.type === state.schema.marks.pFontFamily && activeFamilies.add(m.attrs.fontFamily);
+                m.type === state.schema.marks.pFontColor && activeColors.add(m.attrs.fontColor);
                 m.type === state.schema.marks.pFontSize && activeSizes.add(m.attrs.fontSize);
-                m.type === state.schema.marks.marker && activeHighlights.add(String(m.attrs.highlight));
+                m.type === state.schema.marks.pFontHighlight && activeHighlights.add(String(m.attrs.fontHigh));
             });
         } else if (SelectionManager.Views.some(dv => dv.ComponentView instanceof EquationBox)) {
             SelectionManager.Views.forEach(dv => StrCast(dv.Document._text_fontSize) && activeSizes.add(StrCast(dv.Document._text_fontSize)));
@@ -254,26 +257,27 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
         return found;
     }
 
-    //finds all active marks on selection in given group
+    // finds all active marks on selection in given group
     getActiveMarksOnSelection() {
         if (!this.view || !this.TextView?._props.rootSelected?.()) return [] as MarkType[];
 
-        const state = this.view.state;
-        var marks: Mark[] = [...(state.storedMarks ?? [])];
+        const { state } = this.view;
+        let marks: Mark[] = [...(state.storedMarks ?? [])];
         const pos = this.view.state.selection.$from;
         if (state.storedMarks !== null) {
+            /* empty */
         } else if (state.selection.empty) {
             for (let i = 0; i <= pos.depth; i++) {
                 marks = [...Array.from(pos.node(i).marks), ...this.view.state.selection.$anchor.marks(), ...marks];
             }
         } else {
-            state.doc.nodesBetween(state.selection.from, state.selection.to, (node, pos, parent, index) => {
+            state.doc.nodesBetween(state.selection.from, state.selection.to, (node /* , pos, parent, index */) => {
                 node.marks?.filter(mark => !mark.isInSet(marks)).map(mark => marks.push(mark));
             });
         }
         const markGroup = [schema.marks.noAutoLinkAnchor, schema.marks.strong, schema.marks.em, schema.marks.underline, schema.marks.strikethrough, schema.marks.superscript, schema.marks.subscript];
-        return markGroup.filter(mark_type => {
-            const mark = state.schema.mark(mark_type);
+        return markGroup.filter(markType => {
+            const mark = state.schema.mark(markType);
             return mark.isInSet(marks);
         });
     }
@@ -291,7 +295,6 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
         this._superscriptActive = false;
 
         activeMarks.forEach(mark => {
-            // prettier-ignore
             switch (mark.name) {
                 case 'noAutoLinkAnchor': this._noLinkActive = true; break;
                 case 'strong':           this._boldActive = true; break;
@@ -300,7 +303,8 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
                 case 'strikethrough':    this._strikethroughActive = true; break;
                 case 'subscript':        this._subscriptActive = true; break;
                 case 'superscript':      this._superscriptActive = true; break;
-            }
+                default:
+            } // prettier-ignore
         });
     }
 
@@ -353,53 +357,22 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
         }
     };
 
-    setFontSize = (fontSize: string) => {
+    setFontField = (value: string, fontField: 'fontSize' | 'fontFamily' | 'fontColor' | 'fontHighlight') => {
         if (this.view) {
-            if (this.view.state.selection.from === 1 && this.view.state.selection.empty && (!this.view.state.doc.nodeAt(1) || !this.view.state.doc.nodeAt(1)?.marks.some(m => m.type.name === fontSize))) {
-                this.TextView.dataDoc[this.TextView.fieldKey + '_fontSize'] = fontSize;
+            if (this.view.state.selection.from === 1 && this.view.state.selection.empty && (!this.view.state.doc.nodeAt(1) || !this.view.state.doc.nodeAt(1)?.marks.some(m => m.type.name === value))) {
+                this.TextView.dataDoc[this.TextView.fieldKey + `_${fontField}`] = value;
                 this.view.focus();
             } else {
-                const fmark = this.view.state.schema.marks.pFontSize.create({ fontSize });
+                const attrs: { [key: string]: string } = {};
+                attrs[fontField] = value;
+                const fmark = this.view?.state.schema.marks['pF' + fontField.substring(1)].create(attrs);
                 this.setMark(fmark, this.view.state, (tx: any) => this.view!.dispatch(tx.addStoredMark(fmark)), true);
                 this.view.focus();
             }
-        } else if (SelectionManager.Views.length) {
-            SelectionManager.Views.forEach(dv => (dv.layoutDoc[dv.LayoutFieldKey + '_fontSize'] = fontSize));
-        } else Doc.UserDoc().fontSize = fontSize;
-        this.updateMenu(this.view, undefined, this.props, this.layoutDoc);
-    };
-
-    setFontFamily = (family: string) => {
-        if (this.view) {
-            const fmark = this.view.state.schema.marks.pFontFamily.create({ family });
-            this.setMark(fmark, this.view.state, (tx: any) => this.view!.dispatch(tx.addStoredMark(fmark)), true);
-            this.view.focus();
-        } else if (SelectionManager.Views.length) {
-            SelectionManager.Views.forEach(dv => (dv.layoutDoc[dv.LayoutFieldKey + '_fontFamily'] = family));
-        } else Doc.UserDoc().fontFamily = family;
+        } else Doc.UserDoc()[fontField] = value;
         this.updateMenu(this.view, undefined, this.props, this.layoutDoc);
     };
 
-    setHighlight(color: string) {
-        if (this.view) {
-            const highlightMark = this.view.state.schema.mark(this.view.state.schema.marks.marker, { highlight: color });
-            this.setMark(highlightMark, this.view.state, (tx: any) => this.view!.dispatch(tx.addStoredMark(highlightMark)), true);
-            this.view.focus();
-        } else Doc.UserDoc()._fontHighlight = color;
-        this.updateMenu(this.view, undefined, this.props, this.layoutDoc);
-    }
-
-    setColor(color: string) {
-        if (this.view) {
-            const colorMark = this.view.state.schema.mark(this.view.state.schema.marks.pFontColor, { color });
-            this.setMark(colorMark, this.view.state, (tx: any) => this.view!.dispatch(tx.addStoredMark(colorMark)), true);
-            this.view.focus();
-        } else if (SelectionManager.Views.length) {
-            SelectionManager.Views.forEach(dv => (dv.layoutDoc[dv.LayoutFieldKey + '_fontColor'] = color));
-        } else Doc.UserDoc().fontColor = color;
-        this.updateMenu(this.view, undefined, this.props, this.layoutDoc);
-    }
-
     // TODO: remove doesn't work
     // remove all node type and apply the passed-in one to the selected text
     changeListType = (mapStyle: string) => {
@@ -407,7 +380,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
         const newMapStyle = active === mapStyle ? '' : mapStyle;
         if (!this.view || newMapStyle === '') return;
 
-        let inList = this.view.state.selection.$anchor.node(1).type === schema.nodes.ordered_list;
+        const inList = this.view.state.selection.$anchor.node(1).type === schema.nodes.ordered_list;
         const marks = this.view.state.storedMarks || (this.view.state.selection.$to.parentOffset && this.view.state.selection.$from.marks());
         if (inList) {
             const tx2 = updateBullets(this.view.state.tr, schema, newMapStyle, this.view.state.doc.resolve(this.view.state.selection.$anchor.before(1) + 1).pos, this.view.state.doc.resolve(this.view.state.selection.$anchor.after(1)).pos);
@@ -428,7 +401,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
     insertSummarizer(state: EditorState, dispatch: any) {
         if (state.selection.empty) return false;
         const mark = state.schema.marks.summarize.create();
-        const tr = state.tr;
+        const { tr } = state;
         tr.addMark(state.selection.from, state.selection.to, mark);
         const content = tr.selection.content();
         const newNode = state.schema.nodes.summary.create({ visibility: false, text: content, textslice: content.toJSON() });
@@ -436,13 +409,13 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
         return true;
     }
 
-    vcenterToggle = (view: EditorView, dispatch: any) => {
+    vcenterToggle = () => {
         this.layoutDoc && (this.layoutDoc._layout_centered = !this.layoutDoc._layout_centered);
     };
     align = (view: EditorView, dispatch: any, alignment: 'left' | 'right' | 'center') => {
         if (this.TextView?._props.rootSelected?.()) {
-            var tr = view.state.tr;
-            view.state.doc.nodesBetween(view.state.selection.from, view.state.selection.to, (node, pos, parent, index) => {
+            let { tr } = view.state;
+            view.state.doc.nodesBetween(view.state.selection.from, view.state.selection.to, (node, pos) => {
                 if ([schema.nodes.paragraph, schema.nodes.heading].includes(node.type)) {
                     tr = tr.setNodeMarkup(pos, node.type, { ...node.attrs, align: alignment }, node.marks);
                     return false;
@@ -455,56 +428,14 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
         }
     };
 
-    insetParagraph(state: EditorState, dispatch: any) {
-        var tr = state.tr;
-        state.doc.nodesBetween(state.selection.from, state.selection.to, (node, pos, parent, index) => {
+    paragraphSetup(state: EditorState, dispatch: any, field: 'inset' | 'indent', value?: 0 | 10 | -10) {
+        let { tr } = state;
+        state.doc.nodesBetween(state.selection.from, state.selection.to, (node, pos) => {
             if (node.type === schema.nodes.paragraph || node.type === schema.nodes.heading) {
-                const inset = (node.attrs.inset ? Number(node.attrs.inset) : 0) + 10;
-                tr = tr.setNodeMarkup(pos, node.type, { ...node.attrs, inset }, node.marks);
-                return false;
-            }
-            return true;
-        });
-        dispatch?.(tr);
-        return true;
-    }
-    outsetParagraph(state: EditorState, dispatch: any) {
-        var tr = state.tr;
-        state.doc.nodesBetween(state.selection.from, state.selection.to, (node, pos, parent, index) => {
-            if (node.type === schema.nodes.paragraph || node.type === schema.nodes.heading) {
-                const inset = Math.max(0, (node.attrs.inset ? Number(node.attrs.inset) : 0) - 10);
-                tr = tr.setNodeMarkup(pos, node.type, { ...node.attrs, inset }, node.marks);
-                return false;
-            }
-            return true;
-        });
-        dispatch?.(tr);
-        return true;
-    }
-
-    indentParagraph(state: EditorState, dispatch: any) {
-        var tr = state.tr;
-        const heading = false;
-        state.doc.nodesBetween(state.selection.from, state.selection.to, (node, pos, parent, index) => {
-            if (node.type === schema.nodes.paragraph || node.type === schema.nodes.heading) {
-                const nodeval = node.attrs.indent ? Number(node.attrs.indent) : undefined;
-                const indent = !nodeval ? 25 : nodeval < 0 ? 0 : nodeval + 25;
-                tr = tr.setNodeMarkup(pos, node.type, { ...node.attrs, indent }, node.marks);
-                return false;
-            }
-            return true;
-        });
-        !heading && dispatch?.(tr);
-        return true;
-    }
-
-    hangingIndentParagraph(state: EditorState, dispatch: any) {
-        var tr = state.tr;
-        state.doc.nodesBetween(state.selection.from, state.selection.to, (node, pos, parent, index) => {
-            if (node.type === schema.nodes.paragraph || node.type === schema.nodes.heading) {
-                const nodeval = node.attrs.indent ? Number(node.attrs.indent) : undefined;
-                const indent = !nodeval ? -25 : nodeval > 0 ? 0 : nodeval - 10;
-                tr = tr.setNodeMarkup(pos, node.type, { ...node.attrs, indent }, node.marks);
+                const newValue = !value ?  
+                    (node.attrs[field] ? 0 : node.attrs[field] + 10) : 
+                    Math.max(0, value); // prettier-ignore
+                tr = tr.setNodeMarkup(pos, node.type, { ...node.attrs, ...(field === 'inset' ? { inset: newValue } : { indent: newValue }) }, node.marks);
                 return false;
             }
             return true;
@@ -514,7 +445,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
     }
 
     insertBlockquote(state: EditorState, dispatch: any) {
-        const path = (state.selection.$from as any).path;
+        const { path } = state.selection.$from as any;
         if (path.length > 6 && path[path.length - 6].type === schema.nodes.blockquote) {
             lift(state, dispatch);
         } else {
@@ -547,13 +478,13 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
     }
 
     @action
-    fillBrush(state: EditorState, dispatch: any) {
+    fillBrush() {
         if (!this.view) return;
 
         if (!Array.from(this.brushMarks.keys()).length) {
-            const selected_marks = this.getMarksInSelection(this.view.state);
-            if (selected_marks.size >= 0) {
-                this.brushMarks = selected_marks;
+            const selectedMarks = this.getMarksInSelection(this.view.state);
+            if (selectedMarks.size >= 0) {
+                this.brushMarks = selectedMarks;
             }
         } else {
             const { from, to, $from } = this.view.state.selection;
@@ -597,9 +528,12 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
 
         const button = (
             <Tooltip title={<div className="dash-tooltip">set hyperlink</div>} placement="bottom">
-                <button className="antimodeMenu-button color-preview-button">
-                    <FontAwesomeIcon icon="link" size="lg" />
-                </button>
+                {
+                    // eslint-disable-next-line jsx-a11y/control-has-associated-label
+                    <button type="button" className="antimodeMenu-button color-preview-button">
+                        <FontAwesomeIcon icon="link" size="lg" />
+                    </button>
+                }
             </Tooltip>
         );
 
@@ -607,21 +541,22 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
             <div className="dropdown link-menu">
                 <p>Linked to:</p>
                 <input value={link} ref={this._linkToRef} placeholder="Enter URL" onChange={onLinkChange} />
-                <button className="make-button" onPointerDown={e => this.makeLinkToURL(link, 'add:right')}>
+                <button type="button" className="make-button" onPointerDown={() => this.makeLinkToURL(link)}>
                     Apply hyperlink
                 </button>
                 <div className="divider" />
-                <button className="remove-button" onPointerDown={e => this.deleteLink()}>
+                <button type="button" className="remove-button" onPointerDown={() => this.deleteLink()}>
                     Remove link
                 </button>
             </div>
         );
 
-        return <ButtonDropdown view={this.view} key={'link button'} button={button} dropdownContent={dropdownContent} openDropdownOnButton={true} link={true} />;
+        // eslint-disable-next-line no-use-before-define
+        return <ButtonDropdown view={this.view} key="link button" button={button} dropdownContent={dropdownContent} openDropdownOnButton link />;
     }
 
     async getTextLinkTargetTitle() {
-        if (!this.view) return;
+        if (!this.view) return undefined;
 
         const node = this.view.state.selection.$from.nodeAfter;
         const link = node && node.marks.find(m => m.type.name === 'link');
@@ -633,15 +568,15 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
                     if (linkclicked) {
                         const linkDoc = await DocServer.GetRefField(linkclicked);
                         if (linkDoc instanceof Doc) {
-                            const link_anchor_1 = await Cast(linkDoc.link_anchor_1, Doc);
-                            const link_anchor_2 = await Cast(linkDoc.link_anchor_2, Doc);
+                            const linkAnchor1 = await Cast(linkDoc.link_anchor_1, Doc);
+                            const linkAnchor2 = await Cast(linkDoc.link_anchor_2, Doc);
                             const currentDoc = SelectionManager.Docs.lastElement();
-                            if (currentDoc && link_anchor_1 && link_anchor_2) {
-                                if (Doc.AreProtosEqual(currentDoc, link_anchor_1)) {
-                                    return StrCast(link_anchor_2.title);
+                            if (currentDoc && linkAnchor1 && linkAnchor2) {
+                                if (Doc.AreProtosEqual(currentDoc, linkAnchor1)) {
+                                    return StrCast(linkAnchor2.title);
                                 }
-                                if (Doc.AreProtosEqual(currentDoc, link_anchor_2)) {
-                                    return StrCast(link_anchor_1.title);
+                                if (Doc.AreProtosEqual(currentDoc, linkAnchor2)) {
+                                    return StrCast(linkAnchor1.title);
                                 }
                             }
                         }
@@ -653,11 +588,12 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
                 return link.attrs.title;
             }
         }
+        return undefined;
     }
 
     // TODO: should check for valid URL
     @undoBatch
-    makeLinkToURL = (target: string, lcoation: string) => {
+    makeLinkToURL = (target: string) => {
         ((this.view as any)?.TextView as FormattedTextBox).makeLinkAnchor(undefined, 'onRadd:rightight', target, target);
     };
 
@@ -736,7 +672,11 @@ export class ButtonDropdown extends ObservableReactComponent<ButtonDropdownProps
 
     render() {
         return (
-            <div className="button-dropdown-wrapper" ref={node => (this.ref = node)}>
+            <div
+                className="button-dropdown-wrapper"
+                ref={node => {
+                    this.ref = node;
+                }}>
                 {!this._props.pdf ? (
                     <div className="antimodeMenu-button dropdown-button-combined" onPointerDown={this._props.openDropdownOnButton ? this.onDropdownClick : undefined}>
                         {this._props.button}
@@ -747,9 +687,12 @@ export class ButtonDropdown extends ObservableReactComponent<ButtonDropdownProps
                 ) : (
                     <>
                         {this._props.button}
-                        <button className="dropdown-button antimodeMenu-button" key="antimodebutton" onPointerDown={this.onDropdownClick}>
-                            <FontAwesomeIcon icon="caret-down" size="sm" />
-                        </button>
+                        {
+                            // eslint-disable-next-line jsx-a11y/control-has-associated-label
+                            <button type="button" className="dropdown-button antimodeMenu-button" key="antimodebutton" onPointerDown={this.onDropdownClick}>
+                                <FontAwesomeIcon icon="caret-down" size="sm" />
+                            </button>
+                        }
                     </>
                 )}
                 {this.showDropdown ? this._props.dropdownContent : null}
@@ -762,10 +705,11 @@ interface RichTextMenuPluginProps {
     editorProps: any;
 }
 export class RichTextMenuPlugin extends React.Component<RichTextMenuPluginProps> {
-    render() {
-        return null;
-    }
+    // eslint-disable-next-line react/no-unused-class-component-methods
     update(view: EditorView, lastState: EditorState | undefined) {
         RichTextMenu.Instance?.updateMenu(view, lastState, this.props.editorProps, (view as any).TextView?.layoutDoc);
     }
+    render() {
+        return null;
+    }
 }
diff --git a/src/client/views/nodes/formattedText/RichTextRules.ts b/src/client/views/nodes/formattedText/RichTextRules.ts
index 5b5617484..88adab66e 100644
--- a/src/client/views/nodes/formattedText/RichTextRules.ts
+++ b/src/client/views/nodes/formattedText/RichTextRules.ts
@@ -263,7 +263,7 @@ export class RichTextRules {
                 };
 
                 if (isValidColor(color)) {
-                    return state.tr.deleteRange(start, end).addStoredMark(schema.marks.pFontColor.create({ color: color }));
+                    return state.tr.deleteRange(start, end).addStoredMark(schema.marks.pFontColor.create({ fontColor: color }));
                 }
 
                 return null;
diff --git a/src/client/views/nodes/formattedText/marks_rts.ts b/src/client/views/nodes/formattedText/marks_rts.ts
index d1c7b72a5..6e1f325cf 100644
--- a/src/client/views/nodes/formattedText/marks_rts.ts
+++ b/src/client/views/nodes/formattedText/marks_rts.ts
@@ -127,29 +127,29 @@ export const marks: { [index: string]: MarkSpec } = {
 
     /* FONTS */
     pFontFamily: {
-        attrs: { family: { default: '' } },
+        attrs: { fontFamily: { default: '' } },
         parseDOM: [
             {
                 tag: 'span',
                 getAttrs(dom: any) {
                     const cstyle = getComputedStyle(dom);
                     if (cstyle.font) {
-                        if (cstyle.font.indexOf('Times New Roman') !== -1) return { family: 'Times New Roman' };
-                        if (cstyle.font.indexOf('Arial') !== -1) return { family: 'Arial' };
-                        if (cstyle.font.indexOf('Georgia') !== -1) return { family: 'Georgia' };
-                        if (cstyle.font.indexOf('Comic Sans') !== -1) return { family: 'Comic Sans MS' };
-                        if (cstyle.font.indexOf('Tahoma') !== -1) return { family: 'Tahoma' };
-                        if (cstyle.font.indexOf('Crimson') !== -1) return { family: 'Crimson Text' };
+                        if (cstyle.font.indexOf('Times New Roman') !== -1) return { fontFamily: 'Times New Roman' };
+                        if (cstyle.font.indexOf('Arial') !== -1) return { fontFamily: 'Arial' };
+                        if (cstyle.font.indexOf('Georgia') !== -1) return { fontFamily: 'Georgia' };
+                        if (cstyle.font.indexOf('Comic Sans') !== -1) return { fontFamily: 'Comic Sans MS' };
+                        if (cstyle.font.indexOf('Tahoma') !== -1) return { fontFamily: 'Tahoma' };
+                        if (cstyle.font.indexOf('Crimson') !== -1) return { fontFamily: 'Crimson Text' };
                     }
-                    return { family: '' };
+                    return { fontFamily: '' };
                 },
             },
         ],
-        toDOM: node => (node.attrs.family ? ['span', { style: `font-family: "${node.attrs.family}";` }] : ['span', 0]),
+        toDOM: node => (node.attrs.fontFamily ? ['span', { style: `font-family: "${node.attrs.fontFamily}";` }] : ['span', 0]),
     },
     // :: MarkSpec Coloring on text. Has `color` attribute that defined the color of the marked text.
     pFontColor: {
-        attrs: { color: { default: '' } },
+        attrs: { fontColor: { default: '' } },
         inclusive: true,
         parseDOM: [
             {
@@ -159,24 +159,24 @@ export const marks: { [index: string]: MarkSpec } = {
                 },
             },
         ],
-        toDOM: node => (node.attrs.color ? ['span', { style: 'color:' + node.attrs.color }] : ['span', 0]),
+        toDOM: node => (node.attrs.fontColor ? ['span', { style: 'color:' + node.attrs.fontColor }] : ['span', 0]),
     },
 
-    marker: {
+    pFontHighlight: {
         attrs: {
-            highlight: { default: 'transparent' },
+            fontHighlight: { default: 'transparent' },
         },
         inclusive: true,
         parseDOM: [
             {
                 tag: 'span',
                 getAttrs(dom: any) {
-                    return { highlight: dom.getAttribute('backgroundColor') };
+                    return { fontHighlight: dom.getAttribute('background-color') };
                 },
             },
         ],
         toDOM(node: any) {
-            return node.attrs.highlight ? ['span', { style: 'background-color:' + node.attrs.highlight }] : ['span', { style: 'background-color: transparent' }];
+            return node.attrs.fontHighlight ? ['span', { style: 'background-color:' + node.attrs.fontHighlight }] : ['span', { style: 'background-color: transparent' }];
         },
     },
 
diff --git a/src/client/views/nodes/formattedText/nodes_rts.ts b/src/client/views/nodes/formattedText/nodes_rts.ts
index 70b6604ab..184487b7d 100644
--- a/src/client/views/nodes/formattedText/nodes_rts.ts
+++ b/src/client/views/nodes/formattedText/nodes_rts.ts
@@ -4,14 +4,14 @@ import { ParagraphNodeSpec, toParagraphDOM, getParagraphNodeAttrs } from './Para
 import { DocServer } from '../../../DocServer';
 import { Doc, Field, FieldType } from '../../../../fields/Doc';
 
-const blockquoteDOM: DOMOutputSpec = ['blockquote', 0],
-    hrDOM: DOMOutputSpec = ['hr'],
-    preDOM: DOMOutputSpec = ['pre', ['code', 0]],
-    brDOM: DOMOutputSpec = ['br'],
-    ulDOM: DOMOutputSpec = ['ul', 0];
+const blockquoteDOM: DOMOutputSpec = ['blockquote', 0];
+const hrDOM: DOMOutputSpec = ['hr'];
+const preDOM: DOMOutputSpec = ['pre', ['code', 0]];
+const brDOM: DOMOutputSpec = ['br'];
+// const ulDOM: DOMOutputSpec = ['ul', 0];
 
-function formatAudioTime(time: number) {
-    time = Math.round(time);
+function formatAudioTime(timeIn: number) {
+    const time = Math.round(timeIn);
     const hours = Math.floor(time / 60 / 60);
     const minutes = Math.floor(time / 60) - hours * 60;
     const seconds = time % 60;
diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx
index 518bf66cd..a3b1a419b 100644
--- a/src/client/views/nodes/trails/PresBox.tsx
+++ b/src/client/views/nodes/trails/PresBox.tsx
@@ -1,3 +1,5 @@
+/* eslint-disable jsx-a11y/no-static-element-interactions */
+/* eslint-disable jsx-a11y/click-events-have-key-events */
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { Tooltip } from '@mui/material';
 import { action, computed, IReactionDisposer, makeObservable, observable, ObservableSet, reaction, runInAction } from 'mobx';
@@ -13,7 +15,6 @@ import { ObjectField } from '../../../../fields/ObjectField';
 import { listSpec } from '../../../../fields/Schema';
 import { ComputedField, ScriptField } from '../../../../fields/ScriptField';
 import { BoolCast, Cast, DocCast, NumCast, StrCast } from '../../../../fields/Types';
-import { AudioField } from '../../../../fields/URLField';
 import { emptyFunction, emptyPath, stringHash } from '../../../../Utils';
 import { DocServer } from '../../../DocServer';
 import { Docs } from '../../../documents/Documents';
@@ -30,7 +31,7 @@ import { CollectionFreeFormView } from '../../collections/collectionFreeForm';
 import { CollectionStackedTimeline } from '../../collections/CollectionStackedTimeline';
 import { CollectionView } from '../../collections/CollectionView';
 import { TreeView } from '../../collections/TreeView';
-import { pinDataTypes, PinProps, ViewBoxBaseComponent } from '../../DocComponent';
+import { pinDataTypes as dataTypes, PinProps, ViewBoxBaseComponent } from '../../DocComponent';
 import { Colors } from '../../global/globalEnums';
 import { LightboxView } from '../../LightboxView';
 import { DocumentView, OpenWhere, OpenWhereMod } from '../DocumentView';
@@ -38,6 +39,7 @@ import { FieldView, FieldViewProps, FocusViewOptions } from '../FieldView';
 import { ScriptingBox } from '../ScriptingBox';
 import './PresBox.scss';
 import { PresEffect, PresEffectDirection, PresMovement, PresStatus } from './PresEnums';
+import { SettingsManager } from '../../../util/SettingsManager';
 
 @observer
 export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
@@ -61,6 +63,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
     _unmounting = false; // flag that view is unmounting used to block RemFromMap from deleting things
     _presTimer: NodeJS.Timeout | undefined;
 
+    // eslint-disable-next-line no-use-before-define
     @observable public static Instance: PresBox;
 
     @observable _isChildActive = false;
@@ -118,6 +121,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
     @computed get selectedDocumentView() {
         if (SelectionManager.Views.length) return SelectionManager.Views[0];
         if (this.selectedArray.size) return DocumentManager.Instance.getDocumentView(this.Document);
+        return undefined;
     }
     @computed get isPres() {
         return this.selectedDoc === this.Document;
@@ -172,7 +176,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
             () => this.layoutDoc.presentation_status === PresStatus.Edit,
             editing => editing && 
                 this.childDocs.filter(doc => doc.presentation_indexed !== undefined).forEach(doc => {
-                    this.progressivizedItems(doc)?.forEach(indexedDoc => (indexedDoc.opacity = undefined));
+                    this.progressivizedItems(doc)?.forEach(indexedDoc => { indexedDoc.opacity = undefined; });
                     doc.presentation_indexed = Math.min(this.progressivizedItems(doc)?.length ?? 0, 1);
                 }) // prettier-ignore
         );
@@ -202,13 +206,13 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         }
     };
 
-    //TODO: al: it seems currently that tempMedia doesn't stop onslidechange after clicking the button; the time the tempmedia stop depends on the start & end time
+    // TODO: al: it seems currently that tempMedia doesn't stop onslidechange after clicking the button; the time the tempmedia stop depends on the start & end time
     // TODO: to handle child slides (entering into subtrail and exiting), also the next() and back() functions
     // No more frames in current doc and next slide is defined, therefore move to next slide
     nextSlide = (slideNum?: number) => {
         const nextSlideInd = slideNum ?? this.itemIndex + 1;
         let curSlideInd = nextSlideInd;
-        //CollectionStackedTimeline.CurrentlyPlaying?.map(clipView => clipView?.ComponentView?.Pause?.());
+        // CollectionStackedTimeline.CurrentlyPlaying?.map(clipView => clipView?.ComponentView?.Pause?.());
         this.clearSelectedArray();
         const doGroupWithUp =
             (nextSelected: number, force = false) =>
@@ -220,7 +224,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                         if (serial) {
                             this.gotoDocument(nextSelected, this.activeItem, true, async () => {
                                 const waitTime = NumCast(this.activeItem.presentation_duration);
-                                await new Promise<void>(res => setTimeout(() => res(), Math.max(0, waitTime)));
+                                await new Promise<void>(res => {
+                                    setTimeout(res, Math.max(0, waitTime));
+                                });
                                 doGroupWithUp(nextSelected + 1)();
                             });
                         } else {
@@ -239,8 +245,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         const targetList = PresBox.targetRenderedDoc(doc);
         if (doc.presentation_indexed !== undefined && targetList) {
             const listItems = (Cast(targetList[Doc.LayoutFieldKey(targetList)], listSpec(Doc), null)?.filter(d => d instanceof Doc) as Doc[]) ?? DocListCast(targetList[Doc.LayoutFieldKey(targetList) + '_annotations']);
-            return listItems.filter(doc => !doc.layout_unrendered);
+            return listItems.filter(ldoc => !ldoc.layout_unrendered);
         }
+        return undefined;
     };
 
     // go to documents chain
@@ -259,7 +266,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                 const targetRenderedDoc = PresBox.targetRenderedDoc(this.activeItem);
                 targetRenderedDoc._dataTransition = 'all 1s';
                 targetRenderedDoc.opacity = 1;
-                setTimeout(() => (targetRenderedDoc._dataTransition = 'inherit'), 1000);
+                setTimeout(() => {
+                    targetRenderedDoc._dataTransition = 'inherit';
+                }, 1000);
                 const listItems = this.progressivizedItems(this.activeItem);
                 if (listItems && presIndexed < listItems.length) {
                     if (!first) {
@@ -280,6 +289,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                     return true;
                 }
             }
+            return undefined;
         };
         if (progressiveReveal(false)) return true;
         if (this.childDocs[this.itemIndex + 1] !== undefined) {
@@ -289,7 +299,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
 
             // before moving onto next slide, run the subroutines :)
             const currentDoc = this.childDocs[this.itemIndex];
-            //could i do this.childDocs[this.itemIndex] for first arg?
+            // could i do this.childDocs[this.itemIndex] for first arg?
             this.runSubroutines(TreeView.GetRunningChildren.get(currentDoc)?.(), this.childDocs[this.itemIndex + 1]);
 
             this.nextSlide(curLast + 1 === this.childDocs.length ? (this.layoutDoc.presLoop ? 0 : curLast) : curLast + 1);
@@ -309,7 +319,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
     // Called when the user activates 'back' - to move to the previous part of the pres. trail
     @action
     back = () => {
-        const activeItem: Doc = this.activeItem;
+        const { activeItem } = this;
         let prevSelected = this.itemIndex;
         // Functionality for group with up
         let didZoom = activeItem.presentation_movement;
@@ -328,8 +338,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         return this.itemIndex;
     };
 
-    //The function that is called when a document is clicked or reached through next or back.
-    //it'll also execute the necessary actions if presentation is playing.
+    // The function that is called when a document is clicked or reached through next or back.
+    // it'll also execute the necessary actions if presentation is playing.
     @undoBatch
     public gotoDocument = action((index: number, from?: Doc, group?: boolean, finished?: () => void) => {
         Doc.UnBrushAllDocs();
@@ -346,13 +356,13 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                 this.startTempMedia(this.targetDoc, this.activeItem);
             }
             if (!group) this.clearSelectedArray();
-            this.childDocs[index] && this.addToSelectedArray(this.childDocs[index]); //Update selected array
+            this.childDocs[index] && this.addToSelectedArray(this.childDocs[index]); // Update selected array
             this.turnOffEdit();
-            this.navigateToActiveItem(finished); //Handles movement to element only when presentationTrail is list
-            this.doHideBeforeAfter(); //Handles hide after/before
+            this.navigateToActiveItem(finished); // Handles movement to element only when presentationTrail is list
+            this.doHideBeforeAfter(); // Handles hide after/before
         }
     });
-    static pinDataTypes(target?: Doc): pinDataTypes {
+    static pinDataTypes(target?: Doc): dataTypes {
         const targetType = target?.type as any;
         const inkable = [DocumentType.INK].includes(targetType);
         const scrollable = [DocumentType.PDF, DocumentType.RTF, DocumentType.WEB].includes(targetType) || target?._type_collection === CollectionViewType.Stacking;
@@ -363,19 +373,22 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         const datarange = [DocumentType.FUNCPLOT].includes(targetType);
         const dataview = [DocumentType.INK, DocumentType.COL, DocumentType.IMG, DocumentType.RTF].includes(targetType) && target?.activeFrame === undefined;
         const poslayoutview = [DocumentType.COL].includes(targetType) && target?.activeFrame === undefined;
-        const type_collection = targetType === DocumentType.COL;
+        const typeCollection = targetType === DocumentType.COL;
         const filters = true;
         const pivot = true;
         const dataannos = false;
-        return { scrollable, pannable, inkable, type_collection, pivot, map, filters, temporal, clippable, dataview, datarange, poslayoutview, dataannos };
+        return { scrollable, pannable, inkable, type_collection: typeCollection, pivot, map, filters, temporal, clippable, dataview, datarange, poslayoutview, dataannos };
     }
 
     @action
-    playAnnotation = (anno: AudioField) => {};
+    playAnnotation = (/* anno: AudioField */) => {
+        /* empty */
+    };
     @action
-    static restoreTargetDocView(bestTargetView: Opt<DocumentView>, activeItem: Doc, transTime: number, pinDocLayout: boolean = BoolCast(activeItem.config_pinLayout), pinDataTypes?: pinDataTypes, targetDoc?: Doc) {
+    // eslint-disable-next-line default-param-last
+    static restoreTargetDocView(bestTargetView: Opt<DocumentView>, activeItem: Doc, transTime: number, pinDocLayout: boolean = BoolCast(activeItem.config_pinLayout), pinDataTypes?: dataTypes, targetDoc?: Doc) {
         const bestTarget = bestTargetView?.Document ?? (targetDoc?.layout_unrendered ? DocCast(targetDoc?.annotationOn) : targetDoc);
-        if (!bestTarget) return;
+        if (!bestTarget) return undefined;
         let changed = false;
         if (pinDocLayout) {
             if (
@@ -392,20 +405,22 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                 bestTarget.width = NumCast(activeItem.config_width, NumCast(bestTarget.width));
                 bestTarget.height = NumCast(activeItem.config_height, NumCast(bestTarget.height));
                 bestTarget[TransitionTimer] && clearTimeout(bestTarget[TransitionTimer]);
-                bestTarget[TransitionTimer] = setTimeout(() => (bestTarget[TransitionTimer] = bestTarget._dataTransition = undefined), transTime + 10);
+                bestTarget[TransitionTimer] = setTimeout(() => {
+                    bestTarget[TransitionTimer] = bestTarget._dataTransition = undefined;
+                }, transTime + 10);
                 changed = true;
             }
         }
 
         const activeFrame = activeItem.config_activeFrame ?? activeItem.config_currentFrame;
         if (activeFrame !== undefined) {
-            const transTime = NumCast(activeItem.presentation_transition, 500);
+            const frameTime = NumCast(activeItem.presentation_transition, 500);
             const acontext = activeItem.config_activeFrame !== undefined ? DocCast(DocCast(activeItem.presentation_targetDoc).embedContainer) : DocCast(activeItem.presentation_targetDoc);
             const context = DocCast(acontext)?.annotationOn ? DocCast(DocCast(acontext).annotationOn) : acontext;
             if (context) {
                 const ffview = DocumentManager.Instance.getFirstDocumentView(context)?.CollectionFreeFormView;
                 if (ffview?.childDocs) {
-                    PresBox.Instance._keyTimer = CollectionFreeFormView.gotoKeyframe(PresBox.Instance._keyTimer, ffview.childDocs, transTime);
+                    PresBox.Instance._keyTimer = CollectionFreeFormView.gotoKeyframe(PresBox.Instance._keyTimer, ffview.childDocs, frameTime);
                     ffview.layoutDoc._currentFrame = NumCast(activeFrame);
                 }
             }
@@ -423,7 +438,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                 bestTargetData[fkey] = activeItem.config_data instanceof ObjectField ? activeItem.config_data[Copy]() : activeItem.config_data;
             }
             bestTarget[fkey + '_usePath'] = activeItem.config_usePath;
-            setTimeout(() => (bestTarget._dataTransition = undefined), transTime + 10);
+            setTimeout(() => {
+                bestTarget._dataTransition = undefined;
+            }, transTime + 10);
         }
         if (pinDataTypes?.datarange || (!pinDataTypes && activeItem.config_xRange !== undefined)) {
             if (bestTarget.xRange !== activeItem.config_xRange) {
@@ -565,7 +582,15 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                         Doc.AddDocToList(bestTarget[DocData], layoutField, doc);
                     }
                 });
-            setTimeout(() => Array.from(transitioned).forEach(action(doc => (doc._dataTransition = undefined))), transTime + 10);
+            setTimeout(
+                () =>
+                    Array.from(transitioned).forEach(
+                        action(doc => {
+                            doc._dataTransition = undefined;
+                        })
+                    ),
+                transTime + 10
+            );
         }
         if ((pinDataTypes?.pannable || (!pinDataTypes && (activeItem.config_viewBounds !== undefined || activeItem.config_panX !== undefined || activeItem.config_viewScale !== undefined))) && !bestTarget.isGroup) {
             const contentBounds = Cast(activeItem.config_viewBounds, listSpec('number'));
@@ -580,18 +605,17 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                     activeItem.presentation_movement === PresMovement.Zoom && (bestTarget._freeform_scale = computedScale);
                     dv.ComponentView?.brushView?.(viewport, transTime, 2500);
                 }
-            } else {
-                if (bestTarget._freeform_panX !== activeItem.config_panX || bestTarget._freeform_panY !== activeItem.config_panY || bestTarget._freeform_scale !== activeItem.config_viewScale) {
-                    bestTarget._freeform_panX = activeItem.config_panX ?? bestTarget._freeform_panX;
-                    bestTarget._freeform_panY = activeItem.config_panY ?? bestTarget._freeform_panY;
-                    bestTarget._freeform_scale = activeItem.config_viewScale ?? bestTarget._freeform_scale;
-                    changed = true;
-                }
+            } else if (bestTarget._freeform_panX !== activeItem.config_panX || bestTarget._freeform_panY !== activeItem.config_panY || bestTarget._freeform_scale !== activeItem.config_viewScale) {
+                bestTarget._freeform_panX = activeItem.config_panX ?? bestTarget._freeform_panX;
+                bestTarget._freeform_panY = activeItem.config_panY ?? bestTarget._freeform_panY;
+                bestTarget._freeform_scale = activeItem.config_viewScale ?? bestTarget._freeform_scale;
+                changed = true;
             }
         }
         if (changed) {
             return bestTargetView?.setViewTransition('all', transTime);
         }
+        return undefined;
     }
 
     /// copies values from the targetDoc (which is the prototype of the pinDoc) to
@@ -628,8 +652,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                 pinDoc.config_data = targetDoc[fkey] instanceof ObjectField ? (targetDoc[fkey] as ObjectField)[Copy]() : targetDoc.data;
             }
             if (pinProps.pinData.dataannos) {
-                const fkey = Doc.LayoutFieldKey(targetDoc);
-                pinDoc.config_annotations = new List<Doc>(DocListCast(targetDoc[DocData][fkey + '_annotations']).filter(doc => !doc.layout_unrendered));
+                const fieldKey = Doc.LayoutFieldKey(targetDoc);
+                pinDoc.config_annotations = new List<Doc>(DocListCast(targetDoc[DocData][fieldKey + '_annotations']).filter(doc => !doc.layout_unrendered));
             }
             if (pinProps.pinData.inkable) {
                 pinDoc.config_fillColor = targetDoc.fillColor;
@@ -639,19 +663,19 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
             }
             if (pinProps.pinData.scrollable) pinDoc.config_scrollTop = targetDoc._layout_scrollTop;
             if (pinProps.pinData.clippable) {
-                const fkey = Doc.LayoutFieldKey(targetDoc);
-                pinDoc.config_clipWidth = targetDoc[fkey + '_clipWidth'];
+                const fieldKey = Doc.LayoutFieldKey(targetDoc);
+                pinDoc.config_clipWidth = targetDoc[fieldKey + '_clipWidth'];
             }
             if (pinProps.pinData.datarange) {
-                pinDoc.config_xRange = undefined; //targetDoc?.xrange;
-                pinDoc.config_yRange = undefined; //targetDoc?.yrange;
+                pinDoc.config_xRange = undefined; // targetDoc?.xrange;
+                pinDoc.config_yRange = undefined; // targetDoc?.yrange;
             }
             if (pinProps.pinData.map) {
                 // pinDoc.config_latitude = targetDoc?.latitude;
                 // pinDoc.config_longitude = targetDoc?.longitude;
                 pinDoc.config_map_zoom = targetDoc?.map_zoom;
                 pinDoc.config_map_type = targetDoc?.map_type;
-                //...
+                // ...
             }
             if (pinProps.pinData.poslayoutview)
                 pinDoc.config_pinLayoutData = new List<string>(
@@ -711,8 +735,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
      * on the right.
      */
     navigateToActiveItem = (afterNav?: () => void) => {
-        const activeItem: Doc = this.activeItem;
-        const targetDoc: Doc = this.targetDoc;
+        const { activeItem, targetDoc } = this;
         const finished = () => {
             afterNav?.();
             targetDoc[Animation] = undefined;
@@ -766,7 +789,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         if (targetDoc) {
             if (activeItem.presentation_targetDoc instanceof Doc) activeItem.presentation_targetDoc[Animation] = undefined;
 
-            DocumentManager.Instance.AddViewRenderedCb(LightboxView.LightboxDoc, dv => {
+            DocumentManager.Instance.AddViewRenderedCb(LightboxView.LightboxDoc, () => {
                 // if target or the doc it annotates is not in the lightbox, then close the lightbox
                 if (!DocumentManager.Instance.getLightboxDocumentView(DocCast(targetDoc.annotationOn) ?? targetDoc)) {
                     LightboxView.Instance.SetLightboxDoc(undefined);
@@ -798,7 +821,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                     opacity = 0;
                 } else if (index === this.itemIndex || !curDoc.presentation_hideAfter) {
                     opacity = 1;
-                    setTimeout(() => (tagDoc._dataTransition = undefined), 1000);
+                    setTimeout(() => {
+                        tagDoc._dataTransition = undefined;
+                    }, 1000);
                 }
             }
             const hidingIndAft =
@@ -828,12 +853,16 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         const savedStates = docs.map(doc => {
             switch (doc.type) {
                 case DocumentType.COL:
-                    if (doc._type_collection === CollectionViewType.Freeform) return { type: CollectionViewType.Freeform, doc, x: NumCast(doc.freeform_panX), y: NumCast(doc.freeform_panY), s: NumCast(doc.freeform_scale) };
+                    if (doc._type_collection === CollectionViewType.Freeform) {
+                        return { type: CollectionViewType.Freeform, doc, x: NumCast(doc.freeform_panX), y: NumCast(doc.freeform_panY), s: NumCast(doc.freeform_scale) };
+                    }
                     break;
                 case DocumentType.INK:
                     if (doc.data instanceof InkField) {
                         return { type: doc.type, doc, data: doc.data?.[Copy](), fillColor: doc.fillColor, color: doc.color, x: NumCast(doc.x), y: NumCast(doc.y) };
                     }
+                    break;
+                default:
             }
             return undefined;
         });
@@ -841,7 +870,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         this._exitTrail = () => {
             savedStates
                 .filter(savedState => savedState)
-                .map(savedState => {
+                .forEach(savedState => {
                     switch (savedState?.type) {
                         case CollectionViewType.Freeform:
                             {
@@ -861,6 +890,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                                 doc.color = color;
                             }
                             break;
+                        default:
                     }
                 });
             LightboxView.Instance.SetLightboxDoc(undefined);
@@ -879,8 +909,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         }
     };
 
-    //The function that resets the presentation by removing every action done by it. It also
-    //stops the presentaton.
+    // The function that resets the presentation by removing every action done by it. It also
+    // stops the presentaton.
     resetPresentation = () => {
         this.childDocs
             .map(doc => PresBox.targetRenderedDoc(doc))
@@ -897,12 +927,14 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
     // The function allows for viewing the pres path on toggle
     @action togglePath = (off?: boolean) => {
         this._pathBoolean = off ? false : !this._pathBoolean;
-        CollectionFreeFormView.ShowPresPaths = this._pathBoolean;
+        SnappingManager.SetShowPresPaths(this._pathBoolean);
     };
 
     // The function allows for expanding the view of pres on toggle
     @action toggleExpandMode = () => {
-        runInAction(() => (this._expandBoolean = !this._expandBoolean));
+        runInAction(() => {
+            this._expandBoolean = !this._expandBoolean;
+        });
         this.Document.expandBoolean = this._expandBoolean;
         this.childDocs.forEach(doc => {
             doc.presentation_expandInlineButton = this._expandBoolean;
@@ -918,7 +950,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                 const startInd = NumCast(doc.presentation_indexedStart);
                 this.progressivizedItems(doc)
                     ?.slice(startInd)
-                    .forEach(indexedDoc => (indexedDoc.opacity = 0));
+                    .forEach(indexedDoc => {
+                        indexedDoc.opacity = 0;
+                    });
                 doc.presentation_indexed = Math.min(this.progressivizedItems(doc)?.length ?? 0, startInd);
             }
             // if (doc.presentation_hide && this.childDocs.indexOf(doc) === startIndex)  tagDoc.opacity = 0;
@@ -969,13 +1003,13 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
 
     public static minimizedWidth = 198;
     public static OpenPresMinimized(doc: Doc, pt: number[]) {
-        doc.overlayX = pt[0];
-        doc.overlayY = pt[1];
+        [doc.overlayX, doc.overlayY] = pt;
         doc._height = 30;
         doc._width = PresBox.minimizedWidth;
         Doc.AddToMyOverlay(doc);
         PresBox.Instance?.initializePresState(PresBox.Instance.itemIndex);
-        return (doc.presentation_status = PresStatus.Manual);
+        doc.presentation_status = PresStatus.Manual;
+        return doc.presentation_status;
     }
 
     /**
@@ -984,12 +1018,11 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
      */
     @undoBatch
     viewChanged = action((e: React.ChangeEvent) => {
-        //@ts-ignore
-        const type_collection = e.target.selectedOptions[0].value as CollectionViewType;
-        this.layoutDoc.presFieldKey = this.fieldKey + (type_collection === CollectionViewType.Tree ? '-linearized' : '');
+        const typeCollection = (e.target as any).selectedOptions[0].value as CollectionViewType;
+        this.layoutDoc.presFieldKey = this.fieldKey + (typeCollection === CollectionViewType.Tree ? '-linearized' : '');
         // pivot field may be set by the user in timeline view (or some other way) -- need to reset it here
-        [CollectionViewType.Tree || CollectionViewType.Stacking].includes(type_collection) && (this.Document._pivotField = undefined);
-        this.Document._type_collection = type_collection;
+        [CollectionViewType.Tree || CollectionViewType.Stacking].includes(typeCollection) && (this.Document._pivotField = undefined);
+        this.Document._type_collection = typeCollection;
         if (this.isTreeOrStack) {
             this.layoutDoc._gridGap = 0;
         }
@@ -1001,10 +1034,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
      */
     // @undoBatch
     mediaStopChanged = action((e: React.ChangeEvent) => {
-        const activeItem: Doc = this.activeItem;
-        //@ts-ignore
-        const stopDoc = e.target.selectedOptions[0].value as string;
-        const stopDocIndex: number = Number(stopDoc[0]);
+        const { activeItem } = this;
+        const stopDoc = (e.target as any).selectedOptions[0].value as string;
+        const stopDocIndex = Number(stopDoc[0]);
         activeItem.mediaStopDoc = stopDocIndex;
         if (this.childDocs[stopDocIndex - 1].mediaStopTriggerList) {
             const list = DocListCast(this.childDocs[stopDocIndex - 1].mediaStopTriggerList);
@@ -1025,10 +1057,12 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         return StrCast(activeItem.presentation_movement);
     });
 
-    whenChildContentsActiveChanged = action((isActive: boolean) => this._props.whenChildContentsActiveChanged((this._isChildActive = isActive)));
+    whenChildContentsActiveChanged = action((isActive: boolean) => {
+        this._props.whenChildContentsActiveChanged((this._isChildActive = isActive));
+    });
     // For dragging documents into the presentation trail
     addDocumentFilter = (docs: Doc[]) => {
-        docs.forEach((doc, i) => {
+        const results = docs.map(doc => {
             if (doc.presentation_targetDoc) return true;
             if (doc.type === DocumentType.LABEL) {
                 const audio = Cast(doc.annotationOn, Doc, null);
@@ -1041,13 +1075,15 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                     return false;
                 }
             } else if (doc.type !== DocumentType.PRES) {
+                // eslint-disable-next-line operator-assignment
                 if (!doc.presentation_targetDoc) doc.title = doc.title + ' - Slide';
                 doc.presentation_targetDoc = doc.createdFrom ?? doc; // dropped document will be a new embedding of an embedded document somewhere else.
                 doc.presentation_movement = PresMovement.Zoom;
                 if (this._expandBoolean) doc.presentation_expandInlineButton = true;
             }
+            return false;
         });
-        return true;
+        return !results.some(r => !r);
     };
 
     childLayoutTemplate = () => Docs.Create.PresElementBoxDocument();
@@ -1068,24 +1104,28 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
             const tagDoc = Cast(curDoc.presentation_targetDoc, Doc, null);
             if (curDoc && curDoc === this.activeItem)
                 return (
+                    // eslint-disable-next-line react/no-array-index-key
                     <div key={index} className="selectedList-items">
                         <b>
                             {index + 1}. {curDoc.title}
                         </b>
                     </div>
                 );
-            else if (tagDoc)
+            if (tagDoc)
                 return (
+                    // eslint-disable-next-line react/no-array-index-key
                     <div key={index} className="selectedList-items">
                         {index + 1}. {curDoc.title}
                     </div>
                 );
-            else if (curDoc)
+            if (curDoc)
                 return (
+                    // eslint-disable-next-line react/no-array-index-key
                     <div key={index} className="selectedList-items">
                         {index + 1}. {curDoc.title}
                     </div>
                 );
+            return null;
         });
     }
 
@@ -1095,15 +1135,15 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         presDocView && SelectionManager.SelectView(presDocView, false);
     };
 
-    focusElement = (doc: Doc, options: FocusViewOptions) => {
+    focusElement = (doc: Doc) => {
         this.selectElement(doc);
         return undefined;
     };
 
-    //Regular click
+    // Regular click
     @action
     selectElement = (doc: Doc, noNav = false) => {
-        CollectionStackedTimeline.CurrentlyPlaying?.map((clip, i) => clip?.ComponentView?.Pause?.());
+        CollectionStackedTimeline.CurrentlyPlaying?.map(clip => clip?.ComponentView?.Pause?.());
         if (noNav) {
             const index = this.childDocs.indexOf(doc);
             if (index >= 0 && index < this.childDocs.length) {
@@ -1115,7 +1155,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         this.updateCurrentPresentation(DocCast(doc.embedContainer));
     };
 
-    //Command click
+    // Command click
     @action
     multiSelect = (doc: Doc, ref: HTMLElement, drag: HTMLElement) => {
         if (!this.selectedArray.has(doc)) {
@@ -1130,7 +1170,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         this.selectPres();
     };
 
-    //Shift click
+    // Shift click
     @action
     shiftSelect = (doc: Doc, ref: HTMLElement, drag: HTMLElement) => {
         this.clearSelectedArray();
@@ -1145,7 +1185,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         this.selectPres();
     };
 
-    //regular click
+    // regular click
     @action
     regularSelect = (doc: Doc, ref: HTMLElement, drag: HTMLElement, noNav: boolean, selectPres = true) => {
         this.clearSelectedArray();
@@ -1176,9 +1216,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                 if (this.layoutDoc.presentation_status === 'edit') {
                     undoBatch(
                         action(() => {
-                            for (const doc of this.selectedArray) {
-                                this.removeDocument(doc);
-                            }
+                            Array.from(this.selectedArray).forEach(doc => this.removeDocument(doc));
                             this.clearSelectedArray();
                             this._eleArray.length = 0;
                             this._dragArray.length = 0;
@@ -1245,8 +1283,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                     this.childDocs.forEach(doc => this.addToSelectedArray(doc));
                     handled = true;
                 }
-            default:
                 break;
+            default:
         }
         if (handled) {
             e.stopPropagation();
@@ -1266,6 +1304,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
             const tagDoc = PresBox.targetRenderedDoc(doc);
             const srcContext = Cast(tagDoc.embedContainer, Doc, null);
             const labelCreator = (top: number, left: number, edge: number, fontSize: number) => (
+                // eslint-disable-next-line react/no-array-index-key
                 <div className="pathOrder" key={tagDoc.id + 'pres' + index} style={{ top, left, width: edge, height: edge, fontSize }} onClick={() => this.selectElement(doc)}>
                     <div className="pathOrder-frame">{index + 1}</div>
                 </div>
@@ -1298,7 +1337,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                 order.push(
                     <>
                         {labelCreator(top - indEdge / 2, left - indEdge / 2, indEdge, indFontSize)}
-                        <div className="pathOrder-presPinView" style={{ top, left, width, height, borderWidth: indEdge / 10 }}></div>
+                        <div className="pathOrder-presPinView" style={{ top, left, width, height, borderWidth: indEdge / 10 }} />
                     </>
                 );
             }
@@ -1321,17 +1360,12 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
             .filter(doc => PresBox.targetRenderedDoc(doc)?.embedContainer === collection)
             .forEach((doc, index) => {
                 const tagDoc = PresBox.targetRenderedDoc(doc);
-                if (tagDoc) {
-                    const n1x = NumCast(tagDoc.x) + NumCast(tagDoc._width) / 2;
-                    const n1y = NumCast(tagDoc.y) + NumCast(tagDoc._height) / 2;
-                    if ((index = 0)) pathPoints = n1x + ',' + n1y;
-                    else pathPoints = pathPoints + ' ' + n1x + ',' + n1y;
-                } else if (doc.config_pinView) {
-                    const n1x = NumCast(doc.config_panX);
-                    const n1y = NumCast(doc.config_panY);
-                    if ((index = 0)) pathPoints = n1x + ',' + n1y;
-                    else pathPoints = pathPoints + ' ' + n1x + ',' + n1y;
-                }
+                const [n1x, n1y] = tagDoc //
+                    ? [NumCast(tagDoc.x) + NumCast(tagDoc._width) / 2, NumCast(tagDoc.y) + NumCast(tagDoc._height) / 2]
+                    : [NumCast(doc.config_panX), NumCast(doc.config_panY)];
+
+                if (index === 0) pathPoints = n1x + ',' + n1y;
+                else pathPoints = pathPoints + ' ' + n1x + ',' + n1y;
             });
         return (
             <>
@@ -1377,7 +1411,14 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
 
     @undoBatch
     updateTransitionTime = (number: String, change?: number) => {
-        PresBox.SetTransitionTime(number, (timeInMS: number) => this.selectedArray.forEach(doc => (doc.presentation_transition = timeInMS)), change);
+        PresBox.SetTransitionTime(
+            number,
+            (timeInMS: number) =>
+                this.selectedArray.forEach(doc => {
+                    doc.presentation_transition = timeInMS;
+                }),
+            change
+        );
     };
 
     // Converts seconds to ms and updates presentation_transition
@@ -1387,7 +1428,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         if (change) scale += change;
         if (scale < 0.01) scale = 0.01;
         if (scale > 1) scale = 1;
-        this.selectedArray.forEach(doc => (doc.config_zoom = scale));
+        this.selectedArray.forEach(doc => {
+            doc.config_zoom = scale;
+        });
     };
 
     /*
@@ -1399,76 +1442,96 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         if (change) timeInMS += change;
         if (timeInMS < 100) timeInMS = 100;
         if (timeInMS > 20000) timeInMS = 20000;
-        this.selectedArray.forEach(doc => (doc.presentation_duration = timeInMS));
+        this.selectedArray.forEach(doc => {
+            doc.presentation_duration = timeInMS;
+        });
     };
 
     @undoBatch
-    updateMovement = action((movement: PresMovement, all?: boolean) => (all ? this.childDocs : this.selectedArray).forEach(doc => (doc.presentation_movement = movement)));
+    updateMovement = action((movement: PresMovement, all?: boolean) =>
+        (all ? this.childDocs : this.selectedArray).forEach(doc => {
+            doc.presentation_movement = movement;
+        })
+    );
 
     @undoBatch
     updateHideBefore = (activeItem: Doc) => {
         activeItem.presentation_hideBefore = !activeItem.presentation_hideBefore;
-        this.selectedArray.forEach(doc => (doc.presentation_hideBefore = activeItem.presentation_hideBefore));
+        this.selectedArray.forEach(doc => {
+            doc.presentation_hideBefore = activeItem.presentation_hideBefore;
+        });
     };
 
     @undoBatch
     updateHide = (activeItem: Doc) => {
         activeItem.presentation_hide = !activeItem.presentation_hide;
-        this.selectedArray.forEach(doc => (doc.presentation_hide = activeItem.presentation_hide));
+        this.selectedArray.forEach(doc => {
+            doc.presentation_hide = activeItem.presentation_hide;
+        });
     };
 
     @undoBatch
     updateHideAfter = (activeItem: Doc) => {
         activeItem.presentation_hideAfter = !activeItem.presentation_hideAfter;
-        this.selectedArray.forEach(doc => (doc.presentation_hideAfter = activeItem.presentation_hideAfter));
+        this.selectedArray.forEach(doc => {
+            doc.presentation_hideAfter = activeItem.presentation_hideAfter;
+        });
     };
 
     @undoBatch
     updateOpenDoc = (activeItem: Doc) => {
         activeItem.presentation_openInLightbox = !activeItem.presentation_openInLightbox;
-        this.selectedArray.forEach(doc => (doc.presentation_openInLightbox = activeItem.presentation_openInLightbox));
+        this.selectedArray.forEach(doc => {
+            doc.presentation_openInLightbox = activeItem.presentation_openInLightbox;
+        });
     };
 
     @undoBatch
     updateEaseFunc = (activeItem: Doc) => {
         activeItem.presEaseFunc = activeItem.presEaseFunc === 'linear' ? 'ease' : 'linear';
-        this.selectedArray.forEach(doc => (doc.presEaseFunc = activeItem.presEaseFunc));
+        this.selectedArray.forEach(doc => {
+            doc.presEaseFunc = activeItem.presEaseFunc;
+        });
     };
 
     @undoBatch
-    updateEffectDirection = (effect: PresEffectDirection, all?: boolean) => (all ? this.childDocs : this.selectedArray).forEach(doc => (doc.presentation_effectDirection = effect));
+    updateEffectDirection = (effect: PresEffectDirection, all?: boolean) =>
+        (all ? this.childDocs : this.selectedArray).forEach(doc => {
+            doc.presentation_effectDirection = effect;
+        });
 
     @undoBatch
-    updateEffect = (effect: PresEffect, bullet: boolean, all?: boolean) => (all ? this.childDocs : this.selectedArray).forEach(doc => (bullet ? (doc.presBulletEffect = effect) : (doc.presentation_effect = effect)));
+    updateEffect = (effect: PresEffect, bullet: boolean, all?: boolean) =>
+        (all ? this.childDocs : this.selectedArray).forEach(doc => {
+            bullet ? (doc.presBulletEffect = effect) : (doc.presentation_effect = effect);
+        });
 
     static _sliderBatch: any;
     static endBatch = () => {
         PresBox._sliderBatch.end();
         document.removeEventListener('pointerup', PresBox.endBatch, true);
     };
-    public static inputter = (min: string, step: string, max: string, value: number, active: boolean, change: (val: string) => void, hmargin?: number) => {
-        return (
-            <input
-                type="range"
-                step={step}
-                min={min}
-                max={max}
-                value={value}
-                readOnly={true}
-                style={{ marginLeft: hmargin, marginRight: hmargin, width: `calc(100% - ${2 * (hmargin ?? 0)}px)`, background: SnappingManager.userColor, color: SnappingManager.userVariantColor }}
-                className={`toolbar-slider ${active ? '' : 'none'}`}
-                onPointerDown={e => {
-                    PresBox._sliderBatch = UndoManager.StartBatch('pres slider');
-                    document.addEventListener('pointerup', PresBox.endBatch, true);
-                    e.stopPropagation();
-                }}
-                onChange={e => {
-                    e.stopPropagation();
-                    change(e.target.value);
-                }}
-            />
-        );
-    };
+    public static inputter = (min: string, step: string, max: string, value: number, active: boolean, change: (val: string) => void, hmargin?: number) => (
+        <input
+            type="range"
+            step={step}
+            min={min}
+            max={max}
+            value={value}
+            readOnly
+            style={{ marginLeft: hmargin, marginRight: hmargin, width: `calc(100% - ${2 * (hmargin ?? 0)}px)`, background: SnappingManager.userColor, color: SnappingManager.userVariantColor }}
+            className={`toolbar-slider ${active ? '' : 'none'}`}
+            onPointerDown={e => {
+                PresBox._sliderBatch = UndoManager.StartBatch('pres slider');
+                document.addEventListener('pointerup', PresBox.endBatch, true);
+                e.stopPropagation();
+            }}
+            onChange={e => {
+                e.stopPropagation();
+                change(e.target.value);
+            }}
+        />
+    );
 
     @undoBatch
     applyTo = (array: Doc[]) => {
@@ -1476,17 +1539,18 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         this.updateEffect(this.activeItem.presentation_effect as PresEffect, false, true);
         this.updateEffect(this.activeItem.presBulletEffect as PresEffect, true, true);
         this.updateEffectDirection(this.activeItem.presentation_effectDirection as PresEffectDirection, true);
-        const { presentation_transition, presentation_duration, presentation_hideBefore, presentation_hideAfter } = this.activeItem;
+        // eslint-disable-next-line camelcase
+        const { presentation_transition: pt, presentation_duration: pd, presentation_hideBefore: ph, presentation_hideAfter: pa } = this.activeItem;
         array.forEach(curDoc => {
-            curDoc.presentation_transition = presentation_transition;
-            curDoc.presentation_duration = presentation_duration;
-            curDoc.presentation_hideBefore = presentation_hideBefore;
-            curDoc.presentation_hideAfter = presentation_hideAfter;
+            curDoc.presentation_transition = pt;
+            curDoc.presentation_duration = pd;
+            curDoc.presentation_hideBefore = ph;
+            curDoc.presentation_hideAfter = pa;
         });
     };
 
     @computed get visibilityDurationDropdown() {
-        const activeItem = this.activeItem;
+        const { activeItem } = this;
         if (activeItem && this.targetDoc) {
             const targetType = this.targetDoc.type;
             let duration = activeItem.presentation_duration ? NumCast(activeItem.presentation_duration) / 1000 : 0;
@@ -1502,7 +1566,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                                 Hide before
                             </div>
                         </Tooltip>
-                        <Tooltip title={<div className="dash-tooltip">{'Hide while presented'}</div>}>
+                        <Tooltip title={<div className="dash-tooltip">Hide while presented</div>}>
                             <div
                                 className={`ribbon-toggle ${activeItem.presentation_hide ? 'active' : ''}`}
                                 style={{ border: `solid 1px ${SnappingManager.userColor}`, color: SnappingManager.userColor, background: activeItem.presentation_hide ? SnappingManager.userVariantColor : SnappingManager.userBackgroundColor }}
@@ -1511,7 +1575,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                             </div>
                         </Tooltip>
 
-                        <Tooltip title={<div className="dash-tooltip">{'Hide after presented'}</div>}>
+                        <Tooltip title={<div className="dash-tooltip">Hide after presented</div>}>
                             <div
                                 className={`ribbon-toggle ${activeItem.presentation_hideAfter ? 'active' : ''}`}
                                 style={{ border: `solid 1px ${SnappingManager.userColor}`, color: SnappingManager.userColor, background: activeItem.presentation_hideAfter ? SnappingManager.userVariantColor : SnappingManager.userBackgroundColor }}
@@ -1520,7 +1584,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                             </div>
                         </Tooltip>
 
-                        <Tooltip title={<div className="dash-tooltip">{'Open in lightbox view'}</div>}>
+                        <Tooltip title={<div className="dash-tooltip">Open in lightbox view</div>}>
                             <div
                                 className="ribbon-toggle"
                                 style={{
@@ -1550,15 +1614,15 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                                 </div>
                                 <div className="ribbon-propertyUpDown" style={{ color: SnappingManager.userBackgroundColor, background: SnappingManager.userColor }}>
                                     <div className="ribbon-propertyUpDownItem" onClick={() => this.updateDurationTime(String(duration), 1000)}>
-                                        <FontAwesomeIcon icon={'caret-up'} />
+                                        <FontAwesomeIcon icon="caret-up" />
                                     </div>
                                     <div className="ribbon-propertyUpDownItem" onClick={() => this.updateDurationTime(String(duration), -1000)}>
-                                        <FontAwesomeIcon icon={'caret-down'} />
+                                        <FontAwesomeIcon icon="caret-down" />
                                     </div>
                                 </div>
                             </div>
                             {PresBox.inputter('0.1', '0.1', '20', duration, targetType !== DocumentType.AUDIO, this.updateDurationTime)}
-                            <div className={'slider-headers'} style={{ display: targetType === DocumentType.AUDIO ? 'none' : 'grid' }}>
+                            <div className="slider-headers" style={{ display: targetType === DocumentType.AUDIO ? 'none' : 'grid' }}>
                                 <div className="slider-text">Short</div>
                                 <div className="slider-text">Medium</div>
                                 <div className="slider-text">Long</div>
@@ -1568,17 +1632,18 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                 </div>
             );
         }
+        return undefined;
     }
     @computed get progressivizeDropdown() {
-        const activeItem = this.activeItem;
+        const { activeItem } = this;
         if (activeItem && this.targetDoc) {
             const effect = activeItem.presBulletEffect ? activeItem.presBulletEffect : PresMovement.None;
-            const bulletEffect = (effect: PresEffect) => (
+            const bulletEffect = (presEffect: PresEffect) => (
                 <div
-                    className={`presBox-dropdownOption ${activeItem.presentation_effect === effect || (effect === PresEffect.None && !activeItem.presentation_effect) ? 'active' : ''}`}
+                    className={`presBox-dropdownOption ${activeItem.presentation_effect === presEffect || (presEffect === PresEffect.None && !activeItem.presentation_effect) ? 'active' : ''}`}
                     onPointerDown={StopEvent}
-                    onClick={() => this.updateEffect(effect, true)}>
-                    {effect}
+                    onClick={() => this.updateEffect(presEffect, true)}>
+                    {presEffect}
                 </div>
             );
             return (
@@ -1598,12 +1663,12 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                                 // a progressivized slide doesn't have sub-slides, but rather iterates over the data list of the target being progressivized.
                                 // to avoid creating a new slide to correspond to each of the target's data list, we create a computedField to refernce the target's data list.
                                 let dataField = Doc.LayoutFieldKey(tagDoc);
-                                if (Cast(tagDoc[dataField], listSpec(Doc), null)?.filter(d => d instanceof Doc) === undefined) dataField = dataField + '_annotations';
+                                if (Cast(tagDoc[dataField], listSpec(Doc), null)?.filter(d => d instanceof Doc) === undefined) dataField += '_annotations';
 
                                 if (DocCast(activeItem.presentation_targetDoc).annotationOn) activeItem.data = ComputedField.MakeFunction(`this.presentation_targetDoc.annotationOn?.["${dataField}"]`);
                                 else activeItem.data = ComputedField.MakeFunction(`this.presentation_targetDoc?.["${dataField}"]`);
                             }}
-                            checked={Cast(activeItem.presentation_indexed, 'number', null) !== undefined ? true : false}
+                            checked={Cast(activeItem.presentation_indexed, 'number', null) !== undefined}
                         />
                     </div>
                     <div className="ribbon-doubleButton" style={{ display: 'inline-flex' }}>
@@ -1612,7 +1677,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                             className="presBox-checkbox"
                             style={{ margin: 10, border: `solid 1px ${SnappingManager.userColor}` }}
                             type="checkbox"
-                            onChange={() => (activeItem.presentation_indexedStart = activeItem.presentation_indexedStart ? 0 : 1)}
+                            onChange={() => {
+                                activeItem.presentation_indexedStart = activeItem.presentation_indexedStart ? 0 : 1;
+                            }}
                             checked={!NumCast(activeItem.presentation_indexedStart)}
                         />
                     </div>
@@ -1622,7 +1689,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                             className="presBox-checkbox"
                             style={{ margin: 10, border: `solid 1px ${SnappingManager.userColor}` }}
                             type="checkbox"
-                            onChange={() => (activeItem.presBulletExpand = !activeItem.presBulletExpand)}
+                            onChange={() => {
+                                activeItem.presBulletExpand = !activeItem.presBulletExpand;
+                            }}
                             checked={BoolCast(activeItem.presBulletExpand)}
                         />
                     </div>
@@ -1642,14 +1711,14 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                                 border: this._openBulletEffectDropdown ? `solid 2px ${SnappingManager.userVariantColor}` : `solid 1px ${SnappingManager.userColor}`,
                             }}>
                             {effect?.toString()}
-                            <FontAwesomeIcon className="presBox-dropdownIcon" style={{ gridColumn: 2, color: this._openBulletEffectDropdown ? Colors.MEDIUM_BLUE : 'black' }} icon={'angle-down'} />
+                            <FontAwesomeIcon className="presBox-dropdownIcon" style={{ gridColumn: 2, color: this._openBulletEffectDropdown ? Colors.MEDIUM_BLUE : 'black' }} icon="angle-down" />
                             <div
-                                className={'presBox-dropdownOptions'}
+                                className="presBox-dropdownOptions"
                                 style={{ display: this._openBulletEffectDropdown ? 'grid' : 'none', color: SnappingManager.userColor, background: SnappingManager.userBackgroundColor }}
                                 onPointerDown={e => e.stopPropagation()}>
                                 {Object.values(PresEffect)
                                     .filter(v => isNaN(Number(v)))
-                                    .map(effect => bulletEffect(effect))}
+                                    .map(peffect => bulletEffect(peffect))}
                             </div>
                         </div>
                     </div>
@@ -1659,7 +1728,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         return null;
     }
     @computed get transitionDropdown() {
-        const activeItem = this.activeItem;
+        const { activeItem } = this;
         const preseEffect = (effect: PresEffect) => (
             <div
                 className={`presBox-dropdownOption ${activeItem.presentation_effect === effect || (effect === PresEffect.None && !activeItem.presentation_effect) ? 'active' : ''}`}
@@ -1715,10 +1784,10 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                                 border: this._openMovementDropdown ? `solid 2px ${SnappingManager.userVariantColor}` : `solid 1px ${SnappingManager.userColor}`,
                             }}>
                             {this.movementName(activeItem)}
-                            <FontAwesomeIcon className="presBox-dropdownIcon" style={{ gridColumn: 2, color: this._openMovementDropdown ? Colors.MEDIUM_BLUE : 'black' }} icon={'angle-down'} />
+                            <FontAwesomeIcon className="presBox-dropdownIcon" style={{ gridColumn: 2, color: this._openMovementDropdown ? Colors.MEDIUM_BLUE : 'black' }} icon="angle-down" />
                             <div
                                 className="presBox-dropdownOptions"
-                                id={'presBoxMovementDropdown'}
+                                id="presBoxMovementDropdown"
                                 onPointerDown={StopEvent}
                                 style={{
                                     color: SnappingManager.userColor,
@@ -1739,10 +1808,10 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                             </div>
                             <div className="ribbon-propertyUpDown" style={{ color: SnappingManager.userBackgroundColor, background: SnappingManager.userColor }}>
                                 <div className="ribbon-propertyUpDownItem" onClick={() => this.updateZoom(String(zoom), 0.1)}>
-                                    <FontAwesomeIcon icon={'caret-up'} />
+                                    <FontAwesomeIcon icon="caret-up" />
                                 </div>
                                 <div className="ribbon-propertyUpDownItem" onClick={() => this.updateZoom(String(zoom), -0.1)}>
-                                    <FontAwesomeIcon icon={'caret-down'} />
+                                    <FontAwesomeIcon icon="caret-down" />
                                 </div>
                             </div>
                         </div>
@@ -1750,19 +1819,19 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                         <div className="ribbon-doubleButton" style={{ display: 'inline-flex' }}>
                             <div className="presBox-subheading">Transition Time</div>
                             <div className="ribbon-property" style={{ border: `solid 1px ${SnappingManager.userColor}` }}>
-                                <input className="presBox-input" type="number" readOnly={true} value={transitionSpeed} onKeyDown={e => e.stopPropagation()} onChange={action(e => this.updateTransitionTime(e.target.value))} /> s
+                                <input className="presBox-input" type="number" readOnly value={transitionSpeed} onKeyDown={e => e.stopPropagation()} onChange={action(e => this.updateTransitionTime(e.target.value))} /> s
                             </div>
                             <div className="ribbon-propertyUpDown" style={{ color: SnappingManager.userBackgroundColor, background: SnappingManager.userColor }}>
                                 <div className="ribbon-propertyUpDownItem" onClick={() => this.updateTransitionTime(String(transitionSpeed), 1000)}>
-                                    <FontAwesomeIcon icon={'caret-up'} />
+                                    <FontAwesomeIcon icon="caret-up" />
                                 </div>
                                 <div className="ribbon-propertyUpDownItem" onClick={() => this.updateTransitionTime(String(transitionSpeed), -1000)}>
-                                    <FontAwesomeIcon icon={'caret-down'} />
+                                    <FontAwesomeIcon icon="caret-down" />
                                 </div>
                             </div>
                         </div>
                         {PresBox.inputter('0.1', '0.1', '100', transitionSpeed, true, this.updateTransitionTime)}
-                        <div className={'slider-headers'}>
+                        <div className="slider-headers">
                             <div className="slider-text">Fast</div>
                             <div className="slider-text">Medium</div>
                             <div className="slider-text">Slow</div>
@@ -1776,7 +1845,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                                 className="presBox-checkbox"
                                 style={{ margin: 10, border: `solid 1px ${SnappingManager.userColor}` }}
                                 type="checkbox"
-                                onChange={() => (activeItem.presPlayAudio = !BoolCast(activeItem.presPlayAudio))}
+                                onChange={() => {
+                                    activeItem.presPlayAudio = !BoolCast(activeItem.presPlayAudio);
+                                }}
                                 checked={BoolCast(activeItem.presPlayAudio)}
                             />
                         </div>
@@ -1786,7 +1857,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                                 className="presBox-checkbox"
                                 style={{ margin: 10, border: `solid 1px ${SnappingManager.userColor}` }}
                                 type="checkbox"
-                                onChange={() => (activeItem.presentation_zoomText = !BoolCast(activeItem.presentation_zoomText))}
+                                onChange={() => {
+                                    activeItem.presentation_zoomText = !BoolCast(activeItem.presentation_zoomText);
+                                }}
                                 checked={BoolCast(activeItem.presentation_zoomText)}
                             />
                         </div>
@@ -1800,13 +1873,13 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                                 color: SnappingManager.userColor,
                                 background: SnappingManager.userVariantColor,
                                 borderBottomLeftRadius: this._openEffectDropdown ? 0 : 5,
-                                border: this._openEffectDropdown ? `solid 2px ${SettingsSnappingManagerManager.userVariantColor}` : `solid 1px ${SnappingManager.userColor}`,
+                                border: this._openEffectDropdown ? `solid 2px ${SnappingManager.userVariantColor}` : `solid 1px ${SnappingManager.userColor}`,
                             }}>
                             {effect?.toString()}
-                            <FontAwesomeIcon className="presBox-dropdownIcon" style={{ gridColumn: 2, color: this._openEffectDropdown ? Colors.MEDIUM_BLUE : 'black' }} icon={'angle-down'} />
+                            <FontAwesomeIcon className="presBox-dropdownIcon" style={{ gridColumn: 2, color: this._openEffectDropdown ? Colors.MEDIUM_BLUE : 'black' }} icon="angle-down" />
                             <div
                                 className="presBox-dropdownOptions"
-                                id={'presBoxMovementDropdown'}
+                                id="presBoxMovementDropdown"
                                 style={{
                                     color: SnappingManager.userColor,
                                     background: SnappingManager.userBackgroundColor,
@@ -1815,7 +1888,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                                 onPointerDown={e => e.stopPropagation()}>
                                 {Object.values(PresEffect)
                                     .filter(v => isNaN(Number(v)))
-                                    .map(effect => preseEffect(effect))}
+                                    .map(presEffect => preseEffect(presEffect))}
                             </div>
                         </div>
                         <div className="ribbon-doubleButton" style={{ display: effect === PresEffectDirection.None ? 'none' : 'inline-flex' }}>
@@ -1840,20 +1913,21 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                 </div>
             );
         }
+        return undefined;
     }
     @computed get mediaOptionsDropdown() {
-        const activeItem = this.activeItem;
+        const { activeItem } = this;
         if (activeItem && this.targetDoc) {
             const renderTarget = PresBox.targetRenderedDoc(this.activeItem);
             const clipStart = NumCast(renderTarget.clipStart);
             const clipEnd = NumCast(renderTarget.clipEnd, clipStart + NumCast(renderTarget[Doc.LayoutFieldKey(renderTarget) + '_duration']));
-            const config_clipEnd = NumCast(activeItem.config_clipEnd) < NumCast(activeItem.config_clipStart) ? clipEnd - clipStart : NumCast(activeItem.config_clipEnd);
+            const configClipEnd = NumCast(activeItem.config_clipEnd) < NumCast(activeItem.config_clipStart) ? clipEnd - clipStart : NumCast(activeItem.config_clipEnd);
             return (
-                <div className={'presBox-ribbon'} onClick={e => e.stopPropagation()} onPointerUp={e => e.stopPropagation()} onPointerDown={e => e.stopPropagation()}>
+                <div className="presBox-ribbon" onClick={e => e.stopPropagation()} onPointerUp={e => e.stopPropagation()} onPointerDown={e => e.stopPropagation()}>
                     <div>
                         <div className="ribbon-box">
-                            Start {'&'} End Time
-                            <div className={'slider-headers'}>
+                            Start & End Time
+                            <div className="slider-headers">
                                 <div className="slider-block">
                                     <div className="slider-text" style={{ fontWeight: 500 }}>
                                         Start time (s)
@@ -1863,10 +1937,12 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                                             className="presBox-input"
                                             style={{ textAlign: 'center', width: '100%', height: 15, fontSize: 10 }}
                                             type="number"
-                                            readOnly={true}
+                                            readOnly
                                             value={NumCast(activeItem.config_clipStart).toFixed(2)}
                                             onKeyDown={e => e.stopPropagation()}
-                                            onChange={action(e => (activeItem.config_clipStart = Number(e.target.value)))}
+                                            onChange={action(e => {
+                                                activeItem.config_clipStart = Number(e.target.value);
+                                            })}
                                         />
                                     </div>
                                 </div>
@@ -1875,7 +1951,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                                         Duration (s)
                                     </div>
                                     <div className="slider-number" style={{ color: SnappingManager.userColor, backgroundColor: SnappingManager.userBackgroundColor }}>
-                                        {Math.round((config_clipEnd - NumCast(activeItem.config_clipStart)) * 10) / 10}
+                                        {Math.round((configClipEnd - NumCast(activeItem.config_clipStart)) * 10) / 10}
                                     </div>
                                 </div>
                                 <div className="slider-block">
@@ -1888,9 +1964,11 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                                             onKeyDown={e => e.stopPropagation()}
                                             style={{ textAlign: 'center', width: '100%', height: 15, fontSize: 10 }}
                                             type="number"
-                                            readOnly={true}
-                                            value={config_clipEnd.toFixed(2)}
-                                            onChange={action(e => (activeItem.config_clipEnd = Number(e.target.value)))}
+                                            readOnly
+                                            value={configClipEnd.toFixed(2)}
+                                            onChange={action(e => {
+                                                activeItem.config_clipEnd = Number(e.target.value);
+                                            })}
                                         />
                                     </div>
                                 </div>
@@ -1901,7 +1979,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                                     step="0.1"
                                     min={clipStart}
                                     max={clipEnd}
-                                    value={config_clipEnd}
+                                    value={configClipEnd}
                                     style={{ gridColumn: 1, gridRow: 1, background: SnappingManager.userColor, color: SnappingManager.userVariantColor }}
                                     className={`toolbar-slider ${'end'}`}
                                     id="toolbar-slider"
@@ -1909,7 +1987,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                                         this._batch = UndoManager.StartBatch('config_clipEnd');
                                         const endBlock = document.getElementById('endTime');
                                         if (endBlock) {
-                                            endBlock.style.backgroundColor = SnappingManager.userVariantColor;
+                                            endBlock.style.backgroundColor = SnappingManager.userVariantColor ?? '';
                                         }
                                         e.stopPropagation();
                                     }}
@@ -1917,7 +1995,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                                         this._batch?.end();
                                         const endBlock = document.getElementById('endTime');
                                         if (endBlock) {
-                                            endBlock.style.backgroundColor = SnappingManager.userBackgroundColor;
+                                            endBlock.style.backgroundColor = SnappingManager.userBackgroundColor ?? '';
                                         }
                                     }}
                                     onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
@@ -1938,7 +2016,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                                         this._batch = UndoManager.StartBatch('config_clipStart');
                                         const startBlock = document.getElementById('startTime');
                                         if (startBlock) {
-                                            startBlock.style.backgroundColor = SnappingManager.userVariantColor;
+                                            startBlock.style.backgroundColor = SnappingManager.userVariantColor ?? '';
                                         }
                                         e.stopPropagation();
                                     }}
@@ -1946,7 +2024,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                                         this._batch?.end();
                                         const startBlock = document.getElementById('startTime');
                                         if (startBlock) {
-                                            startBlock.style.backgroundColor = SnappingManager.userBackgroundColor;
+                                            startBlock.style.backgroundColor = SnappingManager.userBackgroundColor ?? '';
                                         }
                                     }}
                                     onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
@@ -1957,7 +2035,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                             </div>
                             <div className="slider-headers">
                                 <div className="slider-text">{clipStart.toFixed(2)} s</div>
-                                <div className="slider-text"></div>
+                                <div className="slider-text" />
                                 <div className="slider-text">{clipEnd.toFixed(2)} s</div>
                             </div>
                         </div>
@@ -1970,7 +2048,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                                         className="presBox-checkbox"
                                         type="checkbox"
                                         style={{ border: `solid 1px ${SnappingManager.userColor}` }}
-                                        onChange={() => (activeItem.presentation_mediaStart = 'manual')}
+                                        onChange={() => {
+                                            activeItem.presentation_mediaStart = 'manual';
+                                        }}
                                         checked={activeItem.presentation_mediaStart === 'manual'}
                                     />
                                     <div>On click</div>
@@ -1980,7 +2060,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                                         className="presBox-checkbox"
                                         style={{ border: `solid 1px ${SnappingManager.userColor}` }}
                                         type="checkbox"
-                                        onChange={() => (activeItem.presentation_mediaStart = 'auto')}
+                                        onChange={() => {
+                                            activeItem.presentation_mediaStart = 'auto';
+                                        }}
                                         checked={activeItem.presentation_mediaStart === 'auto'}
                                     />
                                     <div>Automatically</div>
@@ -1993,7 +2075,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                                         className="presBox-checkbox"
                                         type="checkbox"
                                         style={{ border: `solid 1px ${SnappingManager.userColor}` }}
-                                        onChange={() => (activeItem.presentation_mediaStop = 'manual')}
+                                        onChange={() => {
+                                            activeItem.presentation_mediaStop = 'manual';
+                                        }}
                                         checked={activeItem.presentation_mediaStop === 'manual'}
                                     />
                                     <div>At media end time</div>
@@ -2003,7 +2087,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                                         className="presBox-checkbox"
                                         type="checkbox"
                                         style={{ border: `solid 1px ${SnappingManager.userColor}` }}
-                                        onChange={() => (activeItem.presentation_mediaStop = 'auto')}
+                                        onChange={() => {
+                                            activeItem.presentation_mediaStop = 'auto';
+                                        }}
                                         checked={activeItem.presentation_mediaStop === 'auto'}
                                     />
                                     <div>On slide change</div>
@@ -2031,6 +2117,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                 </div>
             );
         }
+        return undefined;
     }
     @computed get newDocumentToolbarDropdown() {
         return (
@@ -2094,9 +2181,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
 
     @computed get newDocumentDropdown() {
         return (
-            <div className={'presBox-ribbon'} onClick={e => e.stopPropagation()} onPointerDown={e => e.stopPropagation()}>
+            <div className="presBox-ribbon" onClick={e => e.stopPropagation()} onPointerDown={e => e.stopPropagation()}>
                 <div className="ribbon-box">
-                    Slide Title: <br></br>
+                    Slide Title: <br />
                     <input
                         className="ribbon-textInput"
                         placeholder="..."
@@ -2105,16 +2192,31 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                         onChange={e => {
                             e.stopPropagation();
                             e.preventDefault();
-                            runInAction(() => (this.title = e.target.value));
-                        }}></input>
+                            runInAction(() => {
+                                this.title = e.target.value;
+                            });
+                        }}
+                    />
                 </div>
                 <div className="ribbon-box">
                     Choose type:
                     <div className="ribbon-doubleButton">
-                        <div title="Text" className={'ribbon-toggle'} style={{ background: this.addFreeform ? '' : Colors.LIGHT_BLUE }} onClick={action(() => (this.addFreeform = !this.addFreeform))}>
+                        <div
+                            title="Text"
+                            className="ribbon-toggle"
+                            style={{ background: this.addFreeform ? '' : Colors.LIGHT_BLUE }}
+                            onClick={action(() => {
+                                this.addFreeform = !this.addFreeform;
+                            })}>
                             Text
                         </div>
-                        <div title="Freeform" className={'ribbon-toggle'} style={{ background: this.addFreeform ? Colors.LIGHT_BLUE : '' }} onClick={action(() => (this.addFreeform = !this.addFreeform))}>
+                        <div
+                            title="Freeform"
+                            className="ribbon-toggle"
+                            style={{ background: this.addFreeform ? Colors.LIGHT_BLUE : '' }}
+                            onClick={action(() => {
+                                this.addFreeform = !this.addFreeform;
+                            })}>
                             Freeform
                         </div>
                     </div>
@@ -2122,23 +2224,49 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                 <div className="ribbon-box" style={{ display: this.addFreeform ? 'grid' : 'none' }}>
                     Preset layouts:
                     <div className="layout-container" style={{ height: this.openLayouts ? 'max-content' : '75px' }}>
-                        <div className="layout" style={{ border: this.layout === 'blank' ? `solid 2px ${Colors.MEDIUM_BLUE}` : '' }} onClick={action(() => (this.layout = 'blank'))} />
-                        <div className="layout" style={{ border: this.layout === 'title' ? `solid 2px ${Colors.MEDIUM_BLUE}` : '' }} onClick={action(() => (this.layout = 'title'))}>
+                        <div
+                            className="layout"
+                            style={{ border: this.layout === 'blank' ? `solid 2px ${Colors.MEDIUM_BLUE}` : '' }}
+                            onClick={action(() => {
+                                this.layout = 'blank';
+                            })}
+                        />
+                        <div
+                            className="layout"
+                            style={{ border: this.layout === 'title' ? `solid 2px ${Colors.MEDIUM_BLUE}` : '' }}
+                            onClick={action(() => {
+                                this.layout = 'title';
+                            })}>
                             <div className="title">Title</div>
                             <div className="subtitle">Subtitle</div>
                         </div>
-                        <div className="layout" style={{ border: this.layout === 'header' ? `solid 2px ${Colors.MEDIUM_BLUE}` : '' }} onClick={action(() => (this.layout = 'header'))}>
+                        <div
+                            className="layout"
+                            style={{ border: this.layout === 'header' ? `solid 2px ${Colors.MEDIUM_BLUE}` : '' }}
+                            onClick={action(() => {
+                                this.layout = 'header';
+                            })}>
                             <div className="title" style={{ alignSelf: 'center', fontSize: 10 }}>
                                 Section header
                             </div>
                         </div>
-                        <div className="layout" style={{ border: this.layout === 'content' ? `solid 2px ${Colors.MEDIUM_BLUE}` : '' }} onClick={action(() => (this.layout = 'content'))}>
+                        <div
+                            className="layout"
+                            style={{ border: this.layout === 'content' ? `solid 2px ${Colors.MEDIUM_BLUE}` : '' }}
+                            onClick={action(() => {
+                                this.layout = 'content';
+                            })}>
                             <div className="title" style={{ alignSelf: 'center' }}>
                                 Title
                             </div>
                             <div className="content">Text goes here</div>
                         </div>
-                        <div className="layout" style={{ border: this.layout === 'twoColumns' ? `solid 2px ${Colors.MEDIUM_BLUE}` : '' }} onClick={action(() => (this.layout = 'twoColumns'))}>
+                        <div
+                            className="layout"
+                            style={{ border: this.layout === 'twoColumns' ? `solid 2px ${Colors.MEDIUM_BLUE}` : '' }}
+                            onClick={action(() => {
+                                this.layout = 'twoColumns';
+                            })}>
                             <div className="title" style={{ alignSelf: 'center', gridColumn: '1/3' }}>
                                 Title
                             </div>
@@ -2150,8 +2278,12 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                             </div>
                         </div>
                     </div>
-                    <div className="open-layout" onClick={action(() => (this.openLayouts = !this.openLayouts))}>
-                        <FontAwesomeIcon style={{ transition: 'all 0.3s', transform: this.openLayouts ? 'rotate(180deg)' : 'rotate(0deg)' }} icon={'caret-down'} size={'lg'} />
+                    <div
+                        className="open-layout"
+                        onClick={action(() => {
+                            this.openLayouts = !this.openLayouts;
+                        })}>
+                        <FontAwesomeIcon style={{ transition: 'all 0.3s', transform: this.openLayouts ? 'rotate(180deg)' : 'rotate(0deg)' }} icon="caret-down" size="lg" />
                     </div>
                 </div>
                 <div className="ribbon-final-box">
@@ -2166,17 +2298,17 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
     }
 
     createNewSlide = (layout?: string, title?: string, freeform?: boolean) => {
-        let doc = undefined;
+        let doc;
         if (layout) doc = this.createTemplate(layout);
         if (freeform && layout) doc = this.createTemplate(layout, title);
         if (!freeform && !layout) doc = Docs.Create.TextDocument('', { _nativeWidth: 400, _width: 225, title: title });
         if (doc) {
             const tabMap = CollectionDockingView.Instance?.tabMap;
-            const tab = tabMap && Array.from(tabMap).find(tab => tab.DashDoc.type === DocumentType.COL)?.DashDoc;
-            const presCollection = DocumentManager.GetContextPath(this.activeItem).reverse().lastElement().presentation_targetDoc ?? tab;
+            const docTab = tabMap && Array.from(tabMap).find(tab => tab.DashDoc.type === DocumentType.COL)?.DashDoc;
+            const presCollection = DocumentManager.GetContextPath(this.activeItem).reverse().lastElement().presentation_targetDoc ?? docTab;
             const data = Cast(presCollection?.data, listSpec(Doc));
-            const config_data = Cast(this.Document.data, listSpec(Doc));
-            if (data && config_data) {
+            const configData = Cast(this.Document.data, listSpec(Doc));
+            if (data && configData) {
                 data.push(doc);
                 this._props.pinToPres(doc, {});
                 this.gotoDocument(this.childDocs.length, this.activeItem);
@@ -2198,12 +2330,14 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         const content2 = () => Docs.Create.TextDocument('Click to change text', { title: 'Column 2', _width: 185, _height: 140, x: 205, y: 80, _text_fontSize: '14pt' });
         // prettier-ignore
         switch (layout) {
-            case 'blank': return Docs.Create.FreeformDocument([], { title: input ? input : 'Blank slide', _width: 400, _height: 225, x, y });
-            case 'title': return Docs.Create.FreeformDocument([title(), subtitle()], { title: input ? input : 'Title slide', _width: 400, _height: 225, _freeform_fitContentsToBox: true, x, y });
-            case 'header': return Docs.Create.FreeformDocument([header()], { title: input ? input : 'Section header', _width: 400, _height: 225, _freeform_fitContentsToBox: true, x, y });
-            case 'content': return Docs.Create.FreeformDocument([contentTitle(), content()], { title: input ? input : 'Title and content', _width: 400, _height: 225, _freeform_fitContentsToBox: true, x, y });
-            case 'twoColumns': return Docs.Create.FreeformDocument([contentTitle(), content1(), content2()], { title: input ? input : 'Title and two columns', _width: 400, _height: 225, _freeform_fitContentsToBox: true, x, y })
+            case 'blank': return Docs.Create.FreeformDocument([], { title: input || 'Blank slide', _width: 400, _height: 225, x, y });
+            case 'title': return Docs.Create.FreeformDocument([title(), subtitle()], { title: input || 'Title slide', _width: 400, _height: 225, _freeform_fitContentsToBox: true, x, y });
+            case 'header': return Docs.Create.FreeformDocument([header()], { title: input || 'Section header', _width: 400, _height: 225, _freeform_fitContentsToBox: true, x, y });
+            case 'content': return Docs.Create.FreeformDocument([contentTitle(), content()], { title: input || 'Title and content', _width: 400, _height: 225, _freeform_fitContentsToBox: true, x, y });
+            case 'twoColumns': return Docs.Create.FreeformDocument([contentTitle(), content1(), content2()], { title: input || 'Title and two columns', _width: 400, _height: 225, _freeform_fitContentsToBox: true, x, y })
+            default:
         }
+        return undefined;
     };
 
     // Dropdown that appears when the user wants to begin presenting (either minimize or sidebar view)
@@ -2246,17 +2380,19 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
     }
 
     @action
-    toggleProperties = () => (SnappingManager.Instance.propertiesWidth = SnappingManager.Instance.propertiesWidth > 0 ? 0 : 250);
+    toggleProperties = () => {
+        SettingsManager.Instance.propertiesWidth = SettingsManager.Instance.propertiesWidth > 0 ? 0 : 250;
+    };
 
     @computed get toolbar() {
-        const propIcon = SnappingManager.Instance.propertiesWidth > 0 ? 'angle-double-right' : 'angle-double-left';
-        const propTitle = SnappingManager.Instance.propertiesWidth > 0 ? 'Close Presentation Panel' : 'Open Presentation Panel';
+        const propIcon = SettingsManager.Instance.propertiesWidth > 0 ? 'angle-double-right' : 'angle-double-left';
+        const propTitle = SettingsManager.Instance.propertiesWidth > 0 ? 'Close Presentation Panel' : 'Open Presentation Panel';
         const mode = StrCast(this.Document._type_collection) as CollectionViewType;
         const isMini: boolean = this.toolbarWidth <= 100;
         const activeColor = SnappingManager.userVariantColor;
         const inactiveColor = lightOrDark(SnappingManager.userBackgroundColor) === Colors.WHITE ? Colors.WHITE : SnappingManager.userBackgroundColor;
         return mode === CollectionViewType.Carousel3D || Doc.IsInMyOverlay(this.Document) ? null : (
-            <div id="toolbarContainer" className={'presBox-toolbar'}>
+            <div id="toolbarContainer" className="presBox-toolbar">
                 {/* <Tooltip title={<><div className="dash-tooltip">{"Add new slide"}</div></>}><div className={`toolbar-button ${this.newDocumentTools ? "active" : ""}`} onClick={action(() => this.newDocumentTools = !this.newDocumentTools)}>
                     <FontAwesomeIcon icon={"plus"} />
                     <FontAwesomeIcon className={`dropdown ${this.newDocumentTools ? "active" : ""}`} icon={"angle-down"} />
@@ -2266,7 +2402,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                         style={{ opacity: this.childDocs.length > 1 ? 1 : 0.3, color: this._pathBoolean ? Colors.MEDIUM_BLUE : 'white', width: isMini ? '100%' : undefined }}
                         className="toolbar-button"
                         onClick={this.childDocs.length > 1 ? () => this.togglePath() : undefined}>
-                        <FontAwesomeIcon icon={'exchange-alt'} />
+                        <FontAwesomeIcon icon="exchange-alt" />
                     </div>
                 </Tooltip>
                 {isMini ? null : (
@@ -2274,12 +2410,12 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                         <div className="toolbar-divider" />
                         <Tooltip title={<div className="dash-tooltip">{this._presKeyEvents ? 'Keys are active' : 'Keys are not active - click anywhere on the presentation trail to activate keys'}</div>}>
                             <div className="toolbar-button" style={{ cursor: this._presKeyEvents ? 'default' : 'pointer', position: 'absolute', right: 30, fontSize: 16 }}>
-                                <FontAwesomeIcon className={'toolbar-thumbtack'} icon={'keyboard'} style={{ color: this._presKeyEvents ? activeColor : inactiveColor }} />
+                                <FontAwesomeIcon className="toolbar-thumbtack" icon="keyboard" style={{ color: this._presKeyEvents ? activeColor : inactiveColor }} />
                             </div>
                         </Tooltip>
                         <Tooltip title={<div className="dash-tooltip">{propTitle}</div>}>
                             <div className="toolbar-button" style={{ position: 'absolute', right: 4, fontSize: 16 }} onClick={this.toggleProperties}>
-                                <FontAwesomeIcon className={'toolbar-thumbtack'} icon={propIcon} style={{ color: SnappingManager.Instance.propertiesWidth > 0 ? activeColor : inactiveColor }} />
+                                <FontAwesomeIcon className="toolbar-thumbtack" icon={propIcon} style={{ color: SettingsManager.Instance.propertiesWidth > 0 ? activeColor : inactiveColor }} />
                             </div>
                         </Tooltip>
                     </>
@@ -2324,7 +2460,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                                     this.gotoDocument(this.itemIndex, this.activeItem);
                                 }
                             })}>
-                            <FontAwesomeIcon icon={'play-circle'} />
+                            <FontAwesomeIcon icon="play-circle" />
                             <div style={{ display: this._props.PanelWidth() > 200 ? 'inline-flex' : 'none' }}>&nbsp; Present</div>
                         </div>
                         {mode === CollectionViewType.Carousel3D || isMini ? null : (
@@ -2333,7 +2469,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                                 onClick={action(() => {
                                     if (this.childDocs.length) this._presentTools = !this._presentTools;
                                 })}>
-                                <FontAwesomeIcon className="dropdown" style={{ margin: 0, transform: this._presentTools ? 'rotate(180deg)' : 'rotate(0deg)' }} icon={'angle-down'} />
+                                <FontAwesomeIcon className="dropdown" style={{ margin: 0, transform: this._presentTools ? 'rotate(180deg)' : 'rotate(0deg)' }} icon="angle-down" />
                                 {this.presentDropdown}
                             </div>
                         )}
@@ -2396,7 +2532,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                             false
                         )
                     }>
-                    <FontAwesomeIcon icon={'arrow-left'} />
+                    <FontAwesomeIcon icon="arrow-left" />
                 </div>
                 <Tooltip title={<div className="dash-tooltip">{this.layoutDoc.presentation_status === PresStatus.Autoplay ? 'Pause' : 'Autoplay'}</div>}>
                     <div className="presPanel-button" onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, () => this.startOrPause(true), false, false)}>
@@ -2424,10 +2560,10 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                             false
                         )
                     }>
-                    <FontAwesomeIcon icon={'arrow-right'} />
+                    <FontAwesomeIcon icon="arrow-right" />
                 </div>
-                <div className="presPanel-divider"></div>
-                <Tooltip title={<div className="dash-tooltip">{'Click to return to 1st slide'}</div>}>
+                <div className="presPanel-divider" />
+                <Tooltip title={<div className="dash-tooltip">Click to return to 1st slide</div>}>
                     <div
                         className="presPanel-button"
                         style={{ border: 'solid 1px white' }}
@@ -2451,7 +2587,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                     {inOverlay ? '' : 'Slide'} {this.itemIndex + 1}
                     {this.activeItem?.presentation_indexed !== undefined ? `(${this.activeItem.presentation_indexed}/${this.progressivizedItems(this.activeItem)?.length})` : ''} / {this.childDocs.length}
                 </div>
-                <div className="presPanel-divider"></div>
+                <div className="presPanel-divider" />
                 {this._props.PanelWidth() > 250 ? (
                     <div
                         className="presPanel-button-text"
@@ -2465,7 +2601,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                     </div>
                 ) : (
                     <div className="presPanel-button" onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, this.exitClicked, false, false)}>
-                        <FontAwesomeIcon icon={'times'} />
+                        <FontAwesomeIcon icon="times" />
                     </div>
                 )}
             </div>
@@ -2480,7 +2616,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
     };
 
     @action
-    prevClicked = (e: PointerEvent) => {
+    prevClicked = () => {
         this.back();
         if (this._presTimer) {
             clearTimeout(this._presTimer);
@@ -2489,7 +2625,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
     };
 
     @action
-    nextClicked = (e: PointerEvent) => {
+    nextClicked = () => {
         this.next();
         if (this._presTimer) {
             clearTimeout(this._presTimer);
@@ -2505,7 +2641,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
 
     AddToMap = (treeViewDoc: Doc, index: number[]) => {
         if (!treeViewDoc.presentation_targetDoc) return this.childDocs; // if treeViewDoc is not a pres elements, then it's a sub-bullet of a progressivized slide which isn't added to the linearized list of pres elements since it's not really a pres element.
-        var indexNum = 0;
+        let indexNum = 0;
         for (let i = 0; i < index.length; i++) {
             indexNum += index[i] * 10 ** -i;
         }
@@ -2517,19 +2653,21 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                 this.dataDoc[this.presFieldKey] = new List<Doc>(sorted); // this is a flat array of Docs
             }
         }
+        return undefined;
     };
 
     SlideIndex = (slideDoc: Doc) => DocListCast(this.dataDoc[this.presFieldKey]).indexOf(slideDoc);
 
-    RemFromMap = (treeViewDoc: Doc, index: number[]) => {
+    RemFromMap = (treeViewDoc: Doc) => {
         if (!treeViewDoc.presentation_targetDoc) return this.childDocs; // if treeViewDoc is not a pres elements, then it's a sub-bullet of a progressivized slide which isn't added to the linearized list of pres elements since it's not really a pres element.
         if (!this._unmounting && this.isTree) {
             this._treeViewMap.delete(treeViewDoc);
             this.dataDoc[this.presFieldKey] = new List<Doc>(this.sort(this._treeViewMap));
         }
+        return undefined;
     };
 
-    sort = (treeView_Map: Map<Doc, number>) => [...treeView_Map.entries()].sort((a: [Doc, number], b: [Doc, number]) => (a[1] > b[1] ? 1 : a[1] < b[1] ? -1 : 0)).map(kv => kv[0]);
+    sort = (treeViewMap: Map<Doc, number>) => [...treeViewMap.entries()].sort((a: [Doc, number], b: [Doc, number]) => (a[1] > b[1] ? 1 : a[1] < b[1] ? -1 : 0)).map(kv => kv[0]);
 
     render() {
         // needed to ensure that the childDocs are loaded for looking up fields
@@ -2541,21 +2679,38 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
             (this.activeItem.presentation_indexed === undefined || NumCast(this.activeItem.presentation_indexed) === (this.progressivizedItems(this.activeItem)?.length ?? 0));
         const presStart = !this.layoutDoc.presLoop && this.itemIndex === 0;
         return this._props.addDocTab === returnFalse ? ( // bcz: hack!! - addDocTab === returnFalse only when this is being rendered by the OverlayView which means the doc is a mini player
-            <div className="miniPres" onClick={e => e.stopPropagation()} onPointerEnter={action(e => (this._forceKeyEvents = true))}>
+            <div
+                className="miniPres"
+                onClick={e => e.stopPropagation()}
+                onPointerEnter={action(() => {
+                    this._forceKeyEvents = true;
+                })}>
                 <div
                     className="presPanelOverlay"
                     style={{ display: 'inline-flex', height: 30, background: Doc.ActivePresentation === this.Document ? 'green' : '#323232', top: 0, zIndex: 3000000, boxShadow: this._presKeyEvents ? '0 0 0px 3px ' + Colors.MEDIUM_BLUE : undefined }}>
-                    <Tooltip title={<div className="dash-tooltip">{'Loop'}</div>}>
+                    <Tooltip title={<div className="dash-tooltip">Loop</div>}>
                         <div
                             className="presPanel-button"
                             style={{ color: this.layoutDoc.presLoop ? Colors.MEDIUM_BLUE : undefined }}
-                            onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, returnFalse, () => (this.layoutDoc.presLoop = !this.layoutDoc.presLoop), false, false)}>
-                            <FontAwesomeIcon icon={'redo-alt'} />
+                            onPointerDown={e =>
+                                setupMoveUpEvents(
+                                    this,
+                                    e,
+                                    returnFalse,
+                                    returnFalse,
+                                    () => {
+                                        this.layoutDoc.presLoop = !this.layoutDoc.presLoop;
+                                    },
+                                    false,
+                                    false
+                                )
+                            }>
+                            <FontAwesomeIcon icon="redo-alt" />
                         </div>
                     </Tooltip>
-                    <div className="presPanel-divider"></div>
+                    <div className="presPanel-divider" />
                     <div className="presPanel-button" style={{ opacity: presStart ? 0.4 : 1 }} onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, returnFalse, this.prevClicked, false, false)}>
-                        <FontAwesomeIcon icon={'arrow-left'} />
+                        <FontAwesomeIcon icon="arrow-left" />
                     </div>
                     <Tooltip title={<div className="dash-tooltip">{this.layoutDoc.presentation_status === PresStatus.Autoplay ? 'Pause' : 'Autoplay'}</div>}>
                         <div className="presPanel-button" onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, returnFalse, () => this.startOrPause(true), false, false)}>
@@ -2563,10 +2718,10 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                         </div>
                     </Tooltip>
                     <div className="presPanel-button" style={{ opacity: presEnd ? 0.4 : 1 }} onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, returnFalse, this.nextClicked, false, false)}>
-                        <FontAwesomeIcon icon={'arrow-right'} />
+                        <FontAwesomeIcon icon="arrow-right" />
                     </div>
-                    <div className="presPanel-divider"></div>
-                    <Tooltip title={<div className="dash-tooltip">{'Click to return to 1st slide'}</div>}>
+                    <div className="presPanel-divider" />
+                    <Tooltip title={<div className="dash-tooltip">Click to return to 1st slide</div>}>
                         <div className="presPanel-button" style={{ border: 'solid 1px white' }} onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, returnFalse, () => this.gotoDocument(0, this.activeItem), false, false)}>
                             <b>1</b>
                         </div>
@@ -2590,15 +2745,16 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                     <div className="Slide">
                         {mode !== CollectionViewType.Invalid ? (
                             <CollectionView
+                                // eslint-disable-next-line react/jsx-props-no-spreading
                                 {...this._props}
                                 PanelWidth={this._props.PanelWidth}
                                 PanelHeight={this.panelHeight}
-                                childIgnoreNativeSize={true}
+                                childIgnoreNativeSize
                                 moveDocument={returnFalse}
-                                ignoreUnrendered={true}
+                                ignoreUnrendered
                                 childDragAction={dropActionType.move}
                                 setContentViewBox={emptyFunction}
-                                //childLayoutFitWidth={returnTrue}
+                                // childLayoutFitWidth={returnTrue}
                                 childOpacity={returnOne}
                                 childClickScript={PresBox.navigateToDocScript}
                                 childLayoutTemplate={this.childLayoutTemplate}
@@ -2629,6 +2785,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
     }
 }
 
+// eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function navigateToDoc(bestTarget: Doc, activeItem: Doc) {
     PresBox.NavigateToTarget(bestTarget, activeItem);
 });
diff --git a/src/client/views/nodes/trails/PresElementBox.tsx b/src/client/views/nodes/trails/PresElementBox.tsx
index fca5a2770..5fa32ad12 100644
--- a/src/client/views/nodes/trails/PresElementBox.tsx
+++ b/src/client/views/nodes/trails/PresElementBox.tsx
@@ -1,14 +1,16 @@
+/* eslint-disable jsx-a11y/no-static-element-interactions */
+/* eslint-disable jsx-a11y/click-events-have-key-events */
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { Tooltip } from '@mui/material';
 import { action, computed, IReactionDisposer, makeObservable, observable, reaction, runInAction } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
+import { returnEmptyDoclist, returnFalse, returnTrue, setupMoveUpEvents } from '../../../../ClientUtils';
 import { Doc, DocListCast, Opt } from '../../../../fields/Doc';
 import { Id } from '../../../../fields/FieldSymbols';
 import { List } from '../../../../fields/List';
 import { BoolCast, Cast, DocCast, NumCast, StrCast } from '../../../../fields/Types';
 import { emptyFunction } from '../../../../Utils';
-import { returnEmptyDoclist, returnFalse, returnTrue, setupMoveUpEvents } from '../../../../ClientUtils';
 import { Docs } from '../../../documents/Documents';
 import { CollectionViewType } from '../../../documents/DocumentTypes';
 import { DocumentManager } from '../../../util/DocumentManager';
@@ -20,9 +22,9 @@ import { TreeView } from '../../collections/TreeView';
 import { ViewBoxBaseComponent } from '../../DocComponent';
 import { EditableView } from '../../EditableView';
 import { Colors } from '../../global/globalEnums';
-import { DocumentView } from '../../nodes/DocumentView';
-import { FieldView, FieldViewProps } from '../../nodes/FieldView';
 import { StyleProp } from '../../StyleProvider';
+import { DocumentView } from '../DocumentView';
+import { FieldView, FieldViewProps } from '../FieldView';
 import { PresBox } from './PresBox';
 import './PresElementBox.scss';
 import { PresMovement } from './PresEnums';
@@ -72,7 +74,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
 
     // computes index of this presentation slide in the presBox list
     @computed get indexInPres() {
-        return this.presBoxView?.SlideIndex(this.slideDoc);
+        return this.presBoxView?.SlideIndex(this.slideDoc) ?? 0;
     }
 
     @computed get selectedArray() {
@@ -87,7 +89,9 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
         this.layoutDoc.layout_hideLinkButton = true;
         this._heightDisposer = reaction(
             () => ({ expand: this.slideDoc.presentation_expandInlineButton, height: this.collapsedHeight }),
-            ({ expand, height }) => (this.layoutDoc._height = height + (expand ? this.expandViewHeight : 0)),
+            ({ expand, height }) => {
+                this.layoutDoc._height = height + (expand ? this.expandViewHeight : 0);
+            },
             { fireImmediately: true }
         );
     }
@@ -95,12 +99,14 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
         this._heightDisposer?.();
     }
 
-    presExpandDocumentClick = () => (this.slideDoc.presentation_expandInlineButton = !this.slideDoc.presentation_expandInlineButton);
+    presExpandDocumentClick = () => {
+        this.slideDoc.presentation_expandInlineButton = !this.slideDoc.presentation_expandInlineButton;
+    };
     embedHeight = () => this.collapsedHeight + this.expandViewHeight;
     embedWidth = () => this._props.PanelWidth() / 2;
-    styleProvider = (doc: Doc | undefined, props: Opt<FieldViewProps>, property: string): any => {
-        return property === StyleProp.Opacity ? 1 : this._props.styleProvider?.(doc, props, property);
-    };
+    // prettier-ignore
+    styleProvider = ( doc: Doc | undefined, props: Opt<FieldViewProps>, property: string ): any => 
+        (property === StyleProp.Opacity ? 1 : this._props.styleProvider?.(doc, props, property));
     /**
      * The function that is responsible for rendering a preview or not for this
      * presentation element.
@@ -114,7 +120,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
                     PanelHeight={this.embedHeight}
                     isContentActive={this._props.isContentActive}
                     styleProvider={this.styleProvider}
-                    hideLinkButton={true}
+                    hideLinkButton
                     ScreenToLocalTransform={Transform.Identity}
                     renderDepth={this._props.renderDepth + 1}
                     containerViewPath={returnEmptyDoclist}
@@ -151,7 +157,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
                         ref={this._titleRef}
                         editing={undefined}
                         contents={doc.title}
-                        overflow={'ellipsis'}
+                        overflow="ellipsis"
                         GetValue={() => StrCast(doc.title)}
                         SetValue={(value: string) => {
                             doc.title = !value.trim().length ? '-untitled-' : value;
@@ -178,10 +184,10 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
         e.preventDefault();
         if (element && !(e.ctrlKey || e.metaKey || e.button === 2)) {
             this.presBoxView?.regularSelect(this.slideDoc, this._itemRef.current!, this._dragRef.current!, true, false);
-            setupMoveUpEvents(this, e, this.startDrag, emptyFunction, e => {
-                e.stopPropagation();
-                e.preventDefault();
-                this.presBoxView?.modifierSelect(this.slideDoc, this._itemRef.current!, this._dragRef.current!, e.shiftKey || e.ctrlKey || e.metaKey, e.ctrlKey || e.metaKey, e.shiftKey);
+            setupMoveUpEvents(this, e, this.startDrag, emptyFunction, clickEv => {
+                clickEv.stopPropagation();
+                clickEv.preventDefault();
+                this.presBoxView?.modifierSelect(this.slideDoc, this._itemRef.current!, this._dragRef.current!, clickEv.shiftKey || clickEv.ctrlKey || clickEv.metaKey, clickEv.ctrlKey || clickEv.metaKey, clickEv.shiftKey);
                 this.presBoxView?.activeItem && this.showRecording(this.presBoxView?.activeItem);
             });
         }
@@ -210,7 +216,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
         } else if (dragArray.length >= 1) {
             const doc = document.createElement('div');
             doc.className = 'presItem-multiDrag';
-            doc.innerText = 'Move ' + this.selectedArray?.size + ' slides';
+            doc.innerText = 'Move ' + (this.selectedArray?.size ?? 0) + ' slides';
             doc.style.position = 'absolute';
             doc.style.top = e.clientY + 'px';
             doc.style.left = e.clientX - 50 + 'px';
@@ -218,7 +224,9 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
         }
 
         if (activeItem) {
-            runInAction(() => (this._dragging = true));
+            runInAction(() => {
+                this._dragging = true;
+            });
             DragManager.StartDocumentDrag(
                 dragItem.map(ele => ele),
                 dragData,
@@ -226,7 +234,10 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
                 e.clientY,
                 undefined,
                 action(() => {
-                    Array.from(classesToRestore).forEach(pair => (pair[0].className = pair[1]));
+                    Array.from(classesToRestore).forEach(pair => {
+                        // eslint-disable-next-line prefer-destructuring
+                        pair[0].className = pair[1];
+                    });
                     this._dragging = false;
                 })
             );
@@ -235,7 +246,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
         return false;
     };
 
-    onPointerOver = (e: any) => {
+    onPointerOver = () => {
         document.removeEventListener('pointermove', this.onPointerMove);
         document.addEventListener('pointermove', this.onPointerMove);
     };
@@ -245,7 +256,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
         const dragIsPresItem = DragManager.docsBeingDragged.some(d => d.presentation_targetDoc);
         if (slide && dragIsPresItem) {
             const rect = slide.getBoundingClientRect();
-            const y = e.clientY - rect.top; //y position within the element.
+            const y = e.clientY - rect.top; // y position within the element.
             const height = slide.clientHeight;
             const halfLine = height / 2;
             if (y <= halfLine) {
@@ -259,7 +270,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
         document.removeEventListener('pointermove', this.onPointerMove);
     };
 
-    onPointerLeave = (e: any) => {
+    onPointerLeave = () => {
         const slide = this._itemRef.current;
         if (slide) {
             slide.style.borderTop = '0px';
@@ -341,7 +352,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
     };
 
     hideRecording = undoable(
-        action((e: React.MouseEvent, iconClick: boolean = false) => {
+        action((e: React.MouseEvent) => {
             e.stopPropagation();
             this.removeAllRecordingInOverlay();
         }),
@@ -396,7 +407,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
     lfg = (e: React.MouseEvent) => {
         e.stopPropagation();
         // TODO: fix this bug
-        const { toggleChildrenRun } = this.slideDoc;
+        // const { toggleChildrenRun } = this.slideDoc;
         TreeView.ToggleChildrenRun.get(this.slideDoc)?.();
 
         // call this.slideDoc.recurChildren() to get all the children
@@ -407,15 +418,13 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
     get toolbarWidth(): number {
         const presBoxDocView = DocumentManager.Instance.getDocumentView(this.presBox);
         const width = NumCast(this.presBox?._width);
-        return presBoxDocView ? presBoxDocView._props.PanelWidth() : width ? width : 300;
+        return presBoxDocView ? presBoxDocView._props.PanelWidth() : width || 300;
     }
 
     @computed get presButtons() {
-        const presBox = this.presBox;
+        const { presBox, targetDoc, slideDoc: activeItem } = this;
         const presBoxColor = StrCast(presBox?._backgroundColor);
         const presColorBool = presBoxColor ? presBoxColor !== Colors.WHITE && presBoxColor !== 'transparent' : false;
-        const targetDoc = this.targetDoc;
-        const activeItem = this.slideDoc;
         const hasChildren = BoolCast(this.slideDoc?.hasChildren);
 
         const items: JSX.Element[] = [];
@@ -442,7 +451,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
         );
         items.push(
             <Tooltip key="slash" title={<div className="dash-tooltip">{this.videoRecordingIsInOverlay ? 'Hide Recording' : `${PresElementBox.videoIsRecorded(activeItem) ? 'Show' : 'Start'} recording`}</div>}>
-                <div className="slideButton" onClick={e => (this.videoRecordingIsInOverlay ? this.hideRecording(e, true) : this.startRecording(e, activeItem))} style={{ fontWeight: 700 }}>
+                <div className="slideButton" onClick={e => (this.videoRecordingIsInOverlay ? this.hideRecording(e) : this.startRecording(e, activeItem))} style={{ fontWeight: 700 }}>
                     <FontAwesomeIcon icon={`video${this.videoRecordingIsInOverlay ? '-slash' : ''}`} onPointerDown={e => e.stopPropagation()} />
                 </div>
             </Tooltip>
@@ -462,7 +471,9 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
                     }>
                     <div
                         className="slideButton"
-                        onClick={() => (activeItem.presentation_groupWithUp = (NumCast(activeItem.presentation_groupWithUp) + 1) % 3)}
+                        onClick={() => {
+                            activeItem.presentation_groupWithUp = (NumCast(activeItem.presentation_groupWithUp) + 1) % 3;
+                        }}
                         style={{
                             zIndex: 1000 - this.indexInPres,
                             fontWeight: 700,
@@ -472,7 +483,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
                             transform: activeItem.presentation_groupWithUp ? 'translate(0, -17px)' : undefined,
                         }}>
                         <div style={{ transform: activeItem.presentation_groupWithUp ? 'rotate(180deg) translate(0, -17.5px)' : 'rotate(0deg)' }}>
-                            <FontAwesomeIcon icon={'arrow-up'} onPointerDown={e => e.stopPropagation()} />
+                            <FontAwesomeIcon icon="arrow-up" onPointerDown={e => e.stopPropagation()} />
                         </div>
                     </div>
                 </Tooltip>
@@ -501,15 +512,15 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
                             this.lfg(e);
                         }}
                         style={{ fontWeight: 700 }}>
-                        <FontAwesomeIcon icon={'circle-play'} onPointerDown={e => e.stopPropagation()} />
+                        <FontAwesomeIcon icon="circle-play" onPointerDown={e => e.stopPropagation()} />
                     </div>
                 </Tooltip>
             );
         }
         items.push(
             <Tooltip key="trash" title={<div className="dash-tooltip">Remove from presentation</div>}>
-                <div className={'slideButton'} onClick={this.removePresentationItem}>
-                    <FontAwesomeIcon icon={'trash'} onPointerDown={e => e.stopPropagation()} />
+                <div className="slideButton" onClick={this.removePresentationItem}>
+                    <FontAwesomeIcon icon="trash" onPointerDown={e => e.stopPropagation()} />
                 </div>
             </Tooltip>
         );
@@ -517,18 +528,17 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
     }
 
     @computed get mainItem() {
-        const isSelected: boolean = this.selectedArray?.has(this.slideDoc) ? true : false;
+        const { presBox, slideDoc: activeItem } = this;
+        const isSelected: boolean = !!this.selectedArray?.has(activeItem);
         const isCurrent: boolean = this.presBox?._itemIndex === this.indexInPres;
         const miniView: boolean = this.toolbarWidth <= 110;
-        const presBox = this.presBox; //presBox
         const presBoxColor: string = StrCast(presBox?._backgroundColor);
         const presColorBool: boolean = presBoxColor ? presBoxColor !== Colors.WHITE && presBoxColor !== 'transparent' : false;
-        const activeItem: Doc = this.slideDoc;
 
         return (
             <div
                 className="presItem-container"
-                key={this.slideDoc[Id] + this.indexInPres}
+                key={activeItem[Id] + this.indexInPres}
                 ref={this._itemRef}
                 style={{
                     backgroundColor: presColorBool ? (isSelected ? 'rgba(250,250,250,0.3)' : 'transparent') : isSelected ? Colors.LIGHT_BLUE : 'transparent',
@@ -538,9 +548,9 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
                     paddingTop: NumCast(this.layoutDoc._yPadding, this._props.yPadding),
                     paddingBottom: NumCast(this.layoutDoc._yPadding, this._props.yPadding),
                 }}
-                onDoubleClick={action(e => {
+                onDoubleClick={action(() => {
                     this.toggleProperties();
-                    this.presBoxView?.regularSelect(this.slideDoc, this._itemRef.current!, this._dragRef.current!, false);
+                    this.presBoxView?.regularSelect(activeItem, this._itemRef.current!, this._dragRef.current!, false);
                 })}
                 onPointerOver={this.onPointerOver}
                 onPointerLeave={this.onPointerLeave}
@@ -552,11 +562,11 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
                 ) : (
                     <div
                         ref={this._dragRef}
-                        className={`presItem-slide ${isCurrent ? 'active' : ''}${this.slideDoc.runProcess ? ' testingv2' : ''}`}
+                        className={`presItem-slide ${isCurrent ? 'active' : ''}${activeItem.runProcess ? ' testingv2' : ''}`}
                         style={{
                             display: 'infline-block',
                             backgroundColor: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BackgroundColor),
-                            //layout_boxShadow: presBoxColor && presBoxColor !== 'white' && presBoxColor !== 'transparent' ? (isCurrent ? '0 0 0px 1.5px' + presBoxColor : undefined) : undefined,
+                            // layout_boxShadow: presBoxColor && presBoxColor !== 'white' && presBoxColor !== 'transparent' ? (isCurrent ? '0 0 0px 1.5px' + presBoxColor : undefined) : undefined,
                             border: presBoxColor && presBoxColor !== 'white' && presBoxColor !== 'transparent' ? (isCurrent ? presBoxColor + ' solid 2.5px' : undefined) : undefined,
                         }}>
                         <div
@@ -564,7 +574,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
                             style={{
                                 display: 'inline-flex',
                                 pointerEvents: isSelected ? undefined : 'none',
-                                width: `calc(100% ${this.slideDoc.presentation_expandInlineButton ? '- 50%' : ''} - ${this.presButtons.length * 22}px`,
+                                width: `calc(100% ${activeItem.presentation_expandInlineButton ? '- 50%' : ''} - ${this.presButtons.length * 22}px`,
                                 cursor: isSelected ? 'text' : 'grab',
                             }}>
                             <div
@@ -577,7 +587,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
                                     }
                                 }}
                                 onClick={e => e.stopPropagation()}>{`${this.indexInPres + 1}.  `}</div>
-                            <EditableView ref={this._titleRef} oneLine={true} editing={!isSelected ? false : undefined} contents={activeItem.title} overflow={'ellipsis'} GetValue={() => StrCast(activeItem.title)} SetValue={this.onSetValue} />
+                            <EditableView ref={this._titleRef} oneLine editing={!isSelected ? false : undefined} contents={activeItem.title} overflow="ellipsis" GetValue={() => StrCast(activeItem.title)} SetValue={this.onSetValue} />
                         </div>
                         {/* <Tooltip title={<><div className="dash-tooltip">{"Movement speed"}</div></>}><div className="presItem-time" style={{ display: showMore ? "block" : "none" }}>{this.transition}</div></Tooltip> */}
                         {/* <Tooltip title={<><div className="dash-tooltip">{"Duration"}</div></>}><div className="presItem-time" style={{ display: showMore ? "block" : "none" }}>{this.duration}</div></Tooltip> */}
diff --git a/src/client/views/pdf/Annotation.tsx b/src/client/views/pdf/Annotation.tsx
index a1f5ce703..053c88e17 100644
--- a/src/client/views/pdf/Annotation.tsx
+++ b/src/client/views/pdf/Annotation.tsx
@@ -62,7 +62,7 @@ class RegionAnnotation extends ObservableReactComponent<IRegionAnnotationProps>
     pinToPres = () => this._props.pinToPres(this.annoTextRegion, {});
 
     @undoBatch
-    makeTargretToggle = () => (this.annoTextRegion.followLinkToggle = !this.annoTextRegion.followLinkToggle);
+    makeTargetToggle = () => { this.annoTextRegion.followLinkToggle = !this.annoTextRegion.followLinkToggle }; // prettier-ignore
 
     isTargetToggler = () => BoolCast(this.annoTextRegion.followLinkToggle);
     @undoBatch
@@ -80,7 +80,7 @@ class RegionAnnotation extends ObservableReactComponent<IRegionAnnotationProps>
         AnchorMenu.Instance.Delete = this.deleteAnnotation.bind(this);
         AnchorMenu.Instance.Pinned = false;
         AnchorMenu.Instance.PinToPres = this.pinToPres;
-        AnchorMenu.Instance.MakeTargetToggle = this.makeTargretToggle;
+        AnchorMenu.Instance.MakeTargetToggle = this.makeTargetToggle;
         AnchorMenu.Instance.IsTargetToggler = this.isTargetToggler;
         AnchorMenu.Instance.ShowTargetTrail = () => this.showTargetTrail(this.annoTextRegion);
         AnchorMenu.Instance.jumpTo(e.clientX, e.clientY, true);
diff --git a/src/client/views/pdf/GPTPopup/GPTPopup.tsx b/src/client/views/pdf/GPTPopup/GPTPopup.tsx
index cd13d4cbc..4362c4fbb 100644
--- a/src/client/views/pdf/GPTPopup/GPTPopup.tsx
+++ b/src/client/views/pdf/GPTPopup/GPTPopup.tsx
@@ -1,3 +1,4 @@
+/* eslint-disable jsx-a11y/label-has-associated-control */
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { Button, IconButton, Type } from 'browndash-components';
 import { action, makeObservable, observable } from 'mobx';
@@ -294,7 +295,7 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> {
     render() {
         return (
             <div className="summary-box" style={{ display: this.visible ? 'flex' : 'none' }}>
-                {this.mode === GPTPopupMode.SUMMARY ? this.summaryBox() : this.mode === GPTPopupMode.IMAGE ? this.imageBox() : <></>}
+                {this.mode === GPTPopupMode.SUMMARY ? this.summaryBox() : this.mode === GPTPopupMode.IMAGE ? this.imageBox() : null}
             </div>
         );
     }
diff --git a/src/client/views/search/SearchBox.tsx b/src/client/views/search/SearchBox.tsx
index af9f05a14..0c1a419aa 100644
--- a/src/client/views/search/SearchBox.tsx
+++ b/src/client/views/search/SearchBox.tsx
@@ -1,3 +1,5 @@
+/* eslint-disable jsx-a11y/no-static-element-interactions */
+/* eslint-disable jsx-a11y/click-events-have-key-events */
 import { Tooltip } from '@mui/material';
 import { action, computed, makeObservable, observable } from 'mobx';
 import { observer } from 'mobx-react';
@@ -40,6 +42,7 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() {
     public static LayoutString(fieldKey: string) {
         return FieldView.LayoutString(SearchBox, fieldKey);
     }
+    // eslint-disable-next-line no-use-before-define
     public static Instance: SearchBox;
 
     private _inputRef = React.createRef<HTMLInputElement>();
@@ -135,20 +138,23 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() {
      * This method iterates asynchronously through an array of docs and all docs within those
      * docs, calling the function func on each doc.
      */
-    static async foreachRecursiveDocAsync(docs: Doc[], func: (depth: number, doc: Doc) => void) {
+    static async foreachRecursiveDocAsync(docsIn: Doc[], func: (depth: number, doc: Doc) => void) {
+        let docs = docsIn;
         let newarray: Doc[] = [];
-        var depth = 0;
+        let depth = 0;
         while (docs.length > 0) {
             newarray = [];
+            // eslint-disable-next-line no-await-in-loop
             await Promise.all(
                 docs
                     .filter(d => d)
+                    // eslint-disable-next-line no-loop-func
                     .map(async d => {
                         const fieldKey = Doc.LayoutFieldKey(d);
                         const annos = !Field.toString(Doc.LayoutField(d) as FieldType).includes('CollectionView');
                         const data = d[annos ? fieldKey + '_annotations' : fieldKey];
-                        const docs = await DocListCastAsync(data);
-                        docs && newarray.push(...docs);
+                        const dataDocs = await DocListCastAsync(data);
+                        dataDocs && newarray.push(...dataDocs);
                         func(depth, d);
                     })
             );
@@ -170,6 +176,7 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() {
             case DocumentType.IMG : return 'Img';
             case DocumentType.RTF : return 'Rtf';
             case DocumentType.COL : return 'Col:'+colType.substring(0,3);
+            default:
         } // prettier-ignore
 
         return type.charAt(0).toUpperCase() + type.substring(1, 3);
@@ -298,35 +305,27 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() {
         if (query) {
             this.searchCollection(query);
             const response = await fetchRecommendations('', query, [], true);
-            const recs = response.recommendations;
+            const recs = response.recommendations as any[];
             const recommendations: IRecommendation[] = [];
-            for (const key in recs) {
-                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;
+            recs.forEach(rec => {
+                const { title, url, type, text, transcript, previewUrl, embedding, distance, source, related_concepts: relatedConcepts, doc_id: docId } = rec;
                 recommendations.push({
-                    title: title,
+                    title,
                     data: url,
-                    type: type,
-                    text: text,
-                    transcript: transcript,
-                    previewUrl: previewUrl,
-                    embedding: embedding,
+                    type,
+                    text,
+                    transcript,
+                    previewUrl,
+                    embedding,
                     distance: Math.round(distance * 100) / 100,
                     source: source,
-                    related_concepts: related_concepts,
-                    docId: docId,
+                    related_concepts: relatedConcepts,
+                    docId,
                 });
-            }
-            const setRecommendations = action(() => (this._recommendations = recommendations));
+            });
+            const setRecommendations = action(() => {
+                this._recommendations = recommendations;
+            });
             setRecommendations();
         }
     };
@@ -375,30 +374,30 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() {
      * This method renders the search input box, select drop-down menu, and search results.
      */
     render() {
-        var validResults = 0;
+        let validResults = 0;
 
         const isLinkSearch: boolean = this._props.linkSearch;
 
         const sortedResults = Array.from(this._results.entries()).sort((a, b) => (this._pageRanks.get(b[0]) ?? 0) - (this._pageRanks.get(a[0]) ?? 0)); // sorted by page rank
 
-        const resultsJSX = Array();
+        const resultsJSX = [] as any[];
 
         const fromDoc = this._props.linkFrom?.();
 
         sortedResults.forEach(result => {
-            var className = 'searchBox-results-scroll-view-result';
+            let className = 'searchBox-results-scroll-view-result';
 
             if (this._selectedResult === result[0]) {
                 className += ' searchBox-results-scroll-view-result-selected';
             }
 
             const formattedType = SearchBox.formatType(StrCast(result[0].type), StrCast(result[0].type_collection));
-            const title = result[0].title;
+            const { title } = result[0];
 
             if (this._docTypeString === 'keys' || this._docTypeString === 'all' || this._docTypeString === result[0].type) {
                 validResults++;
                 resultsJSX.push(
-                    <Tooltip key={result[0][Id]} placement={'right'} title={<div className="dash-tooltip">{title as string}</div>}>
+                    <Tooltip key={result[0][Id]} placement="right" title={<div className="dash-tooltip">{title as string}</div>}>
                         <div
                             onClick={
                                 isLinkSearch
@@ -429,6 +428,7 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() {
             }
         });
 
+        // eslint-disable-next-line react/jsx-props-no-spreading
         const recommendationsJSX: JSX.Element[] = this._recommendations.map(props => <Recommendation {...props} />);
 
         return (
@@ -459,7 +459,7 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() {
                     <div className="searchBox-results-container">
                         <div className="section-header" style={{ background: SettingsManager.userVariantColor }}>
                             <div className="section-title">Results</div>
-                            <div className="section-subtitle">{`${validResults}` + ' result' + (validResults === 1 ? '' : 's')}</div>
+                            <div className="section-subtitle">{`${validResults} result` + (validResults === 1 ? '' : 's')}</div>
                         </div>
                         <div className="searchBox-results-view">{resultsJSX}</div>
                     </div>
@@ -468,7 +468,7 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() {
                     <div className="searchBox-recommendations-container">
                         <div className="section-header" style={{ background: SettingsManager.userVariantColor }}>
                             <div className="section-title">Recommendations</div>
-                            <div className="section-subtitle">{`${validResults}` + ' result' + (validResults === 1 ? '' : 's')}</div>
+                            <div className="section-subtitle">{`${validResults} result` + (validResults === 1 ? '' : 's')}</div>
                         </div>
                         <div className="searchBox-recommendations-view">{recommendationsJSX}</div>
                     </div>
diff --git a/src/client/views/topbar/TopBar.tsx b/src/client/views/topbar/TopBar.tsx
index b87e5cdde..1ab0932a3 100644
--- a/src/client/views/topbar/TopBar.tsx
+++ b/src/client/views/topbar/TopBar.tsx
@@ -36,6 +36,7 @@ import { dropActionType } from '../../util/DropActionTypes';
  */
 @observer
 export class TopBar extends ObservableReactComponent<{}> {
+    // eslint-disable-next-line no-use-before-define
     static Instance: TopBar;
     @observable private _flipDocumentation = 0;
     constructor(props: any) {
@@ -45,10 +46,12 @@ export class TopBar extends ObservableReactComponent<{}> {
     }
 
     navigateToHome = () => {
-        (CollectionDockingView.Instance?.CaptureThumbnail() ?? new Promise<void>(res => res())).then(() => {
+        (CollectionDockingView.Instance?.CaptureThumbnail() ??
+            new Promise<void>(res => {  res();  })) .then(() => 
+        {
             Doc.ActivePage = 'home';
             DashboardView.closeActiveDashboard(); // bcz: if we do this, we need some other way to keep track, for user convenience, of the last dashboard in use
-        });
+        }); // prettier-ignore
     };
 
     @computed get color() {
@@ -62,7 +65,9 @@ export class TopBar extends ObservableReactComponent<{}> {
     }
 
     @observable happyHeart: boolean = PingManager.Instance.IsBeating;
-    setHappyHeart = action((status: boolean) => (this.happyHeart = status));
+    setHappyHeart = action((status: boolean) => {
+        this.happyHeart = status;
+    });
     dispose = reaction(
         () => PingManager.Instance.IsBeating,
         isBeating => this.setHappyHeart(isBeating)
@@ -86,7 +91,7 @@ export class TopBar extends ObservableReactComponent<{}> {
                     />
                 ) : (
                     <div className="logo-container">
-                        <img className="logo" src="/assets/medium-blue-light-blue-circle.png" alt="dash logo"></img>
+                        <img className="logo" src="/assets/medium-blue-light-blue-circle.png" alt="dash logo" />
                         <span style={{ color: isDark(this.backgroundColor) ? Colors.LIGHT_GRAY : Colors.DARK_GRAY, fontWeight: 200 }}>brown</span>
                         <span style={{ color: isDark(this.backgroundColor) ? Colors.LIGHT_BLUE : Colors.MEDIUM_BLUE, fontWeight: 500 }}>dash</span>
                     </div>
@@ -193,11 +198,11 @@ export class TopBar extends ObservableReactComponent<{}> {
                         }}
                     />
                 ) : null}
-                <IconButton tooltip={'Issue Reporter ⌘I'} size={Size.SMALL} color={this.color} onClick={ReportManager.Instance.open} icon={<FaBug />} />
+                <IconButton tooltip="Issue Reporter ⌘I" size={Size.SMALL} color={this.color} onClick={ReportManager.Instance.open} icon={<FaBug />} />
                 <Flip key={this._flipDocumentation}>
-                    <IconButton tooltip={'Documentation ⌘D'} size={Size.SMALL} color={this.color} onClick={() => window.open('https://brown-dash.github.io/Dash-Documentation/', '_blank')} icon={<FontAwesomeIcon icon="question-circle" />} />
+                    <IconButton tooltip="Documentation ⌘D" size={Size.SMALL} color={this.color} onClick={() => window.open('https://brown-dash.github.io/Dash-Documentation/', '_blank')} icon={<FontAwesomeIcon icon="question-circle" />} />
                 </Flip>
-                <IconButton tooltip={'Settings ⌘⇧S'} size={Size.SMALL} color={this.color} onClick={SettingsManager.Instance.open} icon={<FontAwesomeIcon icon="cog" />} />
+                <IconButton tooltip="Settings ⌘⇧S" size={Size.SMALL} color={this.color} onClick={SettingsManager.Instance.openMgr} icon={<FontAwesomeIcon icon="cog" />} />
                 <IconButton
                     size={Size.SMALL}
                     onClick={ServerStats.Instance.open}
@@ -214,11 +219,13 @@ export class TopBar extends ObservableReactComponent<{}> {
     /**
      * Make the documentation icon flip around to draw attention to it.
      */
-    FlipDocumentationIcon = action(() => (this._flipDocumentation = this._flipDocumentation + 1));
+    FlipDocumentationIcon = action(() => {
+        this._flipDocumentation += 1;
+    });
 
     render() {
         return (
-            //TODO:glr Add support for light / dark mode
+            // TODO:glr Add support for light / dark mode
             <div
                 style={{
                     pointerEvents: 'all',
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index 7714ce46d..4512d5c5b 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -7,7 +7,7 @@ import { DocServer } from '../client/DocServer';
 import { CollectionViewType, DocumentType } from '../client/documents/DocumentTypes';
 import { scriptingGlobal, ScriptingGlobals } from '../client/util/ScriptingGlobals';
 import { afterDocDeserialize, autoObject, Deserializable, SerializationHelper } from '../client/util/SerializationHelper';
-import { undoable } from '../client/util/UndoManager';
+import { undoable, UndoManager } from '../client/util/UndoManager';
 import { ClientUtils, incrementTitleCopy } from '../ClientUtils';
 import {
     AclAdmin, AclAugment, AclEdit, AclPrivate, AclReadonly, Animation, AudioPlay, Brushed, CachedUpdates, DirectLinks,
@@ -38,7 +38,7 @@ export namespace Field {
      */
     export function toKeyValueString(doc: Doc, key: string, showComputedValue?: boolean): string {
         const isOnDelegate = !Doc.IsDataProto(doc) && Object.keys(doc).includes(key.replace(/^_/, ''));
-        const field = ComputedField.WithoutComputed(() => FieldValue(doc[key]));
+        const cfield = ComputedField.WithoutComputed(() => FieldValue(doc[key]));
         const valFunc = (field: FieldType): string => {
             const res =
                 field instanceof ComputedField && showComputedValue
@@ -55,7 +55,7 @@ export namespace Field {
                       .trim()
                       .replace(/^new List\((.*)\)$/, '$1');
         };
-        return !Field.IsField(field) ? (key.startsWith('_') ? '=' : '') : (isOnDelegate ? '=' : '') + valFunc(field);
+        return !Field.IsField(cfield) ? (key.startsWith('_') ? '=' : '') : (isOnDelegate ? '=' : '') + valFunc(cfield);
     }
     export function toScriptString(field: FieldType) {
         switch (typeof field) {
@@ -98,9 +98,11 @@ export namespace Field {
     export function IsField(field: any, includeUndefined: boolean = false): field is FieldType | undefined {
         return ['string', 'number', 'boolean'].includes(typeof field) || field instanceof ObjectField || field instanceof RefField || (includeUndefined && field === undefined);
     }
+    // eslint-disable-next-line @typescript-eslint/no-shadow
     export function Copy(field: any) {
         return field instanceof ObjectField ? ObjectField.MakeCopy(field) : field;
     }
+    UndoManager.SetFieldPrinter(toJavascriptString);
 }
 export type FieldType = number | string | boolean | ObjectField | RefField;
 export type Opt<T> = T | undefined;
@@ -369,7 +371,7 @@ export class Doc extends RefField {
         const sameAuthor = this.author === ClientUtils.CurrentUserEmail();
         const fprefix = 'fields.';
         Object.keys(set ?? {})
-            .filter(key => Object.prototype.hasOwnProperty.call(set, key) && key.startsWith(fprefix))
+            .filter(key => key.startsWith(fprefix))
             .forEach(async key => {
                 const fKey = key.substring(fprefix.length);
                 const fn = async () => {
@@ -396,7 +398,7 @@ export class Doc extends RefField {
             });
         const unset = diff.$unset;
         Object.keys(unset ?? {})
-            .filter(key => Object.prototype.hasOwnProperty.call(unset, key) && key.startsWith(fprefix))
+            .filter(key => key.startsWith(fprefix))
             .forEach(async key => {
                 const fKey = key.substring(7);
                 const fn = () => {
@@ -511,15 +513,13 @@ export namespace Doc {
      */
     export function assign<K extends string>(doc: Doc, fields: Partial<Record<K, Opt<FieldType>>>, skipUndefineds: boolean = false, isInitializing = false) {
         isInitializing && (doc[Initializing] = true);
-        Object.keys(fields)
-            .filter(key => Object.prototype.hasOwnProperty.call(fields, key))
-            .forEach(key => {
-                const value = (fields as any)[key];
-                if (!skipUndefineds || value !== undefined) {
-                    // Do we want to filter out undefineds?
-                    doc[key] = value;
-                }
-            });
+        Object.keys(fields).forEach(key => {
+            const value = (fields as any)[key];
+            if (!skipUndefineds || value !== undefined) {
+                // Do we want to filter out undefineds?
+                doc[key] = value;
+            }
+        });
         isInitializing && (doc[Initializing] = false);
         return doc;
     }
@@ -640,7 +640,7 @@ export namespace Doc {
     export function BestEmbedding(doc: Doc) {
         const dataDoc = doc[DocData];
         const availableEmbeddings = Doc.GetEmbeddings(dataDoc);
-        const bestEmbedding = [...(dataDoc !== doc ? [doc] : []), ...availableEmbeddings].find(doc => !doc.embedContainer && doc.author === ClientUtils.CurrentUserEmail());
+        const bestEmbedding = [...(dataDoc !== doc ? [doc] : []), ...availableEmbeddings].find(d => !d.embedContainer && d.author === ClientUtils.CurrentUserEmail());
         bestEmbedding && Doc.AddEmbedding(doc, doc);
         return bestEmbedding ?? Doc.MakeEmbedding(doc);
     }
@@ -673,27 +673,27 @@ export namespace Doc {
                 };
                 const cfield = ComputedField.WithoutComputed(() => FieldValue(doc[key]));
                 const field = ProxyField.WithoutProxy(() => doc[key]);
-                const copyObjectField = async (field: ObjectField) => {
+                const copyObjectField = async (objField: ObjectField) => {
                     const list = await Cast(doc[key], listSpec(Doc));
                     const docs = list && (await DocListCastAsync(list))?.filter(d => d instanceof Doc);
                     if (docs !== undefined && docs.length) {
                         const clones = await Promise.all(docs.map(async d => Doc.makeClone(d, cloneMap, linkMap, rtfs, exclusions, pruneDocs, cloneLinks, cloneTemplates)));
                         assignKey(new List<Doc>(clones));
                     } else {
-                        assignKey(ObjectField.MakeCopy(field));
-                        if (field instanceof RichTextField) {
-                            if (DocsInTextFieldIds.some(id => field.Data.includes(`"${id}":`))) {
+                        assignKey(ObjectField.MakeCopy(objField));
+                        if (objField instanceof RichTextField) {
+                            if (DocsInTextFieldIds.some(id => objField.Data.includes(`"${id}":`))) {
                                 const docidsearch = new RegExp('(' + DocsInTextFieldIds.map(exp => '(' + exp + ')').join('|') + ')":"([a-z-A-Z0-9_]*)"', 'g');
-                                const rawdocids = field.Data.match(docidsearch);
+                                const rawdocids = objField.Data.match(docidsearch);
                                 const docids = rawdocids?.map((str: string) =>
                                     DocsInTextFieldIds.reduce((output, exp) => output.replace(new RegExp(`${exp}":`, 'g'), ''), str)
                                         .replace(/"/g, '')
                                         .trim()
                                 );
                                 const results = docids && (await DocServer.GetRefFields(docids));
-                                const docs = results && Array.from(Object.keys(results)).map(key => DocCast(results[key]));
-                                docs?.map(doc => doc && Doc.makeClone(doc, cloneMap, linkMap, rtfs, exclusions, pruneDocs, cloneLinks, cloneTemplates));
-                                rtfs.push({ copy, key, field });
+                                const rdocs = results && Array.from(Object.keys(results)).map(rkey => DocCast(results[rkey]));
+                                rdocs?.map(d => d && Doc.makeClone(d, cloneMap, linkMap, rtfs, exclusions, pruneDocs, cloneLinks, cloneTemplates));
+                                rtfs.push({ copy, key, field: objField });
                             }
                         }
                     }
@@ -769,7 +769,7 @@ export namespace Doc {
     export async function MakeClone(doc: Doc, cloneLinks = true, cloneTemplates = true, cloneMap: Map<string, Doc> = new Map()) {
         const linkMap = new Map<string, Doc>();
         const rtfMap: { copy: Doc; key: string; field: RichTextField }[] = [];
-        const copy = await Doc.makeClone(doc, cloneMap, linkMap, rtfMap, ['cloneOf'], doc.embedContainer ? [DocCast(doc.embedContainer)] : [], cloneLinks, cloneTemplates);
+        const clone = await Doc.makeClone(doc, cloneMap, linkMap, rtfMap, ['cloneOf'], doc.embedContainer ? [DocCast(doc.embedContainer)] : [], cloneLinks, cloneTemplates);
         const repaired = new Set<Doc>();
         const linkedDocs = Array.from(linkMap.values());
         linkedDocs.forEach(link => Doc.AddLink?.(link, true));
@@ -787,8 +787,8 @@ export namespace Doc {
             copy[key] = new RichTextField(field.Data.replace(docidsearch, replacer).replace(re, replacer2), field.Text);
         });
         const clonedDocs = [...Array.from(cloneMap.values()), ...linkedDocs];
-        clonedDocs.forEach(clone => Doc.repairClone(clone, cloneMap, cloneTemplates, repaired));
-        return { clone: copy, map: cloneMap, linkMap };
+        clonedDocs.forEach(cloneDoc => Doc.repairClone(cloneDoc, cloneMap, cloneTemplates, repaired));
+        return { clone, map: cloneMap, linkMap };
     }
 
     const _pendingMap = new Set<string>();
@@ -1236,9 +1236,9 @@ export namespace Doc {
         });
     }
     /// if doc is defined, then it is unhighlighted, otherwise all highlighted docs are unhighlighted
-    export function UnHighlightDoc(doc?: Doc) {
+    export function UnHighlightDoc(docs?: Doc) {
         runInAction(() => {
-            (doc ? [doc] : Array.from(highlightedDocs)).forEach(doc => {
+            (docs ? [docs] : Array.from(highlightedDocs)).forEach(doc => {
                 highlightedDocs.delete(doc);
                 highlightedDocs.delete(doc[DocData]);
                 doc[Highlight] = doc[DocData][Highlight] = false;
diff --git a/src/fields/List.ts b/src/fields/List.ts
index f6e0473ea..f97f208fe 100644
--- a/src/fields/List.ts
+++ b/src/fields/List.ts
@@ -2,14 +2,12 @@ import { action, computed, makeObservable, observable } from 'mobx';
 import { alias, list, serializable } from 'serializr';
 import { ScriptingGlobals } from '../client/util/ScriptingGlobals';
 import { Deserializable, afterDocDeserialize, autoObject } from '../client/util/SerializationHelper';
-import { Field, FieldType } from './Doc';
+import { Field, FieldType, StrListCast } from './Doc';
 import { FieldTuples, Self, SelfProxy } from './DocSymbols';
 import { Copy, FieldChanged, Parent, ToJavascriptString, ToScriptString, ToString } from './FieldSymbols';
 import { ObjGetRefFields, ObjectField } from './ObjectField';
 import { ProxyField } from './Proxy';
 import { RefField } from './RefField';
-import { listSpec } from './Schema';
-import { Cast } from './Types';
 import { containedFieldChangedHandler, deleteProperty, getter, setter } from './util';
 
 function toObjectField(field: FieldType) {
@@ -223,7 +221,7 @@ class ListImpl<T extends FieldType> extends ObjectField {
         },
     };
     static listGetter(target: any, prop: string | symbol, receiver: any): any {
-        if (ListImpl.listHandlers.hasOwnProperty(prop)) {
+        if (Object.prototype.hasOwnProperty.call(ListImpl.listHandlers, prop)) {
             return ListImpl.listHandlers[prop];
         }
         return getter(target, prop, receiver);
@@ -252,6 +250,7 @@ class ListImpl<T extends FieldType> extends ObjectField {
                 throw new Error("Currently properties can't be defined on documents using Object.defineProperty");
             },
         });
+        // eslint-disable-next-line no-use-before-define
         this[SelfProxy] = list as any as List<FieldType>; // bcz: ugh .. don't know how to convince typesecript that list is a List
         if (fields) {
             this[SelfProxy].push(...fields);
@@ -287,13 +286,13 @@ class ListImpl<T extends FieldType> extends ObjectField {
 
     private set __fieldTuples(value) {
         this[FieldTuples] = value;
-        for (const key in value) {
-            const item = value[key];
+        Object.keys(value).forEach(key => {
+            const item = value[Number(key)];
             if (item instanceof ObjectField) {
                 item[Parent] = this[Self];
                 item[FieldChanged] = containedFieldChangedHandler(this[SelfProxy], Number(key), item);
             }
-        }
+        });
     }
 
     [Copy]() {
@@ -306,25 +305,24 @@ class ListImpl<T extends FieldType> extends ObjectField {
     @observable
     private [FieldTuples]: StoredType<T>[] = [];
     private [Self] = this;
+    // eslint-disable-next-line no-use-before-define
     private [SelfProxy]: List<FieldType>; // also used in utils.ts even though it won't be found using find all references
 
-    [ToJavascriptString]() {
-        return `[${(this as any).map((field: any) => Field.toScriptString(field))}]`;
-    }
-    [ToScriptString]() {
-        return `new List([${(this as any).map((field: any) => Field.toScriptString(field))}])`;
-    }
-    [ToString]() {
-        return `[${(this as any).map((field: any) => Field.toString(field))}]`;
-    }
+    [ToScriptString]()     { return `new List(${this[ToJavascriptString]})`; } // prettier-ignore
+    [ToJavascriptString]() { return `[${(this as any).map((field: any) => Field.toScriptString(field))}]`; } // prettier-ignore
+    [ToString]()           { return `[${(this as any).map((field: any) => Field.toString(field))}]`; } // prettier-ignore
 }
+
+// declare List as a type so you can use it in type declarations, e.g., { l: List, ...}
 export type List<T extends FieldType> = ListImpl<T> & (T | (T extends RefField ? Promise<T> : never))[];
+// decalre List as a value so you can invoke 'new' on it, e.g., new List<Doc>()
+// eslint-disable-next-line no-redeclare
 export const List: { new <T extends FieldType>(fields?: T[]): List<T> } = ListImpl as any;
 
 ScriptingGlobals.add('List', List);
 // eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function compareLists(l1: any, l2: any) {
-    const L1 = Cast(l1, listSpec('string'), []);
-    const L2 = Cast(l2, listSpec('string'), []);
+    const L1 = StrListCast(l1);
+    const L2 = StrListCast(l2);
     return !L1 && !L2 ? true : L1 && L2 && L1.length === L2.length && L2.reduce((p, v) => p && L1.includes(v), true);
 }, 'compare two lists');
diff --git a/src/fields/ObjectField.ts b/src/fields/ObjectField.ts
index 6c70adc1d..231086262 100644
--- a/src/fields/ObjectField.ts
+++ b/src/fields/ObjectField.ts
@@ -5,6 +5,7 @@ import { RefField } from './RefField';
 export abstract class ObjectField {
     // prettier-ignore
     public [FieldChanged]?: (diff?: { op: '$addToSet' | '$remFromSet' | '$set'; 
+                             // eslint-disable-next-line no-use-before-define
                              items: FieldType[] | undefined; 
                              length: number | undefined; 
                              hint?: any }, serverOp?: any) => void;
diff --git a/src/fields/Proxy.ts b/src/fields/Proxy.ts
index 820d9b6ff..4f8058ce4 100644
--- a/src/fields/Proxy.ts
+++ b/src/fields/Proxy.ts
@@ -21,7 +21,7 @@ export class ProxyField<T extends RefField> extends ObjectField {
     constructor(value?: T | string) {
         super();
         if (typeof value === 'string') {
-            //this.cache = DocServer.GetCachedRefField(value) as any;
+            // this.cache = DocServer.GetCachedRefField(value) as any;
             this.fieldId = value;
         } else if (value) {
             this.cache = { field: value, p: undefined };
@@ -29,7 +29,7 @@ export class ProxyField<T extends RefField> extends ObjectField {
         }
     }
 
-    [ToValue](doc: any) {
+    [ToValue](/* doc: any */) {
         return ProxyField.toValue(this);
     }
 
@@ -39,10 +39,10 @@ export class ProxyField<T extends RefField> extends ObjectField {
     }
 
     [ToJavascriptString]() {
-        return Field.toScriptString(this[ToValue](undefined)?.value);
+        return Field.toScriptString(this[ToValue]()?.value);
     }
     [ToScriptString]() {
-        return Field.toScriptString(this[ToValue](undefined)?.value); // not sure this is quite right since it doesn't recreate a proxy field, but better than 'invalid' ?
+        return Field.toScriptString(this[ToValue]()?.value); // not sure this is quite right since it doesn't recreate a proxy field, but better than 'invalid' ?
     }
     [ToString]() {
         return 'ProxyField';
@@ -59,7 +59,9 @@ export class ProxyField<T extends RefField> extends ObjectField {
         return this._cache;
     }
     private set cache(val: { field: T | undefined; p: FieldWaiting<T> | undefined }) {
-        runInAction(() => (this._cache = { ...val }));
+        runInAction(() => {
+            this._cache = { ...val };
+        });
     }
 
     private failed = false;
@@ -78,7 +80,7 @@ export class ProxyField<T extends RefField> extends ObjectField {
         return this.cache.field ?? this.cache.p;
     }
     @computed get needsRequesting(): boolean {
-        return !this.cache.field && !this.failed && !this._cache.p && !DocServer.GetCachedRefField(this.fieldId) ? true : false;
+        return !!(!this.cache.field && !this.failed && !this._cache.p && !DocServer.GetCachedRefField(this.fieldId));
     }
 
     setExternalValuePromise(externalValuePromise: Promise<any>) {
@@ -92,6 +94,7 @@ export class ProxyField<T extends RefField> extends ObjectField {
     }
 }
 
+// eslint-disable-next-line no-redeclare
 export namespace ProxyField {
     let useProxy = true;
     export function DisableProxyFields() {
@@ -115,9 +118,11 @@ export namespace ProxyField {
         if (useProxy) {
             return { value: value.value };
         }
+        return undefined;
     }
 }
 
+// eslint-disable-next-line no-use-before-define
 function prefetchValue(proxy: PrefetchProxy<RefField>) {
     return proxy.value as any;
 }
diff --git a/src/fields/Schema.ts b/src/fields/Schema.ts
index 364899dc7..ed603e5de 100644
--- a/src/fields/Schema.ts
+++ b/src/fields/Schema.ts
@@ -36,7 +36,7 @@ export function makeInterface<T extends Interface[]>(...schemas: T): InterfaceFu
                 if (prop in schema) {
                     const desc = prop === 'proto' ? Doc : (schema as any)[prop]; // bcz: proto doesn't appear in schemas ... maybe it should?
                     if (typeof desc === 'object' && 'defaultVal' in desc && 'type' in desc) {
-                        //defaultSpec
+                        // defaultSpec
                         return Cast(field, desc.type, desc.defaultVal);
                     }
                     if (typeof desc === 'function' && !ObjectField.isPrototypeOf(desc) && !RefField.isPrototypeOf(desc)) {
diff --git a/src/fields/Types.ts b/src/fields/Types.ts
index 6ed94d341..57a310f6d 100644
--- a/src/fields/Types.ts
+++ b/src/fields/Types.ts
@@ -7,6 +7,7 @@ import { RichTextField } from './RichTextField';
 import { ScriptField } from './ScriptField';
 import { CsvField, ImageField, WebField } from './URLField';
 
+// eslint-disable-next-line no-use-before-define
 export type ToConstructor<T extends FieldType> = T extends string ? 'string' : T extends number ? 'number' : T extends boolean ? 'boolean' : T extends List<infer U> ? ListSpec<U> : new (...args: any[]) => T;
 
 export type DefaultFieldConstructor<T extends FieldType> = {
@@ -27,6 +28,7 @@ export type ToType<T extends InterfaceValue> = T extends 'string'
         : T extends ListSpec<infer U>
           ? List<U>
           : // T extends { new(...args: any[]): infer R } ? (R | Promise<R>) : never;
+            // eslint-disable-next-line @typescript-eslint/no-unused-vars
             T extends DefaultFieldConstructor<infer _U>
             ? never
             : T extends { new (...args: any[]): List<FieldType> }
@@ -37,6 +39,10 @@ export type ToType<T extends InterfaceValue> = T extends 'string'
                   ? R
                   : never;
 
+export interface Interface {
+    [key: string]: InterfaceValue;
+    // [key: string]: ToConstructor<Field> | ListSpec<Field[]>;
+}
 export type ToInterface<T extends Interface> = {
     [P in Exclude<keyof T, 'proto'>]: T[P] extends DefaultFieldConstructor<infer F> ? Exclude<FieldResult<F>, undefined> : FieldResult<ToType<T[P]>>;
 };
@@ -47,10 +53,6 @@ export type Head<T extends any[]> = T extends [any, ...any[]] ? T[0] : never;
 export type Tail<T extends any[]> = ((...t: T) => any) extends (_: any, ...tail: infer TT) => any ? TT : [];
 export type HasTail<T extends any[]> = T extends [] | [any] ? false : true;
 // TODO Allow you to optionally specify default values for schemas, which should then make that field not be partial
-export interface Interface {
-    [key: string]: InterfaceValue;
-    // [key: string]: ToConstructor<Field> | ListSpec<Field[]>;
-}
 export type WithoutRefField<T extends FieldType> = T extends RefField ? never : T;
 
 export type CastCtor = ToConstructor<FieldType> | ListSpec<FieldType>;
@@ -58,13 +60,16 @@ export type CastCtor = ToConstructor<FieldType> | ListSpec<FieldType>;
 type WithoutList<T extends FieldType> = T extends List<infer R> ? (R extends RefField ? (R | Promise<R>)[] : R[]) : T;
 
 export function Cast<T extends CastCtor>(field: FieldResult, ctor: T): FieldResult<ToType<T>>;
+// eslint-disable-next-line no-redeclare
 export function Cast<T extends CastCtor>(field: FieldResult, ctor: T, defaultVal: WithoutList<WithoutRefField<ToType<T>>> | null): WithoutList<ToType<T>>;
+// eslint-disable-next-line no-redeclare
 export function Cast<T extends CastCtor>(field: FieldResult, ctor: T, defaultVal?: ToType<T> | null): FieldResult<ToType<T>> | undefined {
     if (field instanceof Promise) {
         return defaultVal === undefined ? (field.then(f => Cast(f, ctor) as any) as any) : defaultVal === null ? undefined : defaultVal;
     }
     if (field !== undefined && !(field instanceof Promise)) {
         if (typeof ctor === 'string') {
+            // eslint-disable-next-line valid-typeof
             if (typeof field === ctor) {
                 return field as ToType<T>;
             }
@@ -118,7 +123,9 @@ export function ImageCast(field: FieldResult, defaultVal: ImageField | null = nu
 }
 
 export function FieldValue<T extends FieldType, U extends WithoutList<T>>(field: FieldResult<T>, defaultValue: U): WithoutList<T>;
+// eslint-disable-next-line no-redeclare
 export function FieldValue<T extends FieldType>(field: FieldResult<T>): Opt<T>;
+// eslint-disable-next-line no-redeclare
 export function FieldValue<T extends FieldType>(field: FieldResult<T>, defaultValue?: T): Opt<T> {
     return field instanceof Promise || field === undefined ? defaultValue : field;
 }
diff --git a/src/fields/documentSchemas.ts b/src/fields/documentSchemas.ts
index 1cacfe30c..c73689e1f 100644
--- a/src/fields/documentSchemas.ts
+++ b/src/fields/documentSchemas.ts
@@ -104,7 +104,7 @@ export const documentSchema = createSchema({
 
 export const collectionSchema = createSchema({
     childLayoutTemplate: Doc, // layout template to use to render children of a collecion
-    childLayoutString: 'string', //layout string to use to render children of a collection
+    childLayoutString: 'string', // layout string to use to render children of a collection
     childClickedOpenTemplateView: Doc, // layout template to apply to a child when its clicked on in a collection and opened (requires onChildClick or other script to read this value and apply template)
     childDontRegisterViews: 'boolean', // whether views made of this document are registered so that they can be found when drawing links
     onChildClick: ScriptField, // script to run for each child when its clicked
@@ -113,4 +113,5 @@ export const collectionSchema = createSchema({
 });
 
 export type Document = makeInterface<[typeof documentSchema]>;
+// eslint-disable-next-line no-redeclare
 export const Document = makeInterface(documentSchema);
diff --git a/src/mobile/ImageUpload.tsx b/src/mobile/ImageUpload.tsx
index 4e9d85b6d..4fff2a8e7 100644
--- a/src/mobile/ImageUpload.tsx
+++ b/src/mobile/ImageUpload.tsx
@@ -27,7 +27,6 @@ const defaultNativeImageDim = Number(DFLT_IMAGE_NATIVE_DIM.replace('px', ''));
 
 @observer
 export class Uploader extends React.Component<ImageUploadProps> {
-    @observable error: string = '';
     @observable nm: string = 'Choose files'; // Text of 'Choose Files' button
     @observable process: string = ''; // Current status of upload
     @observable private dialogueBoxOpacity = 1;
@@ -40,8 +39,8 @@ export class Uploader extends React.Component<ImageUploadProps> {
             await Docs.Prototypes.initialize();
             const imgPrev = document.getElementById('img_preview');
             this.setOpacity(1, '1'); // Slab 1
-            if (imgPrev) {
-                const files: FileList | null = inputRef.current!.files;
+            if (imgPrev && inputRef.current) {
+                const { files } = inputRef.current;
                 this.setOpacity(2, '1'); // Slab 2
                 if (files && files.length !== 0) {
                     this.process = 'Uploading Files';
@@ -69,11 +68,11 @@ export class Uploader extends React.Component<ImageUploadProps> {
                                 doc = Docs.Create.ImageDocument(path, { _nativeWidth: defaultNativeImageDim, _width: 400, title: name });
                             }
                             this.setOpacity(4, '1'); // Slab 4
-                            const res = await rp.get(ClientUtils.prepend('/getUserDocumentIds'));
-                            if (!res) {
+                            const docidsRes = await rp.get(ClientUtils.prepend('/getUserDocumentIds'));
+                            if (!docidsRes) {
                                 throw new Error('No user id returned');
                             }
-                            const field = await DocServer.GetRefField(JSON.parse(res).userDocumentId);
+                            const field = await DocServer.GetRefField(JSON.parse(docidsRes).userDocumentId);
                             let pending: Opt<Doc>;
                             if (field instanceof Doc) {
                                 pending = col;
@@ -100,10 +99,40 @@ export class Uploader extends React.Component<ImageUploadProps> {
                 setTimeout(this.clearUpload, 3000);
             }
         } catch (error) {
-            this.error = JSON.stringify(error);
+            console.log(JSON.stringify(error));
         }
     };
 
+    // Returns the upload interface for mobile
+    private get uploadInterface() {
+        return (
+            <div className="imgupload_cont">
+                <div className="closeUpload" onClick={() => this.closeUpload()}>
+                    <FontAwesomeIcon icon="window-close" size="lg" />
+                </div>
+                <FontAwesomeIcon icon="upload" size="lg" style={{ fontSize: '130' }} />
+                <input type="file" accept="application/pdf, video/*,image/*" className={`inputFile ${this.nm !== 'Choose files' ? 'active' : ''}`} id="input_image_file" ref={inputRef} onChange={this.inputLabel} multiple />
+                <label className="file" id="label" htmlFor="input_image_file">
+                    {this.nm}
+                </label>
+                <div className="upload_label" onClick={this.onClick}>
+                    Upload
+                </div>
+                <img id="img_preview" src="" alt="" />
+                <div className="loadingImage">
+                    <div className="loadingSlab" id="slab1" />
+                    <div className="loadingSlab" id="slab2" />
+                    <div className="loadingSlab" id="slab3" />
+                    <div className="loadingSlab" id="slab4" />
+                    <div className="loadingSlab" id="slab5" />
+                    <div className="loadingSlab" id="slab6" />
+                    <div className="loadingSlab" id="slab7" />
+                </div>
+                <p className="status">{this.process}</p>
+            </div>
+        );
+    }
+
     // Updates label after a files is selected (so user knows a file is uploaded)
     inputLabel = async () => {
         const files: FileList | null = await inputRef.current!.files;
@@ -112,9 +141,7 @@ export class Uploader extends React.Component<ImageUploadProps> {
         } else if (files && files.length > 1) {
             this.nm = files.length.toString() + ' files selected';
         }
-    };
-
-    // Loops through load icons, and resets buttons
+    }; // Loops through load icons, and resets buttons
     @action
     clearUpload = () => {
         for (let i = 1; i < 8; i++) {
@@ -139,36 +166,6 @@ export class Uploader extends React.Component<ImageUploadProps> {
         if (slab) slab.style.opacity = opacity;
     };
 
-    // Returns the upload interface for mobile
-    private get uploadInterface() {
-        return (
-            <div className="imgupload_cont">
-                <div className="closeUpload" onClick={() => this.closeUpload()}>
-                    <FontAwesomeIcon icon="window-close" size="lg" />
-                </div>
-                <FontAwesomeIcon icon="upload" size="lg" style={{ fontSize: '130' }} />
-                <input type="file" accept="application/pdf, video/*,image/*" className={`inputFile ${this.nm !== 'Choose files' ? 'active' : ''}`} id="input_image_file" ref={inputRef} onChange={this.inputLabel} multiple />
-                <label className="file" id="label" htmlFor="input_image_file">
-                    {this.nm}
-                </label>
-                <div className="upload_label" onClick={this.onClick}>
-                    Upload
-                </div>
-                <img id="img_preview" src="" alt="" />
-                <div className="loadingImage">
-                    <div className="loadingSlab" id="slab1" />
-                    <div className="loadingSlab" id="slab2" />
-                    <div className="loadingSlab" id="slab3" />
-                    <div className="loadingSlab" id="slab4" />
-                    <div className="loadingSlab" id="slab5" />
-                    <div className="loadingSlab" id="slab6" />
-                    <div className="loadingSlab" id="slab7" />
-                </div>
-                <p className="status">{this.process}</p>
-            </div>
-        );
-    }
-
     render() {
         return <MainViewModal contents={this.uploadInterface} isDisplayed interactive dialogueBoxDisplayedOpacity={this.dialogueBoxOpacity} overlayDisplayedOpacity={this.overlayOpacity} closeOnExternalClick={this.closeUpload} />;
     }
diff --git a/src/mobile/MobileInterface.tsx b/src/mobile/MobileInterface.tsx
index 1d4c0adce..b355e70a7 100644
--- a/src/mobile/MobileInterface.tsx
+++ b/src/mobile/MobileInterface.tsx
@@ -857,7 +857,7 @@ ScriptingGlobals.add(function switchToMobilePresentation() {
     return MobileInterface.Instance.setupDefaultPresentation();
 }, 'opens the presentation on Dash Mobile');
 ScriptingGlobals.add(function openMobileSettings() {
-    return SettingsManager.Instance.open();
+    return SettingsManager.Instance.openMgr();
 }, 'opens settings on Dash Mobile');
 
 // Other global functions for mobile
diff --git a/src/server/ApiManagers/UploadManager.ts b/src/server/ApiManagers/UploadManager.ts
index 1a759f04d..4cb3d8baf 100644
--- a/src/server/ApiManagers/UploadManager.ts
+++ b/src/server/ApiManagers/UploadManager.ts
@@ -231,9 +231,9 @@ export default class UploadManager extends ApiManager {
                                             await Promise.all(
                                                 [...rdocs, ...ldocs].map(
                                                     doc =>
-                                                        new Promise<void>(res => {
+                                                        new Promise<void>(dbRes => {
                                                             // overwrite mongo doc with json doc contents
-                                                            Database.Instance.replace(doc.id, doc, err => res(err && console.log(err)), true);
+                                                            Database.Instance.replace(doc.id, doc, err => dbRes(err && console.log(err)), true);
                                                         })
                                                 )
                                             );
-- 
cgit v1.2.3-70-g09d2


From 9d69ab27de83ead3e499edc9028ba85749407a1e Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Tue, 23 Apr 2024 18:35:59 -0400
Subject: more lint cleanup

---
 src/client/goldenLayout.d.ts                       |   3 +-
 src/client/views/AntimodeMenu.tsx                  |  18 +-
 src/client/views/ComponentDecorations.tsx          |   3 +-
 src/client/views/ContextMenu.tsx                   |  19 +-
 src/client/views/DocComponent.tsx                  |  28 +--
 src/client/views/DocumentButtonBar.tsx             |   2 +-
 src/client/views/DocumentDecorations.tsx           |  24 +--
 src/client/views/FilterPanel.tsx                   |   2 +-
 src/client/views/InkControlPtHandles.tsx           |  33 +--
 src/client/views/InkStrokeProperties.ts            |  77 ++++---
 src/client/views/InkTangentHandles.tsx             |  13 +-
 src/client/views/InkingStroke.tsx                  |   6 +-
 src/client/views/KeyphraseQueryView.tsx            |  26 +--
 src/client/views/LightboxView.tsx                  |  82 ++++---
 src/client/views/Main.tsx                          |   4 +-
 src/client/views/MainView.tsx                      |   5 +-
 src/client/views/MainViewModal.tsx                 |   5 +-
 src/client/views/MarqueeAnnotator.tsx              |  32 +--
 src/client/views/PreviewCursor.tsx                 |  16 +-
 .../views/PropertiesDocBacklinksSelector.tsx       |   1 +
 src/client/views/PropertiesDocContextSelector.tsx  |  11 +-
 src/client/views/PropertiesSection.tsx             |  59 +++--
 src/client/views/ScriptBox.tsx                     |  39 +++-
 src/debug/Repl.tsx                                 |  18 +-
 src/debug/Test.tsx                                 |   3 +-
 src/debug/Viewer.tsx                               |  40 ++--
 src/extensions/ArrayExtensions.ts                  |  37 ----
 src/extensions/Extensions.ts                       |   9 +
 src/extensions/ExtensionsTypings.ts                |   9 +
 src/extensions/Extensions_Array.ts                 |  37 ++++
 src/extensions/Extensions_String.ts                |  16 ++
 src/extensions/General/Extensions.ts               |   9 -
 src/extensions/General/ExtensionsTypings.ts        |   8 -
 src/extensions/StringExtensions.ts                 |  17 --
 src/fields/DateField.ts                            |   4 +-
 src/fields/HtmlField.ts                            |   4 +-
 src/fields/IconField.ts                            |   2 +-
 src/fields/List.ts                                 |   4 +-
 src/fields/Schema.ts                               |  17 +-
 src/fields/URLField.ts                             |  18 +-
 src/mobile/MobileMain.tsx                          |   2 +-
 src/pen-gestures/ndollar.ts                        | 238 +++++++++++----------
 42 files changed, 547 insertions(+), 453 deletions(-)
 delete mode 100644 src/extensions/ArrayExtensions.ts
 create mode 100644 src/extensions/Extensions.ts
 create mode 100644 src/extensions/ExtensionsTypings.ts
 create mode 100644 src/extensions/Extensions_Array.ts
 create mode 100644 src/extensions/Extensions_String.ts
 delete mode 100644 src/extensions/General/Extensions.ts
 delete mode 100644 src/extensions/General/ExtensionsTypings.ts
 delete mode 100644 src/extensions/StringExtensions.ts

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

diff --git a/src/client/goldenLayout.d.ts b/src/client/goldenLayout.d.ts
index b50240563..26e8ba356 100644
--- a/src/client/goldenLayout.d.ts
+++ b/src/client/goldenLayout.d.ts
@@ -1,3 +1,2 @@
-
 declare const GoldenLayout: any;
-export = GoldenLayout;
\ No newline at end of file
+export = GoldenLayout;
diff --git a/src/client/views/AntimodeMenu.tsx b/src/client/views/AntimodeMenu.tsx
index db7e64deb..b1eb730fa 100644
--- a/src/client/views/AntimodeMenu.tsx
+++ b/src/client/views/AntimodeMenu.tsx
@@ -3,6 +3,7 @@ import * as React from 'react';
 import { SettingsManager } from '../util/SettingsManager';
 import './AntimodeMenu.scss';
 import { ObservableReactComponent } from './ObservableReactComponent';
+
 export interface AntimodeMenuProps {}
 
 /**
@@ -76,7 +77,7 @@ export abstract class AntimodeMenu<T extends AntimodeMenuProps> extends Observab
     };
 
     @action
-    protected pointerLeave = (e: React.PointerEvent) => {
+    protected pointerLeave = () => {
         if (!this.Pinned && this._canFade) {
             this._transitionProperty = 'opacity';
             this._transitionDuration = '0.5s';
@@ -87,7 +88,7 @@ export abstract class AntimodeMenu<T extends AntimodeMenuProps> extends Observab
     };
 
     @action
-    protected pointerEntered = (e: React.PointerEvent) => {
+    protected pointerEntered = () => {
         this._transitionProperty = 'opacity';
         this._transitionDuration = '0.1s';
         this._transitionDelay = '';
@@ -95,8 +96,10 @@ export abstract class AntimodeMenu<T extends AntimodeMenuProps> extends Observab
     };
 
     @action
-    protected togglePin = (e: React.MouseEvent) => {
-        runInAction(() => (this.Pinned = !this.Pinned));
+    protected togglePin = () => {
+        runInAction(() => {
+            this.Pinned = !this.Pinned;
+        });
     };
 
     protected dragStart = (e: React.PointerEvent) => {
@@ -114,8 +117,7 @@ export abstract class AntimodeMenu<T extends AntimodeMenuProps> extends Observab
 
     @action
     protected dragging = (e: PointerEvent) => {
-        const width = this._mainCont.current!.getBoundingClientRect().width;
-        const height = this._mainCont.current!.getBoundingClientRect().height;
+        const { width, height } = this._mainCont.current!.getBoundingClientRect();
 
         const left = e.pageX - this._offsetX;
         const top = e.pageY - this._offsetY;
@@ -139,9 +141,7 @@ export abstract class AntimodeMenu<T extends AntimodeMenuProps> extends Observab
         e.preventDefault();
     };
 
-    protected getDragger = () => {
-        return <div className="antimodeMenu-dragger" key="dragger" onPointerDown={this.dragStart} style={{ width: '20px' }} />;
-    };
+    protected getDragger = () => <div className="antimodeMenu-dragger" key="dragger" onPointerDown={this.dragStart} style={{ width: '20px' }} />;
 
     protected getElement(buttons: JSX.Element, expanded: boolean = false) {
         const containerClass = expanded ? 'antimodeMenu-cont expanded' : 'antimodeMenu-cont';
diff --git a/src/client/views/ComponentDecorations.tsx b/src/client/views/ComponentDecorations.tsx
index ca4e5f2bc..64b8a8446 100644
--- a/src/client/views/ComponentDecorations.tsx
+++ b/src/client/views/ComponentDecorations.tsx
@@ -1,10 +1,11 @@
 import { observer } from 'mobx-react';
+import * as React from 'react';
 import { SelectionManager } from '../util/SelectionManager';
 import './ComponentDecorations.scss';
-import * as React from 'react';
 
 @observer
 export class ComponentDecorations extends React.Component<{ boundsTop: number; boundsLeft: number }, { value: string }> {
+    // eslint-disable-next-line no-use-before-define
     static Instance: ComponentDecorations;
 
     render() {
diff --git a/src/client/views/ContextMenu.tsx b/src/client/views/ContextMenu.tsx
index 18b433a77..d784a14b8 100644
--- a/src/client/views/ContextMenu.tsx
+++ b/src/client/views/ContextMenu.tsx
@@ -1,3 +1,6 @@
+/* eslint-disable react/no-array-index-key */
+/* eslint-disable react/jsx-props-no-spreading */
+/* eslint-disable default-param-last */
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { action, computed, IReactionDisposer, makeObservable, observable } from 'mobx';
 import { observer } from 'mobx-react';
@@ -147,15 +150,13 @@ export class ContextMenu extends ObservableReactComponent<{}> {
 
     @computed get filteredItems(): (OriginalMenuProps | string[])[] {
         const searchString = this._searchString.toLowerCase().split(' ');
-        const matches = (descriptions: string[]): boolean => {
-            return searchString.every(s => descriptions.some(desc => desc.toLowerCase().includes(s)));
-        };
+        const matches = (descriptions: string[]) => searchString.every(s => descriptions.some(desc => desc.toLowerCase().includes(s)));
         const flattenItems = (items: ContextMenuProps[], groupFunc: (groupName: any) => string[]) => {
             let eles: (OriginalMenuProps | string[])[] = [];
 
             const leaves: OriginalMenuProps[] = [];
-            for (const item of items) {
-                const description = item.description;
+            items.forEach(item => {
+                const { description } = item;
                 const path = groupFunc(description);
                 if ('subitems' in item) {
                     const children = flattenItems(item.subitems, name => [...groupFunc(description), name]);
@@ -163,13 +164,10 @@ export class ContextMenu extends ObservableReactComponent<{}> {
                         eles.push(path);
                         eles = eles.concat(children);
                     }
-                } else {
-                    if (!matches(path)) {
-                        continue;
-                    }
+                } else if (matches(path)) {
                     leaves.push(item);
                 }
-            }
+            });
 
             eles = [...leaves, ...eles];
 
@@ -241,6 +239,7 @@ export class ContextMenu extends ObservableReactComponent<{}> {
                             value={this._searchString}
                             onKeyDown={this.onKeyDown}
                             onChange={this.onChange}
+                            // eslint-disable-next-line jsx-a11y/no-autofocus
                             autoFocus
                         />
                     </span>
diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx
index fae3610ca..fd29b1ca4 100644
--- a/src/client/views/DocComponent.tsx
+++ b/src/client/views/DocComponent.tsx
@@ -249,7 +249,7 @@ export function ViewBoxAnnotatableComponent<P extends FieldViewProps>() {
         removeDocument(doc: Doc | Doc[], annotationKey?: string, leavePushpin?: boolean, dontAddToRemoved?: boolean): boolean {
             const effectiveAcl = GetEffectiveAcl(this.dataDoc);
             const indocs = doc instanceof Doc ? [doc] : doc;
-            const docs = indocs.filter(doc => [AclEdit, AclAdmin].includes(effectiveAcl) || GetEffectiveAcl(doc) === AclAdmin);
+            const docs = indocs.filter(fdoc => [AclEdit, AclAdmin].includes(effectiveAcl) || GetEffectiveAcl(fdoc) === AclAdmin);
 
             // docs.forEach(doc => doc.annotationOn === this.Document && Doc.SetInPlace(doc, 'annotationOn', undefined, true));
             const targetDataDoc = this.Document[DocData]; // this.dataDoc;  // we want to write to the template, not the actual data doc
@@ -258,13 +258,13 @@ export function ViewBoxAnnotatableComponent<P extends FieldViewProps>() {
 
             if (toRemove.length !== 0) {
                 const recentlyClosed = this.Document !== Doc.MyRecentlyClosed ? Doc.MyRecentlyClosed : undefined;
-                toRemove.forEach(doc => {
+                toRemove.forEach(rdoc => {
                     // leavePushpin && DocUtils.LeavePushpin(doc, annotationKey ?? this.annotationKey);
-                    Doc.RemoveDocFromList(targetDataDoc, annotationKey ?? this.annotationKey, doc, true);
-                    doc.embedContainer = undefined;
-                    if (recentlyClosed && !dontAddToRemoved && doc.type !== DocumentType.LOADING) {
-                        Doc.AddDocToList(recentlyClosed, 'data', doc, undefined, true, true);
-                        Doc.RemoveEmbedding(doc, doc);
+                    Doc.RemoveDocFromList(targetDataDoc, annotationKey ?? this.annotationKey, rdoc, true);
+                    rdoc.embedContainer = undefined;
+                    if (recentlyClosed && !dontAddToRemoved && rdoc.type !== DocumentType.LOADING) {
+                        Doc.AddDocToList(recentlyClosed, 'data', rdoc, undefined, true, true);
+                        Doc.RemoveEmbedding(rdoc, rdoc);
                     }
                 });
                 if (targetDataDoc.isGroup && DocListCast(targetDataDoc[annotationKey ?? this.annotationKey]).length < 2) {
@@ -295,7 +295,7 @@ export function ViewBoxAnnotatableComponent<P extends FieldViewProps>() {
         @action.bound
         addDocument = (doc: Doc | Doc[], annotationKey?: string): boolean => {
             const docs = doc instanceof Doc ? [doc] : doc;
-            if (this._props.filterAddDocument?.(docs) === false || docs.find(doc => Doc.AreProtosEqual(doc, this.Document) && Doc.LayoutField(doc) === Doc.LayoutField(this.Document))) {
+            if (this._props.filterAddDocument?.(docs) === false || docs.find(fdoc => Doc.AreProtosEqual(fdoc, this.Document) && Doc.LayoutField(fdoc) === Doc.LayoutField(this.Document))) {
                 return false;
             }
             const targetDataDoc = this.Document[DocData]; // this.dataDoc;  // we want to write to the template, not the actual data doc
@@ -307,12 +307,12 @@ export function ViewBoxAnnotatableComponent<P extends FieldViewProps>() {
             const added = docs;
             if (added.length) {
                 if ([AclAugment, AclEdit, AclAdmin].includes(effectiveAcl)) {
-                    added.forEach(doc => {
-                        doc._dragOnlyWithinContainer = undefined;
-                        if (annotationKey ?? this._annotationKeySuffix()) doc[DocData].annotationOn = this.Document;
-                        else doc[DocData].annotationOn = undefined;
-                        Doc.SetContainer(doc, this.Document);
-                        inheritParentAcls(targetDataDoc, doc, true);
+                    added.forEach(adoc => {
+                        adoc._dragOnlyWithinContainer = undefined;
+                        if (annotationKey ?? this._annotationKeySuffix()) adoc[DocData].annotationOn = this.Document;
+                        else adoc[DocData].annotationOn = undefined;
+                        Doc.SetContainer(adoc, this.Document);
+                        inheritParentAcls(targetDataDoc, adoc, true);
                     });
 
                     const annoDocs = Doc.Get(targetDataDoc, annotationKey ?? this.annotationKey, true) as List<Doc>; // get the dataDoc directly ... when using templates there may be some default items already there, but we can't change them, so we copy them below (should really be some kind of inheritance since the template contents could change)
diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx
index 63b485a43..c007af6fa 100644
--- a/src/client/views/DocumentButtonBar.tsx
+++ b/src/client/views/DocumentButtonBar.tsx
@@ -260,7 +260,7 @@ export class DocumentButtonBar extends ObservableReactComponent<{ views: () => (
         const targetDoc = this.view0?.Document;
         return !targetDoc ? null : (
             <Tooltip title={<div className="dash-tooltip">Open Context Menu</div>}>
-                <div className="documentButtonBar-icon" style={{ color: 'white', cursor: 'pointer' }} onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, e => this.openContextMenu(e))}>
+                <div className="documentButtonBar-icon" style={{ color: 'white', cursor: 'pointer' }} onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, clickEv => this.openContextMenu(clickEv))}>
                     <FontAwesomeIcon className="documentdecorations-icon" icon="bars" />
                 </div>
             </Tooltip>
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 009097b3b..3083b9be0 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -154,7 +154,7 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
     onContainerDown = (e: React.PointerEvent) => {
         const effectiveLayoutAcl = GetEffectiveAcl(SelectionManager.Views[0].Document);
         if (effectiveLayoutAcl === AclAdmin || effectiveLayoutAcl === AclEdit || effectiveLayoutAcl === AclAugment) {
-            setupMoveUpEvents(this, e, e => this.onBackgroundMove(true, e), emptyFunction, emptyFunction);
+            setupMoveUpEvents(this, e, moveEv => this.onBackgroundMove(true, moveEv), emptyFunction, emptyFunction);
             e.stopPropagation();
         }
     };
@@ -165,7 +165,7 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
             setupMoveUpEvents(
                 this,
                 e,
-                e => this.onBackgroundMove(true, e),
+                moveEv => this.onBackgroundMove(true, moveEv),
                 emptyFunction,
                 action(() => {
                     const selected = SelectionManager.Views.length === 1 ? SelectionManager.Docs[0] : undefined;
@@ -179,7 +179,7 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
     };
 
     onBackgroundDown = (e: React.PointerEvent) => {
-        setupMoveUpEvents(this, e, e => this.onBackgroundMove(false, e), emptyFunction, emptyFunction);
+        setupMoveUpEvents(this, e, moveEv => this.onBackgroundMove(false, moveEv), emptyFunction, emptyFunction);
         e.stopPropagation();
     };
     @action
@@ -308,10 +308,10 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
         setupMoveUpEvents(
             this,
             e,
-            e => {
+            moveEv => {
                 const [x, y] = [this.Bounds.x + 3, this.Bounds.y + 3];
                 const maxDist = Math.min((this.Bounds.r - this.Bounds.x) / 2, (this.Bounds.b - this.Bounds.y) / 2);
-                const dist = e.clientX < x && e.clientY < y ? 0 : Math.sqrt((e.clientX - x) * (e.clientX - x) + (e.clientY - y) * (e.clientY - y));
+                const dist = moveEv.clientX < x && moveEv.clientY < y ? 0 : Math.sqrt((moveEv.clientX - x) * (moveEv.clientX - x) + (moveEv.clientY - y) * (moveEv.clientY - y));
                 SelectionManager.Docs.forEach(doc => {
                     const docMax = Math.min(NumCast(doc.width) / 2, NumCast(doc.height) / 2);
                     const radius = Math.min(1, dist / maxDist) * docMax; // set radius based on ratio of drag distance to half diagonal distance of bounding box
@@ -358,7 +358,7 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
         setupMoveUpEvents(
             this,
             e,
-            (e: PointerEvent, down: number[], delta: number[]) => // return false to keep getting events
+            (moveEv: PointerEvent, down: number[], delta: number[]) => // return false to keep getting events
                 this.setRotateCenter(seldocview, [this.rotCenter[0] + delta[0], this.rotCenter[1] + delta[1]]) as any as boolean, 
             action(() => { this._isRotating = false; }), // upEvent
             action(() => { seldocview.Document._rotation_centerX = seldocview.Document._rotation_centerY = 0; }),
@@ -399,9 +399,9 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
         setupMoveUpEvents(
             this,
             e,
-            (e: PointerEvent, down: number[], delta: number[]) => {
-                const previousPoint = { X: e.clientX, Y: e.clientY };
-                const movedPoint = { X: e.clientX - delta[0], Y: e.clientY - delta[1] };
+            (moveEv: PointerEvent, down: number[], delta: number[]) => {
+                const previousPoint = { X: moveEv.clientX, Y: moveEv.clientY };
+                const movedPoint = { X: moveEv.clientX - delta[0], Y: moveEv.clientY - delta[1] };
                 const deltaAng = InkStrokeProperties.angleChange(movedPoint, previousPoint, rcScreen);
                 if (selectedInk.length) {
                     deltaAng && InkStrokeProperties.Instance.rotateInk(selectedInk, deltaAng, rcScreen);
@@ -483,8 +483,8 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
             this._interactionLock = true;
             this._snapPt = thisPt;
             e.ctrlKey && (SelectionManager.Views.forEach(docView => !Doc.NativeHeight(docView.Document) && docView.toggleNativeDimensions()));
-            const fixedAspect = SelectionManager.Docs.some(this.hasFixedAspect);
-            const scaleAspect = {x:scale.x === 1 && fixedAspect ? scale.y : scale.x, y: scale.x !== 1 && fixedAspect ? scale.x : scale.y};
+            const hasFixedAspect = SelectionManager.Docs.some(this.hasFixedAspect);
+            const scaleAspect = {x:scale.x === 1 && hasFixedAspect ? scale.y : scale.x, y: scale.x !== 1 && hasFixedAspect ? scale.x : scale.y};
             SelectionManager.Views.forEach(docView => 
                 this.resizeView(docView, refPt, scaleAspect, { dragHdl, ctrlKey:e.ctrlKey })); // prettier-ignore
             await new Promise<any>(res => { setTimeout(() => { res(this._interactionLock = undefined)})});
@@ -692,7 +692,7 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
             });
         const topBtn = (key: string, icon: string, pointerDown: undefined | ((e: React.PointerEvent) => void), click: undefined | ((e: any) => void), title: string) => (
             <Tooltip key={key} title={<div className="dash-tooltip">{title}</div>} placement="top">
-                <div className={`documentDecorations-${key}Button`} onContextMenu={e => e.preventDefault()} onPointerDown={pointerDown ?? (e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, e => click!(e)))}>
+                <div className={`documentDecorations-${key}Button`} onContextMenu={e => e.preventDefault()} onPointerDown={pointerDown ?? (e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, clickEv => click!(clickEv)))}>
                     <FontAwesomeIcon icon={icon as any} />
                 </div>
             </Tooltip>
diff --git a/src/client/views/FilterPanel.tsx b/src/client/views/FilterPanel.tsx
index be307600c..994107c01 100644
--- a/src/client/views/FilterPanel.tsx
+++ b/src/client/views/FilterPanel.tsx
@@ -301,7 +301,7 @@ export class FilterPanel extends ObservableReactComponent<filterProps> {
                         }
                         style={{ color: SettingsManager.userColor, background: SettingsManager.userBackgroundColor }}
                         onBlur={undoable(e => Doc.setDocFilter(this.Document, facetHeader, e.currentTarget.value, !e.currentTarget.value ? 'remove' : 'match'), 'set text filter')}
-                        onKeyDown={e => e.key === 'Enter' && undoable(e => Doc.setDocFilter(this.Document, facetHeader, e.currentTarget.value, !e.currentTarget.value ? 'remove' : 'match'), 'set text filter')(e)}
+                        onKeyDown={e => e.key === 'Enter' && undoable(() => Doc.setDocFilter(this.Document, facetHeader, e.currentTarget.value, !e.currentTarget.value ? 'remove' : 'match'), 'set text filter')()}
                     />
                 );
             case 'checkbox':
diff --git a/src/client/views/InkControlPtHandles.tsx b/src/client/views/InkControlPtHandles.tsx
index 0358344b9..b4fe44733 100644
--- a/src/client/views/InkControlPtHandles.tsx
+++ b/src/client/views/InkControlPtHandles.tsx
@@ -2,7 +2,7 @@ import * as React from 'react';
 import { action, makeObservable, observable } from 'mobx';
 import { observer } from 'mobx-react';
 import { Doc } from '../../fields/Doc';
-import { ControlPoint, InkData, PointData } from '../../fields/InkField';
+import { ControlPoint, InkData } from '../../fields/InkField';
 import { List } from '../../fields/List';
 import { listSpec } from '../../fields/Schema';
 import { Cast } from '../../fields/Types';
@@ -14,6 +14,7 @@ import { InkingStroke } from './InkingStroke';
 import { InkStrokeProperties } from './InkStrokeProperties';
 import { SnappingManager } from '../util/SnappingManager';
 import { ObservableReactComponent } from './ObservableReactComponent';
+import { PointData } from '../../pen-gestures/GestureTypes';
 
 export interface InkControlProps {
     inkDoc: Doc;
@@ -48,7 +49,7 @@ export class InkControlPtHandles extends ObservableReactComponent<InkControlProp
      */
     @action
     onControlDown = (e: React.PointerEvent, controlIndex: number): void => {
-        const ptFromScreen = this._props.inkView.ptFromScreen;
+        const { ptFromScreen } = this._props.inkView;
         if (ptFromScreen) {
             const order = controlIndex % 4;
             const handleIndexA = ((order === 3 ? controlIndex - 1 : controlIndex - 2) + this._props.inkCtrlPoints.length) % this._props.inkCtrlPoints.length;
@@ -60,7 +61,7 @@ export class InkControlPtHandles extends ObservableReactComponent<InkControlProp
             setupMoveUpEvents(
                 this,
                 e,
-                action((e: PointerEvent, down: number[], delta: number[]) => {
+                action((moveEv: PointerEvent, down: number[], delta: number[]) => {
                     if (!this._props.inkView.controlUndo) this._props.inkView.controlUndo = UndoManager.StartBatch('drag ink ctrl pt');
                     const inkMoveEnd = ptFromScreen({ X: delta[0], Y: delta[1] });
                     const inkMoveStart = ptFromScreen({ X: 0, Y: 0 });
@@ -75,9 +76,9 @@ export class InkControlPtHandles extends ObservableReactComponent<InkControlProp
                     this._props.inkView.controlUndo = undefined;
                     UndoManager.FilterBatches(['data', 'x', 'y', 'width', 'height']);
                 }),
-                action((e: PointerEvent, doubleTap: boolean | undefined) => {
+                action((moveEv: PointerEvent, doubleTap: boolean | undefined) => {
                     const equivIndex = controlIndex === 0 ? this._props.inkCtrlPoints.length - 1 : controlIndex === this._props.inkCtrlPoints.length - 1 ? 0 : controlIndex;
-                    if (doubleTap || e.button === 2) {
+                    if (doubleTap || moveEv.button === 2) {
                         if (!brokenIndices?.includes(equivIndex) && !brokenIndices?.includes(controlIndex)) {
                             if (brokenIndices) brokenIndices.push(controlIndex);
                             else this._props.inkDoc.brokenInkIndices = new List<number>([controlIndex]);
@@ -128,7 +129,9 @@ export class InkControlPtHandles extends ObservableReactComponent<InkControlProp
      * Changes the current selected control point.
      */
     @action
-    changeCurrPoint = (i: number) => (InkStrokeProperties.Instance._currentPoint = i);
+    changeCurrPoint = (i: number) => {
+        InkStrokeProperties.Instance._currentPoint = i;
+    };
 
     render() {
         // Accessing the current ink's data and extracting all control points.
@@ -176,7 +179,7 @@ export class InkControlPtHandles extends ObservableReactComponent<InkControlProp
         };
         return (
             <svg>
-                {!nearestScreenPt ? null : <circle key={'npt'} cx={nearestScreenPt.X} cy={nearestScreenPt.Y} r={this._props.screenSpaceLineWidth * 2} fill={'#00007777'} stroke={'#00007777'} strokeWidth={0} pointerEvents="none" />}
+                {!nearestScreenPt ? null : <circle key="npt" cx={nearestScreenPt.X} cy={nearestScreenPt.Y} r={this._props.screenSpaceLineWidth * 2} fill="#00007777" stroke="#00007777" strokeWidth={0} pointerEvents="none" />}
                 {sreenCtrlPoints.map(control => hdl(control, this._overControl !== control.I ? 1 : 3 / 2, Colors.WHITE))}
             </svg>
         );
@@ -207,21 +210,21 @@ export class InkEndPtHandles extends ObservableReactComponent<InkEndProps> {
         setupMoveUpEvents(
             this,
             e,
-            action(e => {
+            action(moveEv => {
                 if (this._throttle++ % 2 !== 0) return false;
                 if (!this._props.inkView.controlUndo) this._props.inkView.controlUndo = UndoManager.StartBatch('stretch ink');
                 // compute stretch factor by finding scaling along axis between start and end points
                 const p1 = pt1();
                 const p2 = pt2();
                 const v1 = { X: p1.X - p2.X, Y: p1.Y - p2.Y };
-                const v2 = { X: e.clientX - p2.X, Y: e.clientY - p2.Y };
+                const v2 = { X: moveEv.clientX - p2.X, Y: moveEv.clientY - p2.Y };
                 const v1len = Math.sqrt(v1.X * v1.X + v1.Y * v1.Y);
                 const v2len = Math.sqrt(v2.X * v2.X + v2.Y * v2.Y);
                 const scaling = v2len / v1len;
                 const v1n = { X: v1.X / v1len, Y: v1.Y / v1len };
                 const v2n = { X: v2.X / v2len, Y: v2.Y / v2len };
                 const angle = Math.acos(v1n.X * v2n.X + v1n.Y * v2n.Y) * Math.sign(v1.X * v2.Y - v2.X * v1.Y);
-                InkStrokeProperties.Instance.stretchInk(SelectionManager.Views, scaling, p2, v1n, e.shiftKey);
+                InkStrokeProperties.Instance.stretchInk(SelectionManager.Views, scaling, p2, v1n, moveEv.shiftKey);
                 InkStrokeProperties.Instance.rotateInk(SelectionManager.Views, angle, pt2()); // bcz: call pt2() func here because pt2 will have changed from previous stretchInk call
                 return false;
             }),
@@ -243,10 +246,14 @@ export class InkEndPtHandles extends ObservableReactComponent<InkEndProps> {
                 cy={pt?.Y}
                 r={this._props.screenSpaceLineWidth * 2}
                 fill={this._overStart ? '#aaaaaa' : '#99999977'}
-                stroke={'#00007777'}
+                stroke="#00007777"
                 strokeWidth={0}
-                onPointerLeave={action(() => (this._overStart = false))}
-                onPointerEnter={action(() => (this._overStart = true))}
+                onPointerLeave={action(() => {
+                    this._overStart = false;
+                })}
+                onPointerEnter={action(() => {
+                    this._overStart = true;
+                })}
                 onPointerDown={dragFunc}
                 pointerEvents="all"
             />
diff --git a/src/client/views/InkStrokeProperties.ts b/src/client/views/InkStrokeProperties.ts
index 52ea89cde..b3f9f9ea7 100644
--- a/src/client/views/InkStrokeProperties.ts
+++ b/src/client/views/InkStrokeProperties.ts
@@ -1,20 +1,22 @@
 import { Bezier } from 'bezier-js';
+import * as _ from 'lodash';
 import { action, makeObservable, observable, reaction, runInAction } from 'mobx';
 import { Doc, NumListCast, Opt } from '../../fields/Doc';
-import { InkData, InkField, InkTool, PointData } from '../../fields/InkField';
+import { InkData, InkField, InkTool } from '../../fields/InkField';
 import { List } from '../../fields/List';
 import { listSpec } from '../../fields/Schema';
 import { Cast, NumCast } from '../../fields/Types';
+import { PointData } from '../../pen-gestures/GestureTypes';
 import { Point } from '../../pen-gestures/ndollar';
 import { DocumentType } from '../documents/DocumentTypes';
-import { FitOneCurve } from '../util/bezierFit';
 import { DocumentManager } from '../util/DocumentManager';
 import { undoBatch } from '../util/UndoManager';
+import { FitOneCurve } from '../util/bezierFit';
 import { InkingStroke } from './InkingStroke';
 import { DocumentView } from './nodes/DocumentView';
-import * as _ from 'lodash';
 
 export class InkStrokeProperties {
+    // eslint-disable-next-line no-use-before-define
     static _Instance: InkStrokeProperties | undefined;
     public static get Instance() {
         return this._Instance || new InkStrokeProperties();
@@ -28,11 +30,15 @@ export class InkStrokeProperties {
         makeObservable(this);
         reaction(
             () => this._controlButton,
-            button => button && (Doc.ActiveTool = InkTool.None)
+            button => {
+                button && (Doc.ActiveTool = InkTool.None);
+            }
         );
         reaction(
             () => Doc.ActiveTool,
-            tool => tool !== InkTool.None && (this._controlButton = false)
+            tool => {
+                tool !== InkTool.None && (this._controlButton = false);
+            }
         );
     }
 
@@ -46,7 +52,7 @@ export class InkStrokeProperties {
         func: (view: DocumentView, ink: InkData, ptsXscale: number, ptsYscale: number, inkStrokeWidth: number) => { X: number; Y: number }[] | undefined,
         requireCurrPoint: boolean = false
     ) => {
-        var appliedFunc = false;
+        let appliedFunc = false;
         (strokes instanceof DocumentView ? [strokes] : strokes)?.forEach(
             action(inkView => {
                 if (!requireCurrPoint || this._currentPoint !== -1) {
@@ -85,7 +91,7 @@ export class InkStrokeProperties {
      */
     @undoBatch
     addPoints = (inkView: DocumentView, t: number, i: number, controls: { X: number; Y: number }[]) => {
-        this.applyFunction(inkView, (view: DocumentView, ink: InkData) => {
+        this.applyFunction(inkView, (view: DocumentView /* , ink: InkData */) => {
             const doc = view.Document;
             const array = [controls[i], controls[i + 1], controls[i + 2], controls[i + 3]];
             const newsegs = new Bezier(array.map(p => ({ x: p.X, y: p.Y }))).split(t);
@@ -94,7 +100,9 @@ export class InkStrokeProperties {
 
             // Updating the indices of the control points whose handle tangency has been broken.
             doc.brokenInkIndices = new List(Cast(doc.brokenInkIndices, listSpec('number'), []).map(control => (control > i ? control + 4 : control)));
-            runInAction(() => (this._currentPoint = -1));
+            runInAction(() => {
+                this._currentPoint = -1;
+            });
 
             return controls;
         });
@@ -126,8 +134,8 @@ export class InkStrokeProperties {
      */
     getNewHandlePoints = (C: PointData[], D: PointData[], newControl: PointData) => {
         const [m, n] = [C.length, D.length];
-        let handleSizeA = Math.sqrt(Math.pow(newControl.X - C[0].X, 2) + Math.pow(newControl.Y - C[0].Y, 2));
-        let handleSizeB = Math.sqrt(Math.pow(D[n - 1].X - newControl.X, 2) + Math.pow(D[n - 1].Y - newControl.Y, 2));
+        let handleSizeA = Math.sqrt((newControl.X - C[0].X) ** 2 + (newControl.Y - C[0].Y) ** 2);
+        let handleSizeB = Math.sqrt((D[n - 1].X - newControl.X) ** 2 + (D[n - 1].Y - newControl.Y) ** 2);
         // Scaling adjustments to improve the ratio between the magnitudes of the two handle lines.
         // (Ensures that the new point added doesn't augment the inital shape of the curve much).
         if (handleSizeA < 75 && handleSizeB < 75) {
@@ -167,13 +175,13 @@ export class InkStrokeProperties {
                     const start = this._currentPoint === 0 ? 0 : this._currentPoint - 4;
                     const splicedPoints = ink.slice(start, start + (this._currentPoint === 0 || this._currentPoint === ink.length - 1 ? 4 : 8));
                     const samples: Point[] = [];
-                    var startDir = { x: 0, y: 0 };
-                    var endDir = { x: 0, y: 0 };
-                    for (var i = 0; i < splicedPoints.length / 4; i++) {
+                    let startDir = { x: 0, y: 0 };
+                    let endDir = { x: 0, y: 0 };
+                    for (let i = 0; i < splicedPoints.length / 4; i++) {
                         const bez = new Bezier(splicedPoints.slice(i * 4, i * 4 + 4).map(p => ({ x: p.X, y: p.Y })));
                         if (i === 0) startDir = bez.derivative(0);
                         if (i === splicedPoints.length / 4 - 1) endDir = bez.derivative(1);
-                        for (var t = 0; t < (i === splicedPoints.length / 4 - 1 ? 1 + 1e-7 : 1); t += 0.05) {
+                        for (let t = 0; t < (i === splicedPoints.length / 4 - 1 ? 1 + 1e-7 : 1); t += 0.05) {
                             const pt = bez.compute(t);
                             samples.push(new Point(pt.x, pt.y));
                         }
@@ -186,7 +194,9 @@ export class InkStrokeProperties {
                     }
                 }
                 doc.brokenInkIndices = new List(brokenIndices.map(control => (control >= this._currentPoint ? control - 4 : control)));
-                runInAction(() => (this._currentPoint = -1));
+                runInAction(() => {
+                    this._currentPoint = -1;
+                });
                 return newPoints.length < 4 ? undefined : newPoints;
             },
             true
@@ -200,7 +210,7 @@ export class InkStrokeProperties {
      */
     @undoBatch
     rotateInk = (inkStrokes: DocumentView[], angle: number, scrpt: PointData) => {
-        this.applyFunction(inkStrokes, (view: DocumentView, ink: InkData, xScale: number, yScale: number, inkStrokeWidth: number) => {
+        this.applyFunction(inkStrokes, (view: DocumentView, ink: InkData, xScale: number, yScale: number /* , inkStrokeWidth: number */) => {
             const inkCenterPt = view.ComponentView?.ptFromScreen?.(scrpt);
             return !inkCenterPt
                 ? ink
@@ -247,8 +257,8 @@ export class InkStrokeProperties {
             const closed = InkingStroke.IsClosed(ink);
             const brokenIndices = Cast(inkView.Document.brokenInkIndices, listSpec('number'), []);
             if (origInk && this._currentPoint > 0 && this._currentPoint < ink.length - 1 && brokenIndices.findIndex(value => value === controlIndex) === -1) {
-                const cpt_before = ink[controlIndex];
-                const cpt = { X: cpt_before.X + deltaX, Y: cpt_before.Y + deltaY };
+                const cptBefore = ink[controlIndex];
+                const cpt = { X: cptBefore.X + deltaX, Y: cptBefore.Y + deltaY };
                 const newink = origInk.slice();
                 const start = this._currentPoint === 0 ? 0 : this._currentPoint - 4;
                 const splicedPoints = origInk.slice(start, start + (this._currentPoint === 0 || this._currentPoint === ink.length - 1 ? 4 : 8));
@@ -256,28 +266,28 @@ export class InkStrokeProperties {
                 if ((nearestSeg === 0 && nearestT < 1e-1) || (nearestSeg === 4 && 1 - nearestT < 1e-1)) return ink.slice();
                 const samplesLeft: Point[] = [];
                 const samplesRight: Point[] = [];
-                var startDir = { x: 0, y: 0 };
-                var endDir = { x: 0, y: 0 };
-                for (var i = 0; i < nearestSeg / 4 + 1; i++) {
+                let startDir = { x: 0, y: 0 };
+                let endDir = { x: 0, y: 0 };
+                for (let i = 0; i < nearestSeg / 4 + 1; i++) {
                     const bez = new Bezier(splicedPoints.slice(i * 4, i * 4 + 4).map(p => ({ x: p.X, y: p.Y })));
                     if (i === 0) startDir = bez.derivative(_.isEqual(bez.derivative(0), { x: 0, y: 0, t: 0 }) ? 1e-8 : 0);
                     if (i === nearestSeg / 4) endDir = bez.derivative(nearestT);
-                    for (var t = 0; t < (i === nearestSeg / 4 ? nearestT + 0.05 : 1); t += 0.05) {
+                    for (let t = 0; t < (i === nearestSeg / 4 ? nearestT + 0.05 : 1); t += 0.05) {
                         const pt = bez.compute(i !== nearestSeg / 4 ? t : Math.min(nearestT, t));
                         samplesLeft.push(new Point(pt.x, pt.y));
                     }
                 }
-                var { finalCtrls } = FitOneCurve(samplesLeft, { X: startDir.x, Y: startDir.y }, { X: endDir.x, Y: endDir.y });
-                for (var i = nearestSeg / 4; i < splicedPoints.length / 4; i++) {
+                let { finalCtrls } = FitOneCurve(samplesLeft, { X: startDir.x, Y: startDir.y }, { X: endDir.x, Y: endDir.y });
+                for (let i = nearestSeg / 4; i < splicedPoints.length / 4; i++) {
                     const bez = new Bezier(splicedPoints.slice(i * 4, i * 4 + 4).map(p => ({ x: p.X, y: p.Y })));
                     if (i === nearestSeg / 4) startDir = bez.derivative(nearestT);
                     if (i === splicedPoints.length / 4 - 1) endDir = bez.derivative(_.isEqual(bez.derivative(1), { x: 0, y: 0, t: 1 }) ? 1 - 1e-8 : 1);
-                    for (var t = i === nearestSeg / 4 ? nearestT : 0; t < (i === nearestSeg / 4 ? 1 + 0.05 + 1e-7 : 1 + 1e-7); t += 0.05) {
+                    for (let t = i === nearestSeg / 4 ? nearestT : 0; t < (i === nearestSeg / 4 ? 1 + 0.05 + 1e-7 : 1 + 1e-7); t += 0.05) {
                         const pt = bez.compute(Math.min(1, t));
                         samplesRight.push(new Point(pt.x, pt.y));
                     }
                 }
-                const { finalCtrls: rightCtrls, error: errorRight } = FitOneCurve(samplesRight, { X: startDir.x, Y: startDir.y }, { X: endDir.x, Y: endDir.y });
+                const { finalCtrls: rightCtrls /* , error: errorRight */ } = FitOneCurve(samplesRight, { X: startDir.x, Y: startDir.y }, { X: endDir.x, Y: endDir.y });
                 finalCtrls = finalCtrls.concat(rightCtrls);
                 newink.splice(this._currentPoint - 4, 8, ...finalCtrls);
                 return newink;
@@ -307,11 +317,12 @@ export class InkStrokeProperties {
         });
 
     public static nearestPtToStroke(ctrlPoints: { X: number; Y: number }[], refInkSpacePt: { X: number; Y: number }, excludeSegs?: number[]) {
-        var distance = Number.MAX_SAFE_INTEGER;
-        var nearestT = -1;
-        var nearestSeg = -1;
-        var nearestPt = { X: 0, Y: 0 };
-        for (var i = 0; i < ctrlPoints.length - 3; i += 4) {
+        let distance = Number.MAX_SAFE_INTEGER;
+        let nearestT = -1;
+        let nearestSeg = -1;
+        let nearestPt = { X: 0, Y: 0 };
+        for (let i = 0; i < ctrlPoints.length - 3; i += 4) {
+            // eslint-disable-next-line no-continue
             if (excludeSegs?.includes(i)) continue;
             const array = [ctrlPoints[i], ctrlPoints[i + 1], ctrlPoints[i + 2], ctrlPoints[i + 3]];
             const point = new Bezier(array.map(p => ({ x: p.X, y: p.Y }))).project({ x: refInkSpacePt.X, y: refInkSpacePt.Y });
@@ -380,6 +391,7 @@ export class InkStrokeProperties {
                     const snappedInkPt = doc === inkView.Document ? snapped.nearestPt : inkView.ComponentView?.ptFromScreen?.(testInkView?.ComponentView?.ptToScreen?.(snapped.nearestPt) ?? { X: 0, Y: 0 }); // convert from snapped ink coordinate system to dragged ink coordinate system by converting to/from screen space
 
                     if (snappedInkPt) {
+                        // eslint-disable-next-line no-param-reassign
                         snapData = { nearestPt: snappedInkPt, distance: snapped.distance };
                     }
                 }
@@ -406,6 +418,7 @@ export class InkStrokeProperties {
                 inkCopy[handleIndexB] = this.rotatePoint(handleB, controlPoint, angleDifference);
                 return inkCopy;
             }
+            return undefined;
         });
     };
 
@@ -430,7 +443,9 @@ export class InkStrokeProperties {
         const magnitudeB = Math.sqrt(vectorB.X * vectorB.X + vectorB.Y * vectorB.Y);
         if (magnitudeA === 0 || magnitudeB === 0) return 0;
         // Normalizing the vectors.
+        // eslint-disable-next-line no-param-reassign
         vectorA = { X: vectorA.X / magnitudeA, Y: vectorA.Y / magnitudeA };
+        // eslint-disable-next-line no-param-reassign
         vectorB = { X: vectorB.X / magnitudeB, Y: vectorB.Y / magnitudeB };
         return Math.acos(vectorB.X * vectorA.X + vectorB.Y * vectorA.Y);
     }
diff --git a/src/client/views/InkTangentHandles.tsx b/src/client/views/InkTangentHandles.tsx
index f3a757f19..577acc4d1 100644
--- a/src/client/views/InkTangentHandles.tsx
+++ b/src/client/views/InkTangentHandles.tsx
@@ -8,17 +8,16 @@ import { listSpec } from '../../fields/Schema';
 import { Cast } from '../../fields/Types';
 import { emptyFunction } from '../../Utils';
 import { setupMoveUpEvents } from '../../ClientUtils';
-import { Transform } from '../util/Transform';
 import { UndoManager } from '../util/UndoManager';
 import { Colors } from './global/globalEnums';
 import { InkingStroke } from './InkingStroke';
 import { InkStrokeProperties } from './InkStrokeProperties';
+
 export interface InkHandlesProps {
     inkDoc: Doc;
     inkView: InkingStroke;
     screenCtrlPoints: InkData;
     screenSpaceLineWidth: number;
-    ScreenToLocalTransform: () => Transform;
 }
 
 @observer
@@ -38,9 +37,9 @@ export class InkTangentHandles extends React.Component<InkHandlesProps> {
         setupMoveUpEvents(
             this,
             e,
-            action((e: PointerEvent, down: number[], delta: number[]) => {
+            action((moveEv: PointerEvent, down: number[], delta: number[]) => {
                 if (!this.props.inkView.controlUndo) this.props.inkView.controlUndo = UndoManager.StartBatch('DocDecs move tangent');
-                if (e.altKey) this.onBreakTangent(controlIndex);
+                if (moveEv.altKey) this.onBreakTangent(controlIndex);
                 const inkMoveEnd = this.props.inkView.ptFromScreen({ X: delta[0], Y: delta[1] });
                 const inkMoveStart = this.props.inkView.ptFromScreen({ X: 0, Y: 0 });
                 this.docView && InkStrokeProperties.Instance.moveTangentHandle(this.docView, -(inkMoveEnd.X - inkMoveStart.X), -(inkMoveEnd.Y - inkMoveStart.Y), handleIndex, oppositeHandleIndex, controlIndex);
@@ -101,11 +100,12 @@ export class InkTangentHandles extends React.Component<InkHandlesProps> {
                 tangentLines.push({ X1: data[i].X, Y1: data[i].Y, X2: data[i + 1].X, Y2: data[i + 1].Y, X3: data[i + 3].X, Y3: data[i + 3].Y, dot1: i + 1, dot2: i + 2 });
             }
         }
-        const screenSpaceLineWidth = this.props.screenSpaceLineWidth;
+        const { screenSpaceLineWidth } = this.props;
 
         return (
             <>
                 {tangentHandles.map((pts, i) => (
+                    // eslint-disable-next-line react/no-array-index-key
                     <svg height="10" width="10" key={`hdl${i}`}>
                         <circle
                             cx={pts.X}
@@ -129,12 +129,13 @@ export class InkTangentHandles extends React.Component<InkHandlesProps> {
                             x2={x2}
                             y2={y2}
                             stroke={Colors.MEDIUM_BLUE}
-                            strokeDasharray={'1 1'}
+                            strokeDasharray="1 1"
                             strokeWidth={1}
                             display={pts.dot1 === InkStrokeProperties.Instance._currentPoint || pts.dot2 === InkStrokeProperties.Instance._currentPoint ? 'inherit' : 'none'}
                         />
                     );
                     return (
+                        // eslint-disable-next-line react/no-array-index-key
                         <svg height="100" width="100" key={`line${i}`}>
                             {tangentLine(pts.X1, pts.Y1, pts.X2, pts.Y2)}
                             {tangentLine(pts.X2, pts.Y2, pts.X3, pts.Y3)}
diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx
index e04daec3b..35067047b 100644
--- a/src/client/views/InkingStroke.tsx
+++ b/src/client/views/InkingStroke.tsx
@@ -144,7 +144,7 @@ export class InkingStroke extends ViewBoxAnnotatableComponent<FieldViewProps>()
             e,
             !isEditing
                 ? returnFalse
-                : action((e: PointerEvent, down: number[], delta: number[]) => {
+                : action((moveEv: PointerEvent, down: number[], delta: number[]) => {
                       if (!this.controlUndo) this.controlUndo = UndoManager.StartBatch('drag ink ctrl pt');
                       const inkMoveEnd = this.ptFromScreen({ X: delta[0], Y: delta[1] });
                       const inkMoveStart = this.ptFromScreen({ X: 0, Y: 0 });
@@ -159,7 +159,7 @@ export class InkingStroke extends ViewBoxAnnotatableComponent<FieldViewProps>()
                       this.controlUndo = undefined;
                       UndoManager.FilterBatches(['data', 'x', 'y', 'width', 'height']);
                   }),
-            action((e: PointerEvent, doubleTap: boolean | undefined) => {
+            action((moveEv: PointerEvent, doubleTap: boolean | undefined) => {
                 if (doubleTap) {
                     InkStrokeProperties.Instance._controlButton = true;
                     InkStrokeProperties.Instance._currentPoint = -1;
@@ -331,7 +331,7 @@ export class InkingStroke extends ViewBoxAnnotatableComponent<FieldViewProps>()
                     false
                 )}
                 <InkControlPtHandles inkView={this} inkDoc={inkDoc} inkCtrlPoints={inkData} screenCtrlPoints={this.screenCtrlPts} nearestScreenPt={this.nearestScreenPt} screenSpaceLineWidth={screenSpaceCenterlineStrokeWidth} />
-                <InkTangentHandles inkView={this} inkDoc={inkDoc} screenCtrlPoints={this.screenCtrlPts} screenSpaceLineWidth={screenSpaceCenterlineStrokeWidth} ScreenToLocalTransform={this.ScreenToLocalBoxXf} />
+                <InkTangentHandles inkView={this} inkDoc={inkDoc} screenCtrlPoints={this.screenCtrlPts} screenSpaceLineWidth={screenSpaceCenterlineStrokeWidth} />
             </div>
         );
     };
diff --git a/src/client/views/KeyphraseQueryView.tsx b/src/client/views/KeyphraseQueryView.tsx
index e996fc946..81f004010 100644
--- a/src/client/views/KeyphraseQueryView.tsx
+++ b/src/client/views/KeyphraseQueryView.tsx
@@ -1,3 +1,4 @@
+/* eslint-disable jsx-a11y/label-has-associated-control */
 import { observer } from 'mobx-react';
 import * as React from 'react';
 import './KeyphraseQueryView.scss';
@@ -9,28 +10,21 @@ export interface KP_Props {
 
 @observer
 export class KeyphraseQueryView extends React.Component<KP_Props> {
-    constructor(props: KP_Props) {
-        super(props);
-    }
-
     render() {
-        const kps = this.props.keyphrases.toString();
         const keyterms = this.props.keyphrases.split(',');
         return (
             <div>
                 <h5>Select queries to send:</h5>
                 <form>
-                    {keyterms.map((kp: string) => {
-                        //return (<p>{"-" + kp}</p>);
-                        return (
-                            <p>
-                                <label>
-                                    <input name="query" type="radio" />
-                                    <span>{kp}</span>
-                                </label>
-                            </p>
-                        );
-                    })}
+                    {keyterms.map((kp: string) => (
+                        //  return (<p>{"-" + kp}</p>);
+                        <p>
+                            <label>
+                                <input name="query" type="radio" />
+                                <span>{kp}</span>
+                            </label>
+                        </p>
+                    ))}
                 </form>
             </div>
         );
diff --git a/src/client/views/LightboxView.tsx b/src/client/views/LightboxView.tsx
index c34554b56..24433cd01 100644
--- a/src/client/views/LightboxView.tsx
+++ b/src/client/views/LightboxView.tsx
@@ -1,3 +1,6 @@
+/* eslint-disable no-use-before-define */
+/* eslint-disable jsx-a11y/no-static-element-interactions */
+/* eslint-disable jsx-a11y/click-events-have-key-events */
 import { IconProp } from '@fortawesome/fontawesome-svg-core';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { Toggle, ToggleType, Type } from 'browndash-components';
@@ -43,6 +46,7 @@ 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
     public static get LightboxDoc() { return LightboxView.Instance?._doc; } // prettier-ignore
+    // eslint-disable-next-line no-use-before-define
     static Instance: LightboxView;
     private _path: {
         doc: Opt<Doc>; //
@@ -72,11 +76,18 @@ export class LightboxView extends ObservableReactComponent<LightboxViewProps> {
     @action
     public SetLightboxDoc(doc: Opt<Doc>, target?: Doc, future?: Doc[], layoutTemplate?: Doc | string) {
         const lightDoc = this._doc;
-        lightDoc && lightDoc !== doc && savedKeys.forEach(key => (lightDoc[key] = this._savedState[key]));
+        lightDoc &&
+            lightDoc !== doc &&
+            savedKeys.forEach(key => {
+                lightDoc[key] = this._savedState[key];
+            });
         this._savedState = {};
 
         if (doc) {
-            lightDoc !== doc && savedKeys.map(key => (this._savedState[key] = Doc.Get(doc, key, true)));
+            lightDoc !== doc &&
+                savedKeys.forEach(key => {
+                    this._savedState[key] = Doc.Get(doc, key, true);
+                });
             const l = DocUtils.MakeLinkToActiveAudio(() => doc).lastElement();
             l && (Cast(l.link_anchor_2, Doc, null).backgroundColor = 'lightgreen');
             CollectionStackedTimeline.CurrentlyPlaying?.forEach(dv => dv.ComponentView?.Pause?.());
@@ -107,8 +118,9 @@ export class LightboxView extends ObservableReactComponent<LightboxViewProps> {
         return true;
     }
 
-    public AddDocTab = (doc: Doc, location: OpenWhere, layoutTemplate?: Doc | string) =>
-        this.SetLightboxDoc(
+    public AddDocTab = (docs: Doc | Doc[], location: OpenWhere, layoutTemplate?: Doc | string) => {
+        const doc = (docs instanceof Doc ? [docs] : docs).lastElement();
+        return this.SetLightboxDoc(
             doc,
             undefined,
             [...DocListCast(doc[Doc.LayoutFieldKey(doc)]), ...DocListCast(doc[Doc.LayoutFieldKey(doc) + '_annotations']).filter(anno => anno.annotationOn !== doc), ...this._future].sort(
@@ -116,6 +128,7 @@ export class LightboxView extends ObservableReactComponent<LightboxViewProps> {
             ),
             layoutTemplate
         );
+    };
     @action
     next = () => {
         const lightDoc = this._doc;
@@ -180,8 +193,12 @@ export class LightboxView extends ObservableReactComponent<LightboxViewProps> {
             this.SetLightboxDoc(undefined);
         }
     };
-    toggleFitWidth = () => this._doc && (this._doc._layout_fitWidth = !this._doc._layout_fitWidth);
-    togglePen = () => (Doc.ActiveTool = Doc.ActiveTool === InkTool.Pen ? InkTool.None : InkTool.Pen);
+    toggleFitWidth = () => {
+        this._doc && (this._doc._layout_fitWidth = !this._doc._layout_fitWidth);
+    };
+    togglePen = () => {
+        Doc.ActiveTool = Doc.ActiveTool === InkTool.Pen ? InkTool.None : InkTool.Pen;
+    };
     toggleExplore = () => SnappingManager.SetExploreMode(!SnappingManager.ExploreMode);
 
     lightboxDoc = () => this._doc;
@@ -191,33 +208,31 @@ export class LightboxView extends ObservableReactComponent<LightboxViewProps> {
     lightboxDocTemplate = () => this._layoutTemplate;
     future = () => this._future;
 
-    renderNavBtn = (left: Opt<string | number>, bottom: Opt<number>, top: number, icon: IconProp, display: any, click: () => void, color?: string) => {
-        return (
+    renderNavBtn = (left: Opt<string | number>, bottom: Opt<number>, top: number, icon: IconProp, display: any, click: () => void, color?: string) => (
+        <div
+            className="lightboxView-navBtn-frame"
+            style={{
+                display: display ? '' : 'none',
+                left,
+                width: bottom !== undefined ? undefined : Math.min(this._props.PanelWidth / 4, this._props.maxBorder[0]),
+                bottom,
+            }}>
             <div
-                className="lightboxView-navBtn-frame"
-                style={{
-                    display: display ? '' : 'none',
-                    left,
-                    width: bottom !== undefined ? undefined : Math.min(this._props.PanelWidth / 4, this._props.maxBorder[0]),
-                    bottom,
+                className="lightboxView-navBtn"
+                title={color}
+                style={{ top, color: SettingsManager.userColor, background: undefined }}
+                onClick={e => {
+                    e.stopPropagation();
+                    click();
                 }}>
-                <div
-                    className="lightboxView-navBtn"
-                    title={color}
-                    style={{ top, color: SettingsManager.userColor, background: undefined }}
-                    onClick={e => {
-                        e.stopPropagation();
-                        click();
-                    }}>
-                    <div style={{ height: 10 }}>{color}</div>
-                    <FontAwesomeIcon icon={icon} size="3x" />
-                </div>
+                <div style={{ height: 10 }}>{color}</div>
+                <FontAwesomeIcon icon={icon} size="3x" />
             </div>
-        );
-    };
+        </div>
+    );
     render() {
-        let downx = 0,
-            downy = 0;
+        let downx = 0;
+        let downy = 0;
         const toggleBtn = (classname: string, tooltip: string, toggleBackground: any, icon: IconProp, icon2: IconProp | string, onClick: () => void) => (
             <div className={classname}>
                 <Toggle
@@ -253,10 +268,12 @@ export class LightboxView extends ObservableReactComponent<LightboxViewProps> {
                         clipPath: `path('${Doc.UserDoc().renderStyle === 'comic' ? wavyBorderPath(this.lightboxWidth(), this.lightboxHeight()) : undefined}')`,
                         background: SettingsManager.userBackgroundColor,
                     }}>
-                    <GestureOverlay isActive={true}>
+                    <GestureOverlay isActive>
                         <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))}
+                            ref={action((r: DocumentView | null) => {
+                                this._docView = r !== null ? r : undefined;
+                            })}
                             Document={this._doc}
                             PanelWidth={this.lightboxWidth}
                             PanelHeight={this.lightboxHeight}
@@ -266,7 +283,7 @@ export class LightboxView extends ObservableReactComponent<LightboxViewProps> {
                             styleProvider={DefaultStyleProvider}
                             ScreenToLocalTransform={this.lightboxScreenToLocal}
                             renderDepth={0}
-                            suppressSetHeight={this._doc._layout_fitWidth ? true : false}
+                            suppressSetHeight={!!this._doc._layout_fitWidth}
                             containerViewPath={returnEmptyDoclist}
                             childFilters={returnEmptyFilter}
                             childFiltersByRanges={returnEmptyFilter}
@@ -303,6 +320,7 @@ export class LightboxView extends ObservableReactComponent<LightboxViewProps> {
 }
 interface LightboxTourBtnProps {
     navBtn: (left: Opt<string | number>, bottom: Opt<number>, top: number, icon: IconProp, display: any, click: () => void, color?: string) => JSX.Element;
+    // eslint-disable-next-line react/no-unused-prop-types
     future: () => Opt<Doc[]>;
     stepInto: () => void;
     lightboxDoc: () => Opt<Doc>;
diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx
index 17c21326d..01f3b032e 100644
--- a/src/client/views/Main.tsx
+++ b/src/client/views/Main.tsx
@@ -1,3 +1,4 @@
+/* eslint-disable no-new */
 // if ((module as any).hot) {
 //     (module as any).hot.accept();
 // }
@@ -5,7 +6,7 @@
 import * as dotenv from 'dotenv'; // see https://github.com/motdotla/dotenv#how-do-i-use-dotenv-with-import
 import * as React from 'react';
 import * as ReactDOM from 'react-dom/client';
-import { AssignAllExtensions } from '../../extensions/General/Extensions';
+import { AssignAllExtensions } from '../../extensions/Extensions';
 import { FieldLoader } from '../../fields/FieldLoader';
 import { CurrentUserUtils } from '../util/CurrentUserUtils';
 import { PingManager } from '../util/PingManager';
@@ -15,6 +16,7 @@ import { CollectionView } from './collections/CollectionView';
 import './global/globalScripts';
 import { MainView } from './MainView';
 import { BranchingTrailManager } from '../util/BranchingTrailManager';
+
 dotenv.config();
 
 AssignAllExtensions();
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index a13fe1cb7..10d423c05 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -747,7 +747,8 @@ export class MainView extends ObservableReactComponent<{}> {
 
     sidebarScreenToLocal = () => new Transform(0, -this.topOfSidebarDoc, 1);
     mainContainerXf = () => this.sidebarScreenToLocal().translate(-this.leftScreenOffsetOfMainDocView, 0);
-    static addDocTabFunc_impl = (doc: Doc, location: OpenWhere): boolean => {
+    static addDocTabFunc_impl = (docs: Doc | Doc[], location: OpenWhere): boolean => {
+        const doc = (docs instanceof Doc ? [docs] : docs).lastElement();
         const whereFields = location.split(':');
         const keyValue = whereFields.includes(OpenWhereMod.keyvalue);
         const whereMods = whereFields.length > 1 ? (whereFields[1] as OpenWhereMod) : OpenWhereMod.none;
@@ -957,7 +958,7 @@ export class MainView extends ObservableReactComponent<{}> {
                     childFiltersByRanges={returnEmptyFilter}
                     searchFilterDocs={returnEmptyDoclist}
                 />
-                {['watching', 'recording'].includes(StrCast(this.userDoc?.presentationMode)) ? <div style={{ border: '.5rem solid green', padding: '5px' }}>{StrCast(this.userDoc?.presentationMode)}</div> : <></>}
+                {['watching', 'recording'].includes(StrCast(this.userDoc?.presentationMode)) ? <div style={{ border: '.5rem solid green', padding: '5px' }}>{StrCast(this.userDoc?.presentationMode)}</div> : null}
             </div>
         );
     }
diff --git a/src/client/views/MainViewModal.tsx b/src/client/views/MainViewModal.tsx
index e52562625..a6dc5c62b 100644
--- a/src/client/views/MainViewModal.tsx
+++ b/src/client/views/MainViewModal.tsx
@@ -1,3 +1,6 @@
+/* eslint-disable jsx-a11y/no-static-element-interactions */
+/* eslint-disable jsx-a11y/click-events-have-key-events */
+/* eslint-disable react/require-default-props */
 import { isDark } from 'browndash-components';
 import { observer } from 'mobx-react';
 import * as React from 'react';
@@ -11,7 +14,6 @@ export interface MainViewOverlayProps {
     dialogueBoxStyle?: React.CSSProperties;
     overlayStyle?: React.CSSProperties;
     dialogueBoxDisplayedOpacity?: number;
-    overlayDisplayedOpacity?: number;
     closeOnExternalClick?: () => void; // the close method of a MainViewModal, triggered if there is a click on the overlay (closing the modal)
 }
 
@@ -20,7 +22,6 @@ export class MainViewModal extends React.Component<MainViewOverlayProps> {
     render() {
         const p = this.props;
         const dialogueOpacity = p.dialogueBoxDisplayedOpacity || 1;
-        const overlayOpacity = p.overlayDisplayedOpacity || 0.4;
         return !p.isDisplayed ? null : (
             <div
                 className="mainViewModal-cont"
diff --git a/src/client/views/MarqueeAnnotator.tsx b/src/client/views/MarqueeAnnotator.tsx
index f92bd68a3..0301632ec 100644
--- a/src/client/views/MarqueeAnnotator.tsx
+++ b/src/client/views/MarqueeAnnotator.tsx
@@ -16,7 +16,6 @@ import { DocumentView } from './nodes/DocumentView';
 import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox';
 import { ObservableReactComponent } from './ObservableReactComponent';
 import { AnchorMenu } from './pdf/AnchorMenu';
-const _global = (window /* browser */ || global) /* node */ as any;
 
 export interface MarqueeAnnotatorProps {
     Document: Doc;
@@ -101,8 +100,8 @@ export class MarqueeAnnotator extends ObservableReactComponent<MarqueeAnnotatorP
         let minY = Number.MAX_VALUE;
         let maxY = -Number.MIN_VALUE;
         const annoDocs: Doc[] = [];
-        savedAnnoMap.forEach((value: HTMLDivElement[], key: number) =>
-            value.map(anno => {
+        savedAnnoMap.forEach((value: HTMLDivElement[]) =>
+            value.forEach(anno => {
                 const textRegion = new Doc();
                 textRegion.x = parseInt(anno.style.left ?? '0');
                 textRegion.y = parseInt(anno.style.top ?? '0');
@@ -131,7 +130,7 @@ export class MarqueeAnnotator extends ObservableReactComponent<MarqueeAnnotatorP
         return textRegionAnno;
     };
     @action
-    highlight = (color: string, isLinkButton: boolean, savedAnnotations?: ObservableMap<number, HTMLDivElement[]>, addAsAnnotation?: boolean, summarize?: boolean) => {
+    highlight = (color: string, isLinkButton: boolean, savedAnnotations?: ObservableMap<number, HTMLDivElement[]>, addAsAnnotation?: boolean) => {
         // creates annotation documents for current highlights
         const effectiveAcl = GetEffectiveAcl(this.props.Document[DocData]);
         const annotationDoc = [AclAugment, AclSelfEdit, AclEdit, AclAdmin].includes(effectiveAcl) && this.makeAnnotationDocument(color, isLinkButton, savedAnnotations);
@@ -157,7 +156,7 @@ export class MarqueeAnnotator extends ObservableReactComponent<MarqueeAnnotatorP
     //    3) localize the unrotated vector by scaling into the marquee container's coordinates
     //    4) reattach the vector to the center of the bounding box
     getTransformedScreenPt = (down: number[]) => {
-        const marqueeContainer = this.props.marqueeContainer;
+        const { marqueeContainer } = this.props;
         const containerXf = this.props.isNativeScaled ? this.props.docView().screenToContentsTransform() : this.props.docView().screenToViewTransform();
         const boundingRect = marqueeContainer.getBoundingClientRect();
         const center = { x: boundingRect.x + boundingRect.width / 2, y: boundingRect.y + boundingRect.height / 2 };
@@ -177,15 +176,15 @@ export class MarqueeAnnotator extends ObservableReactComponent<MarqueeAnnotatorP
         document.addEventListener('pointermove', this.onSelectMove);
         document.addEventListener('pointerup', this.onSelectEnd);
 
-        AnchorMenu.Instance.OnCrop = (e: PointerEvent) => {
+        AnchorMenu.Instance.OnCrop = () => {
             if (this.props.anchorMenuCrop) {
                 UndoManager.RunInBatch(() => this.props.anchorMenuCrop?.(this.highlight('', true, undefined, false), true), 'cropping');
             }
         };
-        AnchorMenu.Instance.OnClick = undoable((e: PointerEvent) => this.props.anchorMenuClick?.()?.(this.highlight(this.props.highlightDragSrcColor ?? 'rgba(173, 216, 230, 0.75)', true, undefined, true)), 'make sidebar annotation');
+        AnchorMenu.Instance.OnClick = undoable(() => this.props.anchorMenuClick?.()?.(this.highlight(this.props.highlightDragSrcColor ?? 'rgba(173, 216, 230, 0.75)', true, undefined, true)), 'make sidebar annotation');
         AnchorMenu.Instance.OnAudio = unimplementedFunction;
         AnchorMenu.Instance.Highlight = (color: string) => this.highlight(color, false, undefined, true);
-        AnchorMenu.Instance.GetAnchor = (savedAnnotations?: ObservableMap<number, HTMLDivElement[]>, addAsAnnotation?: boolean) => this.highlight('rgba(173, 216, 230, 0.75)', true, savedAnnotations, true);
+        AnchorMenu.Instance.GetAnchor = (savedAnnotations?: ObservableMap<number, HTMLDivElement[]> /* , addAsAnnotation?: boolean */) => this.highlight('rgba(173, 216, 230, 0.75)', true, savedAnnotations, true);
         AnchorMenu.Instance.onMakeAnchor = () => AnchorMenu.Instance.GetAnchor(undefined, true);
 
         /**
@@ -220,13 +219,14 @@ export class MarqueeAnnotator extends ObservableReactComponent<MarqueeAnnotatorP
             : action((e: PointerEvent, ele: HTMLElement) => {
                   e.preventDefault();
                   e.stopPropagation();
-                  var cropRegion: Doc | undefined;
+                  let cropRegion: Doc | undefined;
+                  // eslint-disable-next-line no-return-assign
                   const sourceAnchorCreator = () => (cropRegion = this.highlight('', true, undefined, true)); // hyperlink color
-                  const targetCreator = (annotationOn: Doc | undefined) => this.props.anchorMenuCrop!(cropRegion, false)!;
+                  const targetCreator = (/* annotationOn: Doc | undefined */) => this.props.anchorMenuCrop!(cropRegion, false)!;
                   DragManager.StartAnchorAnnoDrag([ele], new DragManager.AnchorAnnoDragData(this.props.docView(), sourceAnchorCreator, targetCreator), e.pageX, e.pageY, {
-                      dragComplete: e => {
-                          if (!e.aborted && e.linkDocument) {
-                              const linkDocData = e.linkDocument[DocData];
+                      dragComplete: dragEx => {
+                          if (!dragEx.aborted && dragEx.linkDocument) {
+                              const linkDocData = dragEx.linkDocument[DocData];
                               linkDocData.link_relationship = 'cropped image';
                               linkDocData.title = 'crop: ' + this.props.Document.title;
                               linkDocData.link_displayLine = false;
@@ -252,7 +252,7 @@ export class MarqueeAnnotator extends ObservableReactComponent<MarqueeAnnotatorP
         const movLoc = this.getTransformedScreenPt([e.clientX, e.clientY]);
         this._width = movLoc.x - this._start.x;
         this._height = movLoc.y - this._start.y;
-        //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.
+        // 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.
     };
 
     onSelectEnd = (e: PointerEvent) => {
@@ -268,7 +268,9 @@ export class MarqueeAnnotator extends ObservableReactComponent<MarqueeAnnotatorP
             // copy the temporary marquee to allow for multiple selections (not currently available though).
             const copy = document.createElement('div');
             const scale = (this.props.scaling?.() || 1) * NumCast(this.props.Document._freeform_scale, 1);
-            ['border', 'opacity', 'top', 'left', 'width', 'height'].forEach(prop => (copy.style[prop as any] = marqueeStyle[prop as any]));
+            ['border', 'opacity', 'top', 'left', 'width', 'height'].forEach(prop => {
+                copy.style[prop as any] = marqueeStyle[prop as any];
+            });
             copy.className = 'marqueeAnnotator-annotationBox';
             copy.style.top = parseInt(marqueeStyle.top.toString().replace('px', '')) / scale + this.props.scrollTop + 'px';
             copy.style.left = parseInt(marqueeStyle.left.toString().replace('px', '')) / scale + 'px';
diff --git a/src/client/views/PreviewCursor.tsx b/src/client/views/PreviewCursor.tsx
index 6d03034ec..fddb40624 100644
--- a/src/client/views/PreviewCursor.tsx
+++ b/src/client/views/PreviewCursor.tsx
@@ -13,6 +13,7 @@ import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox';
 
 @observer
 export class PreviewCursor extends ObservableReactComponent<{}> {
+    // eslint-disable-next-line no-use-before-define
     static _instance: PreviewCursor;
     public static get Instance() {
         return PreviewCursor._instance;
@@ -39,7 +40,9 @@ export class PreviewCursor extends ObservableReactComponent<{}> {
     paste = async (e: ClipboardEvent) => {
         if (this.Visible && e.clipboardData) {
             const newPoint = this._getTransform?.().transformPoint(this._clickPoint[0], this._clickPoint[1]);
-            runInAction(() => (this.Visible = false));
+            runInAction(() => {
+                this.Visible = false;
+            });
 
             // tests for URL and makes web document
             const re: any = /^https?:\/\//g;
@@ -88,10 +91,10 @@ export class PreviewCursor extends ObservableReactComponent<{}> {
                     UndoManager.RunInBatch(() => this._addLiveTextDoc?.(DocUtils.GetNewTextDoc('', newPoint[0], newPoint[1], 500, undefined, undefined)), 'paste');
                 }
             }
-            //pasting in images
+            // pasting in images
             else if (e.clipboardData.getData('text/html') !== '' && e.clipboardData.getData('text/html').includes('<img src=')) {
-                const re: any = /<img src="(.*?)"/g;
-                const arr: any[] = re.exec(e.clipboardData.getData('text/html'));
+                const regEx: any = /<img src="(.*?)"/g;
+                const arr: any[] = regEx.exec(e.clipboardData.getData('text/html'));
 
                 if (newPoint) {
                     undoBatch(() => {
@@ -122,7 +125,7 @@ export class PreviewCursor extends ObservableReactComponent<{}> {
     @action
     onKeyDown = (e: KeyboardEvent) => {
         // Mixing events between React and Native is finicky.
-        //if not these keys, make a textbox if preview cursor is active!
+        // if not these keys, make a textbox if preview cursor is active!
         if (
             e.key !== 'Escape' &&
             e.key !== 'Backspace' &&
@@ -162,7 +165,7 @@ export class PreviewCursor extends ObservableReactComponent<{}> {
         }
     };
 
-    //when focus is lost, this will remove the preview cursor
+    // when focus is lost, this will remove the preview cursor
     @action onBlur = (): void => {
         this.Visible = false;
     };
@@ -192,6 +195,7 @@ export class PreviewCursor extends ObservableReactComponent<{}> {
     }
     render() {
         return !this._clickPoint || !this.Visible ? null : (
+            // eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
             <div className="previewCursor" onBlur={this.onBlur} tabIndex={0} ref={e => e?.focus()} style={{ color: lightOrDark(this.Doc?.backgroundColor ?? 'white'), transform: `translate(${this._clickPoint[0]}px, ${this._clickPoint[1]}px)` }}>
                 I
             </div>
diff --git a/src/client/views/PropertiesDocBacklinksSelector.tsx b/src/client/views/PropertiesDocBacklinksSelector.tsx
index cf5105efc..be5b7ec1f 100644
--- a/src/client/views/PropertiesDocBacklinksSelector.tsx
+++ b/src/client/views/PropertiesDocBacklinksSelector.tsx
@@ -1,3 +1,4 @@
+/* eslint-disable react/no-unused-prop-types */
 import { action } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
diff --git a/src/client/views/PropertiesDocContextSelector.tsx b/src/client/views/PropertiesDocContextSelector.tsx
index b8bbde9de..2d04f2fe3 100644
--- a/src/client/views/PropertiesDocContextSelector.tsx
+++ b/src/client/views/PropertiesDocContextSelector.tsx
@@ -1,7 +1,10 @@
+/* eslint-disable jsx-a11y/no-static-element-interactions */
+/* eslint-disable jsx-a11y/click-events-have-key-events */
+/* eslint-disable jsx-a11y/anchor-is-valid */
 import { computed, makeObservable } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
-import { Doc, DocListCast } from '../../fields/Doc';
+import { Doc } from '../../fields/Doc';
 import { Id } from '../../fields/FieldSymbols';
 import { Cast, StrCast } from '../../fields/Types';
 import { DocFocusOrOpen } from '../util/DocumentManager';
@@ -52,9 +55,9 @@ export class PropertiesDocContextSelector extends ObservableReactComponent<Prope
             .map(doc => ({ col: doc, target }));
     }
 
-    getOnClick = (col: Doc, target: Doc) => {
+    getOnClick = (clickCol: Doc) => {
         if (!this._props.DocView) return;
-        col = Doc.IsDataProto(col) ? Doc.MakeDelegate(col) : col;
+        const col = Doc.IsDataProto(clickCol) ? Doc.MakeDelegate(clickCol) : clickCol;
         DocFocusOrOpen(Doc.GetProto(this._props.DocView.Document), undefined, col);
     };
 
@@ -65,7 +68,7 @@ export class PropertiesDocContextSelector extends ObservableReactComponent<Prope
                 {this._props.hideTitle ? null : <p key="contexts">Contexts:</p>}
                 {this._docs.map(doc => (
                     <p key={doc.col[Id] + doc.target[Id]}>
-                        <a onClick={() => this.getOnClick(doc.col, doc.target)}>{StrCast(doc.col.title)}</a>
+                        <a onClick={() => this.getOnClick(doc.col)}>{StrCast(doc.col.title)}</a>
                     </p>
                 ))}
             </div>
diff --git a/src/client/views/PropertiesSection.tsx b/src/client/views/PropertiesSection.tsx
index 3c9fa1123..f083f03b9 100644
--- a/src/client/views/PropertiesSection.tsx
+++ b/src/client/views/PropertiesSection.tsx
@@ -1,5 +1,8 @@
+/* eslint-disable jsx-a11y/no-static-element-interactions */
+/* eslint-disable jsx-a11y/click-events-have-key-events */
+/* eslint-disable react/require-default-props */
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { action, computed, observable } from 'mobx';
+import { action, computed } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
 import { SettingsManager } from '../util/SettingsManager';
@@ -10,7 +13,6 @@ export interface PropertiesSectionProps {
     children?: JSX.Element | string | null;
     isOpen: boolean;
     setIsOpen: (bool: boolean) => any;
-    inSection?: boolean;
     setInSection?: (bool: boolean) => any;
     onDoubleClick?: () => void;
 }
@@ -21,44 +23,35 @@ export class PropertiesSection extends React.Component<PropertiesSectionProps> {
         return SettingsManager.userColor;
     }
 
-    @computed get backgroundColor() {
-        return SettingsManager.userBackgroundColor;
-    }
-
     @computed get variantColor() {
         return SettingsManager.userVariantColor;
     }
 
-    @observable isDouble: boolean = false;
-
     render() {
         if (this.props.children === undefined || this.props.children === null) return null;
-        else
-            return (
-                <div className="propertiesView-section" onPointerEnter={action(() => this.props.setInSection && this.props.setInSection(true))} onPointerLeave={action(() => this.props.setInSection && this.props.setInSection(false))}>
-                    <div
-                        className="propertiesView-sectionTitle"
-                        onDoubleClick={action(e => {
-                            this.isDouble = true;
-                            this.props.onDoubleClick && this.props.onDoubleClick();
-                            this.props.setIsOpen(true);
-                            setTimeout(() => (this.isDouble = false), 300);
-                        })}
-                        onClick={action(e => {
-                            this.props.setIsOpen(!this.props.isOpen);
-                        })}
-                        style={{
-                            background: this.variantColor,
-                            // this.props.isOpen ? this.variantColor : this.backgroundColor,
-                            color: this.color,
-                        }}>
-                        {this.props.title}
-                        <div className="propertiesView-sectionTitle-icon">
-                            <FontAwesomeIcon icon={this.props.isOpen ? 'caret-down' : 'caret-right'} size="lg" />
-                        </div>
+        return (
+            <div className="propertiesView-section" onPointerEnter={action(() => this.props.setInSection && this.props.setInSection(true))} onPointerLeave={action(() => this.props.setInSection && this.props.setInSection(false))}>
+                <div
+                    className="propertiesView-sectionTitle"
+                    onDoubleClick={action(() => {
+                        this.props.onDoubleClick && this.props.onDoubleClick();
+                        this.props.setIsOpen(true);
+                    })}
+                    onClick={action(() => {
+                        this.props.setIsOpen(!this.props.isOpen);
+                    })}
+                    style={{
+                        background: this.variantColor,
+                        // this.props.isOpen ? this.variantColor : this.backgroundColor,
+                        color: this.color,
+                    }}>
+                    {this.props.title}
+                    <div className="propertiesView-sectionTitle-icon">
+                        <FontAwesomeIcon icon={this.props.isOpen ? 'caret-down' : 'caret-right'} size="lg" />
                     </div>
-                    {!this.props.isOpen ? null : <div className="propertiesView-content">{this.props.children}</div>}
                 </div>
-            );
+                {!this.props.isOpen ? null : <div className="propertiesView-content">{this.props.children}</div>}
+            </div>
+        );
     }
 }
diff --git a/src/client/views/ScriptBox.tsx b/src/client/views/ScriptBox.tsx
index 365980f33..8de359e07 100644
--- a/src/client/views/ScriptBox.tsx
+++ b/src/client/views/ScriptBox.tsx
@@ -1,17 +1,18 @@
+/* eslint-disable react/require-default-props */
 import { action, makeObservable, observable } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
+import { emptyFunction } from '../../Utils';
 import { Doc, Opt } from '../../fields/Doc';
+import { DocData } from '../../fields/DocSymbols';
 import { ScriptField } from '../../fields/ScriptField';
 import { ScriptCast } from '../../fields/Types';
-import { emptyFunction } from '../../ClientUtils';
 import { DragManager } from '../util/DragManager';
 import { CompileScript } from '../util/Scripting';
 import { EditableView } from './EditableView';
-import { DocumentIconContainer } from './nodes/DocumentIcon';
 import { OverlayView } from './OverlayView';
 import './ScriptBox.scss';
-import { DocData } from '../../fields/DocSymbols';
+import { DocumentIconContainer } from './nodes/DocumentIcon';
 
 export interface ScriptBoxProps {
     onSave: (text: string, onError: (error: string) => void) => void;
@@ -25,6 +26,7 @@ export interface ScriptBoxProps {
 export class ScriptBox extends React.Component<ScriptBoxProps> {
     @observable
     private _scriptText: string;
+    overlayDisposer?: () => void;
 
     constructor(props: ScriptBoxProps) {
         super(props);
@@ -42,7 +44,6 @@ export class ScriptBox extends React.Component<ScriptBoxProps> {
         console.log('ScriptBox: ' + error);
     };
 
-    overlayDisposer?: () => void;
     onFocus = () => {
         this.overlayDisposer?.();
         this.overlayDisposer = OverlayView.Instance.addElement(<DocumentIconContainer />, { x: 0, y: 0 });
@@ -53,21 +54,35 @@ export class ScriptBox extends React.Component<ScriptBoxProps> {
     };
 
     render() {
-        let onFocus: Opt<() => void> = undefined,
-            onBlur: Opt<() => void> = undefined;
+        let onFocus: Opt<() => void>;
+        let onBlur: Opt<() => void>;
         if (this.props.showDocumentIcons) {
             onFocus = this.onFocus;
             onBlur = this.onBlur;
         }
-        const params = <EditableView contents={''} display={'block'} maxHeight={72} height={35} fontSize={28} GetValue={() => ''} SetValue={(value: string) => (this.props.setParams?.(value.split(' ').filter(s => s !== ' ')) ? true : true)} />;
+        const params = (
+            <EditableView
+                contents=""
+                display="block"
+                maxHeight={72}
+                height={35}
+                fontSize={28}
+                GetValue={() => ''}
+                SetValue={(value: string) => {
+                    this.props.setParams?.(value.split(' ').filter(s => s !== ' '));
+                    return true;
+                }}
+            />
+        );
         return (
             <div className="scriptBox-outerDiv">
                 <div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
-                    <textarea className="scriptBox-textarea" onChange={this.onChange} value={this._scriptText} onFocus={onFocus} onBlur={onBlur}></textarea>
+                    <textarea className="scriptBox-textarea" onChange={this.onChange} value={this._scriptText} onFocus={onFocus} onBlur={onBlur} />
                     <div style={{ background: 'beige' }}>{params}</div>
                 </div>
                 <div className="scriptBox-toolbar">
                     <button
+                        type="button"
                         onClick={e => {
                             this.props.onSave(this._scriptText, this.onError);
                             e.stopPropagation();
@@ -75,6 +90,7 @@ export class ScriptBox extends React.Component<ScriptBoxProps> {
                         Save
                     </button>
                     <button
+                        type="button"
                         onClick={e => {
                             this.props.onCancel && this.props.onCancel();
                             e.stopPropagation();
@@ -85,11 +101,12 @@ export class ScriptBox extends React.Component<ScriptBoxProps> {
             </div>
         );
     }
-    //let l = docList(this.source[0].data).length; if (l) { let ind = this.target[0].index !== undefined ? (this.target[0].index+1) % l : 0;  this.target[0].index = ind;  this.target[0].proto = getProto(docList(this.source[0].data)[ind]);}
+    // let l = docList(this.source[0].data).length; if (l) { let ind = this.target[0].index !== undefined ? (this.target[0].index+1) % l : 0;  this.target[0].index = ind;  this.target[0].proto = getProto(docList(this.source[0].data)[ind]);}
+    // eslint-disable-next-line react/sort-comp
     public static EditButtonScript(title: string, doc: Doc, fieldKey: string, clientX: number, clientY: number, contextParams?: { [name: string]: string }, defaultScript?: ScriptField) {
         let overlayDisposer: () => void = emptyFunction;
         const script = ScriptCast(doc[fieldKey]) || defaultScript;
-        let originalText: string | undefined = undefined;
+        let originalText: string | undefined;
         if (script) {
             originalText = script.script.originalScript;
         }
@@ -124,7 +141,7 @@ export class ScriptBox extends React.Component<ScriptBoxProps> {
                         div.style.display = 'inline-block';
                         div.style.transform = `translate(${clientX}px, ${clientY}px)`;
                         div.innerHTML = 'button';
-                        params.length && DragManager.StartButtonDrag([div], text, doc.title + '-instance', {}, params, (button: Doc) => {}, clientX, clientY);
+                        params.length && DragManager.StartButtonDrag([div], text, doc.title + '-instance', {}, params, () => {}, clientX, clientY);
 
                         doc[DocData][fieldKey] = new ScriptField(script);
                         overlayDisposer();
diff --git a/src/debug/Repl.tsx b/src/debug/Repl.tsx
index a9f7c085f..8ac502348 100644
--- a/src/debug/Repl.tsx
+++ b/src/debug/Repl.tsx
@@ -30,7 +30,7 @@ class Repl extends React.Component {
             if (!script.compiled) {
                 this.executedCommands.push({ command: this.text, result: 'Compile Error' });
             } else {
-                const result = script.run({ makeInterface }, e => this.executedCommands.push({ command: this.text, result: e.message || e }));
+                const result = script.run({ makeInterface }, err => this.executedCommands.push({ command: this.text, result: err.message || err }));
                 result.success && this.executedCommands.push({ command: this.text, result: result.result });
             }
             this.text = '';
@@ -39,15 +39,13 @@ class Repl extends React.Component {
 
     @computed
     get commands() {
-        return this.executedCommands.map(command => {
-            return (
-                <div style={{ marginTop: '5px' }}>
-                    <p>{command.command}</p>
-                    {/* <pre>{JSON.stringify(command.result, null, 2)}</pre> */}
-                    <pre>{command.result instanceof RefField || command.result instanceof ObjectField ? 'object' : String(command.result)}</pre>
-                </div>
-            );
-        });
+        return this.executedCommands.map(command => (
+            <div style={{ marginTop: '5px' }}>
+                <p>{command.command}</p>
+                {/* <pre>{JSON.stringify(command.result, null, 2)}</pre> */}
+                <pre>{command.result instanceof RefField || command.result instanceof ObjectField ? 'object' : String(command.result)}</pre>
+            </div>
+        ));
     }
 
     render() {
diff --git a/src/debug/Test.tsx b/src/debug/Test.tsx
index c906dcc03..856cf6bd4 100644
--- a/src/debug/Test.tsx
+++ b/src/debug/Test.tsx
@@ -1,11 +1,12 @@
 import * as React from 'react';
 import * as ReactDOM from 'react-dom/client';
-console.log('ENTERED');
+
 class Test extends React.Component {
     render() {
         return <div> HELLO WORLD </div>;
     }
 }
+
 const root = ReactDOM.createRoot(document.getElementById('root')!);
 
 root.render(<Test />);
diff --git a/src/debug/Viewer.tsx b/src/debug/Viewer.tsx
index fc62463e6..f3295a6c6 100644
--- a/src/debug/Viewer.tsx
+++ b/src/debug/Viewer.tsx
@@ -1,7 +1,12 @@
+/* eslint-disable react/no-unescaped-entities */
+/* eslint-disable react/button-has-type */
+/* eslint-disable no-use-before-define */
+/* eslint-disable react/no-array-index-key */
+/* eslint-disable no-redeclare */
 import { action, configure, observable } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
-import * as ReactDOM from 'react-dom';
+import * as ReactDOM from 'react-dom/client';
 import { DocServer } from '../client/DocServer';
 import { resolvedPorts } from '../client/util/CurrentUserUtils';
 import { CompileScript } from '../client/util/Scripting';
@@ -86,14 +91,12 @@ class DocumentViewer extends React.Component<{ field: Doc }> {
         let content;
         if (this.expanded) {
             const keys = Object.keys(this.props.field);
-            const fields = keys.map(key => {
-                return (
-                    <div key={key}>
-                        <b>({key}): </b>
-                        <DebugViewer field={this.props.field[key]} setValue={value => applyToDoc(this.props.field, key, value)}></DebugViewer>
-                    </div>
-                );
-            });
+            const fields = keys.map(key => (
+                <div key={key}>
+                    <b>({key}): </b>
+                    <DebugViewer field={this.props.field[key]} setValue={value => applyToDoc(this.props.field, key, value)} />
+                </div>
+            ));
             content = (
                 <div>
                     Document ({this.props.field[Id]})<div style={{ paddingLeft: '25px' }}>{fields}</div>
@@ -115,7 +118,7 @@ class DocumentViewer extends React.Component<{ field: Doc }> {
 class DebugViewer extends React.Component<{ field: FieldResult; setValue(value: string): boolean }> {
     render() {
         let content;
-        const field = this.props.field;
+        const { field } = this.props;
         if (field instanceof List) {
             content = <ListViewer field={field} />;
         } else if (field instanceof Doc) {
@@ -171,7 +174,7 @@ class Viewer extends React.Component {
                 <input value={this.idToAdd} onChange={this.inputOnChange} onKeyDown={this.onKeyDown} />
                 <div>
                     {this.fields.map((field, index) => (
-                        <DebugViewer field={field} key={index} setValue={() => false}></DebugViewer>
+                        <DebugViewer field={field} key={index} setValue={() => false} />
                     ))}
                 </div>
             </>
@@ -181,10 +184,13 @@ class Viewer extends React.Component {
 
 (async function () {
     await DocServer.init(window.location.protocol, window.location.hostname, resolvedPorts.socket, 'viewer');
-    ReactDOM.render(
-        <div style={{ position: 'absolute', width: '100%', height: '100%' }}>
-            <Viewer />
-        </div>,
-        document.getElementById('root')
-    );
+    const root = document.getElementById('root');
+    if (root) {
+        const reactDom = ReactDOM.createRoot(root);
+        reactDom.render(
+            <div style={{ position: 'absolute', width: '100%', height: '100%' }}>
+                <Viewer />
+            </div>
+        );
+    }
 })();
diff --git a/src/extensions/ArrayExtensions.ts b/src/extensions/ArrayExtensions.ts
deleted file mode 100644
index 8e125766d..000000000
--- a/src/extensions/ArrayExtensions.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-export default class ArrayExtension {
-    private readonly property: string;
-    private readonly body: <T>(this: Array<T>) => any;
-
-    constructor(property: string, body: <T>(this: Array<T>) => any) {
-        this.property = property;
-        this.body = body;
-    }
-
-    assign() {
-        Object.defineProperty(Array.prototype, this.property, {
-            value: this.body,
-            enumerable: false
-        });
-    }
-
-}
-
-/**
- * IMPORTANT: Any extension you add here *must* have a corresponding type definition
- * in the Array<T> interface in ./General/ExtensionsTypings.ts. Otherwise,
- * Typescript will not recognize your new function.
- */
-const extensions = [
-    new ArrayExtension("lastElement", function () {
-        if (!this.length) {
-            return undefined;
-        }
-        return this[this.length - 1];
-    })
-];
-
-function Assign() {
-    extensions.forEach(extension => extension.assign());
-}
-
-export { Assign };
\ No newline at end of file
diff --git a/src/extensions/Extensions.ts b/src/extensions/Extensions.ts
new file mode 100644
index 000000000..5815527d1
--- /dev/null
+++ b/src/extensions/Extensions.ts
@@ -0,0 +1,9 @@
+import { Assign as ArrayAssign } from './Extensions_Array';
+import { Assign as StringAssign } from './Extensions_String';
+
+function AssignAllExtensions() {
+    ArrayAssign();
+    StringAssign();
+}
+
+export { AssignAllExtensions };
diff --git a/src/extensions/ExtensionsTypings.ts b/src/extensions/ExtensionsTypings.ts
new file mode 100644
index 000000000..d6ffd3be3
--- /dev/null
+++ b/src/extensions/ExtensionsTypings.ts
@@ -0,0 +1,9 @@
+/* eslint-disable @typescript-eslint/no-unused-vars */
+interface Array<T> {
+    lastElement(): T;
+}
+
+interface String {
+    removeTrailingNewlines(): string;
+    hasNewline(): boolean;
+}
diff --git a/src/extensions/Extensions_Array.ts b/src/extensions/Extensions_Array.ts
new file mode 100644
index 000000000..a50fb330f
--- /dev/null
+++ b/src/extensions/Extensions_Array.ts
@@ -0,0 +1,37 @@
+export default class ArrayExtension {
+    private readonly property: string;
+    private readonly body: <T>(this: Array<T>) => any;
+
+    constructor(property: string, body: <T>(this: Array<T>) => any) {
+        this.property = property;
+        this.body = body;
+    }
+
+    assign() {
+        // eslint-disable-next-line no-extend-native
+        Object.defineProperty(Array.prototype, this.property, {
+            value: this.body,
+            enumerable: false,
+        });
+    }
+}
+
+/**
+ * IMPORTANT: Any extension you add here *must* have a corresponding type definition
+ * in the Array<T> interface in ./General/ExtensionsTypings.ts. Otherwise,
+ * Typescript will not recognize your new function.
+ */
+const extensions = [
+    new ArrayExtension('lastElement', function () {
+        if (!this.length) {
+            return undefined;
+        }
+        return this[this.length - 1];
+    }),
+];
+
+function Assign() {
+    extensions.forEach(extension => extension.assign());
+}
+
+export { Assign };
diff --git a/src/extensions/Extensions_String.ts b/src/extensions/Extensions_String.ts
new file mode 100644
index 000000000..c95095f8e
--- /dev/null
+++ b/src/extensions/Extensions_String.ts
@@ -0,0 +1,16 @@
+/* eslint-disable no-extend-native */
+function Assign() {
+    String.prototype.removeTrailingNewlines = function () {
+        let sliced = this;
+        while (sliced.endsWith('\n')) {
+            sliced = sliced.substring(0, this.length - 1);
+        }
+        return sliced as string;
+    };
+
+    String.prototype.hasNewline = function () {
+        return this.endsWith('\n');
+    };
+}
+
+export { Assign };
diff --git a/src/extensions/General/Extensions.ts b/src/extensions/General/Extensions.ts
deleted file mode 100644
index 4b6d05d5f..000000000
--- a/src/extensions/General/Extensions.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-import { Assign as ArrayAssign } from "../ArrayExtensions";
-import { Assign as StringAssign } from "../StringExtensions";
-
-function AssignAllExtensions() {
-    ArrayAssign();
-    StringAssign();
-}
-
-export { AssignAllExtensions };
\ No newline at end of file
diff --git a/src/extensions/General/ExtensionsTypings.ts b/src/extensions/General/ExtensionsTypings.ts
deleted file mode 100644
index 370157ed0..000000000
--- a/src/extensions/General/ExtensionsTypings.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-interface Array<T> {
-    lastElement(): T;
-}
-
-interface String {
-    removeTrailingNewlines(): string;
-    hasNewline(): boolean;
-}
\ No newline at end of file
diff --git a/src/extensions/StringExtensions.ts b/src/extensions/StringExtensions.ts
deleted file mode 100644
index 2c76e56c8..000000000
--- a/src/extensions/StringExtensions.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-function Assign() {
-
-    String.prototype.removeTrailingNewlines = function () {
-        let sliced = this;
-        while (sliced.endsWith("\n")) {
-            sliced = sliced.substring(0, this.length - 1);
-        }
-        return sliced as string;
-    };
-
-    String.prototype.hasNewline = function () {
-        return this.endsWith("\n");
-    };
-
-}
-
-export { Assign };
\ No newline at end of file
diff --git a/src/fields/DateField.ts b/src/fields/DateField.ts
index 56a3177f8..f0a851ce6 100644
--- a/src/fields/DateField.ts
+++ b/src/fields/DateField.ts
@@ -1,4 +1,4 @@
-import { serializable, date } from 'serializr';
+import { serializable, date as serializrDate } from 'serializr';
 import { Deserializable } from '../client/util/SerializationHelper';
 import { ObjectField } from './ObjectField';
 import { Copy, ToJavascriptString, ToScriptString, ToString } from './FieldSymbols';
@@ -7,7 +7,7 @@ import { scriptingGlobal, ScriptingGlobals } from '../client/util/ScriptingGloba
 @scriptingGlobal
 @Deserializable('date')
 export class DateField extends ObjectField {
-    @serializable(date())
+    @serializable(serializrDate())
     readonly date: Date;
 
     constructor(date: Date = new Date()) {
diff --git a/src/fields/HtmlField.ts b/src/fields/HtmlField.ts
index b67f0f7e9..536f5ce4c 100644
--- a/src/fields/HtmlField.ts
+++ b/src/fields/HtmlField.ts
@@ -1,7 +1,7 @@
+import { primitive, serializable } from 'serializr';
 import { Deserializable } from '../client/util/SerializationHelper';
-import { serializable, primitive } from 'serializr';
-import { ObjectField } from './ObjectField';
 import { Copy, ToJavascriptString, ToScriptString, ToString } from './FieldSymbols';
+import { ObjectField } from './ObjectField';
 
 @Deserializable('html')
 export class HtmlField extends ObjectField {
diff --git a/src/fields/IconField.ts b/src/fields/IconField.ts
index 4d2badb68..33e5be7af 100644
--- a/src/fields/IconField.ts
+++ b/src/fields/IconField.ts
@@ -1,5 +1,5 @@
-import { Deserializable } from '../client/util/SerializationHelper';
 import { serializable, primitive } from 'serializr';
+import { Deserializable } from '../client/util/SerializationHelper';
 import { ObjectField } from './ObjectField';
 import { Copy, ToJavascriptString, ToScriptString, ToString } from './FieldSymbols';
 
diff --git a/src/fields/List.ts b/src/fields/List.ts
index f97f208fe..852b05bc6 100644
--- a/src/fields/List.ts
+++ b/src/fields/List.ts
@@ -1,5 +1,5 @@
 import { action, computed, makeObservable, observable } from 'mobx';
-import { alias, list, serializable } from 'serializr';
+import { alias, list as serializrList, serializable } from 'serializr';
 import { ScriptingGlobals } from '../client/util/ScriptingGlobals';
 import { Deserializable, afterDocDeserialize, autoObject } from '../client/util/SerializationHelper';
 import { Field, FieldType, StrListCast } from './Doc';
@@ -279,7 +279,7 @@ class ListImpl<T extends FieldType> extends ObjectField {
         return this[FieldTuples].map(toRealField);
     }
 
-    @serializable(alias(ListFieldName, list(autoObject(), { afterDeserialize: afterDocDeserialize })))
+    @serializable(alias(ListFieldName, serializrList(autoObject(), { afterDeserialize: afterDocDeserialize })))
     private get __fieldTuples() {
         return this[FieldTuples];
     }
diff --git a/src/fields/Schema.ts b/src/fields/Schema.ts
index ed603e5de..7217bec37 100644
--- a/src/fields/Schema.ts
+++ b/src/fields/Schema.ts
@@ -1,3 +1,7 @@
+/* eslint-disable guard-for-in */
+/* eslint-disable no-restricted-syntax */
+/* eslint-disable no-redeclare */
+/* eslint-disable no-use-before-define */
 import { Interface, ToInterface, Cast, ToConstructor, HasTail, Head, Tail, ListSpec, ToType, DefaultFieldConstructor } from './Types';
 import { Doc, FieldType } from './Doc';
 import { ObjectField } from './ObjectField';
@@ -12,6 +16,7 @@ type AllToInterface<T extends Interface[]> = {
 
 export const emptySchema = createSchema({});
 export const Document = makeInterface(emptySchema);
+// eslint-disable-next-line no-redeclare
 export type Document = makeInterface<[typeof emptySchema]>;
 
 export interface InterfaceFunc<T extends Interface[]> {
@@ -39,6 +44,7 @@ export function makeInterface<T extends Interface[]>(...schemas: T): InterfaceFu
                         // defaultSpec
                         return Cast(field, desc.type, desc.defaultVal);
                     }
+                    // eslint-disable-next-line no-prototype-builtins
                     if (typeof desc === 'function' && !ObjectField.isPrototypeOf(desc) && !RefField.isPrototypeOf(desc)) {
                         const doc = Cast(field, Doc);
                         if (doc === undefined) {
@@ -73,8 +79,8 @@ export function makeStrictInterface<T extends Interface>(schema: T): (doc: Doc)
             get() {
                 return Cast(this.__doc[key], type as any);
             },
-            set(value) {
-                value = Cast(value, type as any);
+            set(setValue) {
+                const value = Cast(setValue, type as any);
                 if (value !== undefined) {
                     this.__doc[key] = value;
                     return;
@@ -93,14 +99,15 @@ export function makeStrictInterface<T extends Interface>(schema: T): (doc: Doc)
     };
 }
 
+// eslint-disable-next-line @typescript-eslint/no-unused-vars
 export function createSchema<T extends Interface>(schema: T): T & { proto: ToConstructor<Doc> } {
     return undefined as any;
-    (schema as any).proto = Doc;
-    return schema as any;
+    // (schema as any).proto = Doc;
+    // return schema as any;
 }
 
 export function listSpec<U extends ToConstructor<FieldType>>(type: U): ListSpec<ToType<U>> {
-    return { List: type as any }; //TODO Types
+    return { List: type as any }; // TODO Types
 }
 
 export function defaultSpec<T extends ToConstructor<FieldType>>(type: T, defaultVal: ToType<T>): DefaultFieldConstructor<ToType<T>> {
diff --git a/src/fields/URLField.ts b/src/fields/URLField.ts
index c6c51957d..3a83e7ca0 100644
--- a/src/fields/URLField.ts
+++ b/src/fields/URLField.ts
@@ -16,16 +16,18 @@ export abstract class URLField extends ObjectField {
     @serializable(url())
     readonly url: URL;
 
-    constructor(url: string);
-    constructor(url: URL);
-    constructor(url: URL | string) {
+    constructor(urlVal: string);
+    // eslint-disable-next-line @typescript-eslint/no-shadow
+    constructor(urlVal: URL);
+    // eslint-disable-next-line @typescript-eslint/no-shadow
+    constructor(urlVal: URL | string) {
         super();
         this.url =
-            typeof url !== 'string'
-                ? url // it's an URL
-                : url.startsWith('http')
-                  ? new URL(url)
-                  : new URL(url, window.location.origin);
+            typeof urlVal !== 'string'
+                ? urlVal // it's an URL
+                : urlVal.startsWith('http')
+                  ? new URL(urlVal)
+                  : new URL(urlVal, window.location.origin);
     }
 
     [ToScriptString]() {
diff --git a/src/mobile/MobileMain.tsx b/src/mobile/MobileMain.tsx
index dc3a73def..07839b6f6 100644
--- a/src/mobile/MobileMain.tsx
+++ b/src/mobile/MobileMain.tsx
@@ -3,7 +3,7 @@ import * as ReactDOM from 'react-dom';
 import { DocServer } from '../client/DocServer';
 import { Docs } from '../client/documents/Documents';
 import { CurrentUserUtils } from '../client/util/CurrentUserUtils';
-import { AssignAllExtensions } from '../extensions/General/Extensions';
+import { AssignAllExtensions } from '../extensions/Extensions';
 import { MobileInterface } from './MobileInterface';
 
 AssignAllExtensions();
diff --git a/src/pen-gestures/ndollar.ts b/src/pen-gestures/ndollar.ts
index d28d303b8..5f1ea00dc 100644
--- a/src/pen-gestures/ndollar.ts
+++ b/src/pen-gestures/ndollar.ts
@@ -1,3 +1,4 @@
+/* eslint-disable no-use-before-define */
 import { Gestures } from './GestureTypes';
 
 /**
@@ -69,20 +70,34 @@ import { Gestures } from './GestureTypes';
  * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
  * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
  * SUCH DAMAGE.
- **/
+ * */
 
 //
 // Point class
 //
 export class Point {
-    constructor(public X: number, public Y: number) {}
+    // eslint-disable-next-line no-useless-constructor
+    constructor(
+        public X: number,
+        public Y: number
+    ) {
+        /* empty */
+    }
 }
 
 //
 // Rectangle class
 //
 export class Rectangle {
-    constructor(public X: number, public Y: number, public Width: number, public Height: number) {}
+    // eslint-disable-next-line no-useless-constructor
+    constructor(
+        public X: number,
+        public Y: number,
+        public Width: number,
+        public Height: number
+    ) {
+        /* empty */
+    }
 }
 
 //
@@ -93,7 +108,11 @@ export class Unistroke {
     public StartUnitVector: Point;
     public Vector: number[];
 
-    constructor(public Name: string, useBoundedRotationInvariance: boolean, points: Point[]) {
+    constructor(
+        public Name: string,
+        useBoundedRotationInvariance: boolean,
+        points: Point[]
+    ) {
         this.Points = Resample(points, NumPoints);
         const radians = IndicativeAngle(this.Points);
         this.Points = RotateBy(this.Points, -radians);
@@ -121,15 +140,15 @@ export class Multistroke {
         this.NumStrokes = strokes.length; // number of individual strokes
 
         const order = new Array(strokes.length); // array of integer indices
-        for (var i = 0; i < strokes.length; i++) {
+        for (let i = 0; i < strokes.length; i++) {
             order[i] = i; // initialize
         }
-        const orders = new Array(); // array of integer arrays
-        HeapPermute(strokes.length, order, /*out*/ orders);
+        const orders = [] as any[]; // array of integer arrays
+        HeapPermute(strokes.length, order, /* out */ orders);
 
         const unistrokes = MakeUnistrokes(strokes, orders); // returns array of point arrays
         this.Unistrokes = new Array(unistrokes.length); // unistrokes for this multistroke
-        for (var j = 0; j < unistrokes.length; j++) {
+        for (let j = 0; j < unistrokes.length; j++) {
             this.Unistrokes[j] = new Unistroke(this.Name, useBoundedRotationInvariance, unistrokes[j]);
         }
     }
@@ -139,7 +158,14 @@ export class Multistroke {
 // Result class
 //
 export class Result {
-    constructor(public Name: string, public Score: any, public Time: any) {}
+    // eslint-disable-next-line no-useless-constructor
+    constructor(
+        public Name: string,
+        public Score: any,
+        public Time: any
+    ) {
+        /* empty */
+    }
 }
 
 //
@@ -174,44 +200,40 @@ export class NDollarRecognizer {
             new Multistroke(
                 Gestures.Rectangle,
                 useBoundedRotationInvariance,
-                new Array(
-                    new Array(
-                        new Point(30, 146), //new Point(29, 160), new Point(30, 180), new Point(31, 200),
-                        new Point(30, 222), //new Point(50, 219), new Point(70, 225), new Point(90, 230),
-                        new Point(106, 225), //new Point(100, 200), new Point(106, 180), new Point(110, 160),
-                        new Point(106, 146), //new Point(80, 150), new Point(50, 146),
-                        new Point(30, 143)
-                    )
-                )
+                new Array([
+                    new Point(30, 146), // new Point(29, 160), new Point(30, 180), new Point(31, 200),
+                    new Point(30, 222), // new Point(50, 219), new Point(70, 225), new Point(90, 230),
+                    new Point(106, 225), // new Point(100, 200), new Point(106, 180), new Point(110, 160),
+                    new Point(106, 146), // new Point(80, 150), new Point(50, 146),
+                    new Point(30, 143),
+                ])
             )
         );
-        this.Multistrokes.push(new Multistroke(Gestures.Line, useBoundedRotationInvariance, new Array(new Array(new Point(12, 347), new Point(119, 347)))));
+        this.Multistrokes.push(new Multistroke(Gestures.Line, useBoundedRotationInvariance, [[new Point(12, 347), new Point(119, 347)]]));
         this.Multistrokes.push(
             new Multistroke(
                 Gestures.Triangle, // equilateral
                 useBoundedRotationInvariance,
-                new Array(new Array(new Point(40, 100), new Point(100, 200), new Point(140, 102), new Point(42, 100)))
+                new Array([new Point(40, 100), new Point(100, 200), new Point(140, 102), new Point(42, 100)])
             )
         );
         this.Multistrokes.push(
             new Multistroke(
                 Gestures.Circle,
                 useBoundedRotationInvariance,
-                new Array(
-                    new Array(
-                        new Point(200, 250),
-                        new Point(240, 230),
-                        new Point(248, 210),
-                        new Point(248, 190),
-                        new Point(240, 170),
-                        new Point(200, 150),
-                        new Point(160, 170),
-                        new Point(151, 190),
-                        new Point(151, 210),
-                        new Point(160, 230),
-                        new Point(201, 250)
-                    )
-                )
+                new Array([
+                    new Point(200, 250),
+                    new Point(240, 230),
+                    new Point(248, 210),
+                    new Point(248, 190),
+                    new Point(240, 170),
+                    new Point(200, 150),
+                    new Point(160, 170),
+                    new Point(151, 190),
+                    new Point(151, 210),
+                    new Point(160, 230),
+                    new Point(201, 250),
+                ])
             )
         );
         NumMultistrokes = this.Multistrokes.length; // NumMultistrokes flags the end of the non user-defined gstures strokes
@@ -292,20 +314,21 @@ export class NDollarRecognizer {
         const points = CombineStrokes(strokes); // make one connected unistroke from the given strokes
         const candidate = new Unistroke('', useBoundedRotationInvariance, points);
 
-        var u = -1;
-        var b = +Infinity;
+        let u = -1;
+        let b = +Infinity;
         for (
-            var i = 0;
+            let i = 0;
             i < this.Multistrokes.length;
             i++ // for each multistroke template
         ) {
             if (!requireSameNoOfStrokes || strokes.length === this.Multistrokes[i].NumStrokes) {
                 // optional -- only attempt match when same # of component strokes
-                for (const unistroke of this.Multistrokes[i].Unistrokes) {
+                // eslint-disable-next-line no-loop-func
+                this.Multistrokes[i].Unistrokes.forEach(unistroke => {
                     // for each unistroke within this multistroke
                     if (AngleBetweenUnitVectors(candidate.StartUnitVector, unistroke.StartUnitVector) <= AngleSimilarityThreshold) {
                         // strokes start in the same direction
-                        var d;
+                        let d;
                         if (useProtractor) {
                             d = OptimalCosineDistance(unistroke.Vector, candidate.Vector); // Protractor
                         } else {
@@ -316,7 +339,7 @@ export class NDollarRecognizer {
                             u = i; // multistroke owner of unistroke
                         }
                     }
-                }
+                });
             }
         }
         const t1 = Date.now();
@@ -325,12 +348,12 @@ export class NDollarRecognizer {
 
     AddGesture = (name: string, useBoundedRotationInvariance: boolean, strokes: any[]) => {
         this.Multistrokes[this.Multistrokes.length] = new Multistroke(name, useBoundedRotationInvariance, strokes);
-        var num = 0;
-        for (const multistroke of this.Multistrokes) {
+        let num = 0;
+        this.Multistrokes.forEach(multistroke => {
             if (multistroke.Name === name) {
                 num++;
             }
-        }
+        });
         return num;
     };
 
@@ -343,11 +366,11 @@ export class NDollarRecognizer {
 //
 // Private helper functions from here on down
 //
-function HeapPermute(n: number, order: any[], /*out*/ orders: any[]) {
+function HeapPermute(n: number, order: any[], /* out */ orders: any[]) {
     if (n === 1) {
         orders[orders.length] = order.slice(); // append copy
     } else {
-        for (var i = 0; i < n; i++) {
+        for (let i = 0; i < n; i++) {
             HeapPermute(n - 1, order, orders);
             if (n % 2 === 1) {
                 // swap 0, n-1
@@ -364,47 +387,44 @@ function HeapPermute(n: number, order: any[], /*out*/ orders: any[]) {
     }
 }
 
-function MakeUnistrokes(strokes: any, orders: any) {
-    const unistrokes = new Array(); // array of point arrays
-    for (const order of orders) {
+function MakeUnistrokes(strokes: any, orders: any[]) {
+    const unistrokes = [] as any[]; // array of point arrays
+    orders.forEach(order => {
         for (
-            var b = 0;
-            b < Math.pow(2, order.length);
+            let b = 0;
+            b < order.length ** 2;
             b++ // use b's bits for directions
         ) {
-            const unistroke = new Array(); // array of points
-            for (var i = 0; i < order.length; i++) {
-                var pts;
+            const unistroke = [] as any[]; // array of points
+            for (let i = 0; i < order.length; i++) {
+                let pts;
+                // eslint-disable-next-line no-bitwise
                 if (((b >> i) & 1) === 1) {
                     // is b's bit at index i on?
                     pts = strokes[order[i]].slice().reverse(); // copy and reverse
                 } else {
                     pts = strokes[order[i]].slice(); // copy
                 }
-                for (const point of pts) {
+                pts.forEach((point: any) => {
                     unistroke[unistroke.length] = point; // append points
-                }
+                });
             }
             unistrokes[unistrokes.length] = unistroke; // add one unistroke to set
         }
-    }
+    });
     return unistrokes;
 }
 
-function CombineStrokes(strokes: any) {
-    const points = new Array();
-    for (const stroke of strokes) {
-        for (const { X, Y } of stroke) {
-            points[points.length] = new Point(X, Y);
-        }
-    }
+function CombineStrokes(strokes: any[]) {
+    const points: Point[] = [];
+    strokes.forEach(stroke => stroke.forEach((X: any, Y: any) => points.push(new Point(X, Y))));
     return points;
 }
 function Resample(points: any, n: any) {
     const I = PathLength(points) / (n - 1); // interval length
-    var D = 0.0;
+    let D = 0.0;
     const newpoints = new Array(points[0]);
-    for (var i = 1; i < points.length; i++) {
+    for (let i = 1; i < points.length; i++) {
         const d = Distance(points[i - 1], points[i]);
         if (D + d >= I) {
             const qx = points[i - 1].X + ((I - D) / d) * (points[i].X - points[i - 1].X);
@@ -425,55 +445,55 @@ function IndicativeAngle(points: any) {
     const c = Centroid(points);
     return Math.atan2(c.Y - points[0].Y, c.X - points[0].X);
 }
-function RotateBy(points: any, radians: any) {
+function RotateBy(points: Point[], radians: any) {
     // rotates points around centroid
     const c = Centroid(points);
     const cos = Math.cos(radians);
     const sin = Math.sin(radians);
-    const newpoints = new Array();
-    for (const point of points) {
+    const newpoints: Point[] = [];
+    points.forEach(point => {
         const qx = (point.X - c.X) * cos - (point.Y - c.Y) * sin + c.X;
         const qy = (point.X - c.X) * sin + (point.Y - c.Y) * cos + c.Y;
-        newpoints[newpoints.length] = new Point(qx, qy);
-    }
+        newpoints.push(new Point(qx, qy));
+    });
     return newpoints;
 }
 function ScaleDimTo(points: any, size: any, ratio1D: any) {
     // scales bbox uniformly for 1D, non-uniformly for 2D
     const B = BoundingBox(points);
     const uniformly = Math.min(B.Width / B.Height, B.Height / B.Width) <= ratio1D; // 1D or 2D gesture test
-    const newpoints = new Array();
-    for (const { X, Y } of points) {
+    const newpoints: Point[] = [];
+    points.forEach((X: any, Y: any) => {
         const qx = uniformly ? X * (size / Math.max(B.Width, B.Height)) : X * (size / B.Width);
         const qy = uniformly ? Y * (size / Math.max(B.Width, B.Height)) : Y * (size / B.Height);
         newpoints[newpoints.length] = new Point(qx, qy);
-    }
+    });
     return newpoints;
 }
 function TranslateTo(points: any, pt: any) {
     // translates points' centroid
     const c = Centroid(points);
-    const newpoints = new Array();
-    for (const { X, Y } of points) {
+    const newpoints: Point[] = [];
+    points.forEach((X: any, Y: any) => {
         const qx = X + pt.X - c.X;
         const qy = Y + pt.Y - c.Y;
         newpoints[newpoints.length] = new Point(qx, qy);
-    }
+    });
     return newpoints;
 }
 function Vectorize(points: any, useBoundedRotationInvariance: any) {
     // for Protractor
-    var cos = 1.0;
-    var sin = 0.0;
+    let cos = 1.0;
+    let sin = 0.0;
     if (useBoundedRotationInvariance) {
         const iAngle = Math.atan2(points[0].Y, points[0].X);
         const baseOrientation = (Math.PI / 4.0) * Math.floor((iAngle + Math.PI / 8.0) / (Math.PI / 4.0));
         cos = Math.cos(baseOrientation - iAngle);
         sin = Math.sin(baseOrientation - iAngle);
     }
-    var sum = 0.0;
-    const vector = new Array<number>();
-    for (var i = 0; i < points.length; i++) {
+    let sum = 0.0;
+    const vector: number[] = [];
+    for (let i = 0; i < points.length; i++) {
         const newX = points[i].X * cos - points[i].Y * sin;
         const newY = points[i].Y * cos + points[i].X * sin;
         vector[vector.length] = newX;
@@ -481,16 +501,16 @@ function Vectorize(points: any, useBoundedRotationInvariance: any) {
         sum += newX * newX + newY * newY;
     }
     const magnitude = Math.sqrt(sum);
-    for (var i = 0; i < vector.length; i++) {
+    for (let i = 0; i < vector.length; i++) {
         vector[i] /= magnitude;
     }
     return vector;
 }
 function OptimalCosineDistance(v1: any, v2: any) {
     // for Protractor
-    var a = 0.0;
-    var b = 0.0;
-    for (var i = 0; i < v1.length; i += 2) {
+    let a = 0.0;
+    let b = 0.0;
+    for (let i = 0; i < v1.length; i += 2) {
         a += v1[i] * v2[i] + v1[i + 1] * v2[i + 1];
         b += v1[i] * v2[i + 1] - v1[i + 1] * v2[i];
     }
@@ -498,18 +518,20 @@ function OptimalCosineDistance(v1: any, v2: any) {
     return Math.acos(a * Math.cos(angle) + b * Math.sin(angle));
 }
 function DistanceAtBestAngle(points: any, T: any, a: any, b: any, threshold: any) {
-    var x1 = Phi * a + (1.0 - Phi) * b;
-    var f1 = DistanceAtAngle(points, T, x1);
-    var x2 = (1.0 - Phi) * a + Phi * b;
-    var f2 = DistanceAtAngle(points, T, x2);
+    let x1 = Phi * a + (1.0 - Phi) * b;
+    let f1 = DistanceAtAngle(points, T, x1);
+    let x2 = (1.0 - Phi) * a + Phi * b;
+    let f2 = DistanceAtAngle(points, T, x2);
     while (Math.abs(b - a) > threshold) {
         if (f1 < f2) {
+            // eslint-disable-next-line no-param-reassign
             b = x2;
             x2 = x1;
             f2 = f1;
             x1 = Phi * a + (1.0 - Phi) * b;
             f1 = DistanceAtAngle(points, T, x1);
         } else {
+            // eslint-disable-next-line no-param-reassign
             a = x1;
             x1 = x2;
             f1 = f2;
@@ -523,34 +545,34 @@ function DistanceAtAngle(points: any, T: any, radians: any) {
     const newpoints = RotateBy(points, radians);
     return PathDistance(newpoints, T.Points);
 }
-function Centroid(points: any) {
-    var x = 0.0,
-        y = 0.0;
-    for (const point of points) {
+function Centroid(points: Point[]) {
+    let x = 0.0;
+    let y = 0.0;
+    points.forEach(point => {
         x += point.X;
         y += point.Y;
-    }
+    });
     x /= points.length;
     y /= points.length;
     return new Point(x, y);
 }
-function BoundingBox(points: any) {
-    var minX = +Infinity,
-        maxX = -Infinity,
-        minY = +Infinity,
-        maxY = -Infinity;
-    for (const { X, Y } of points) {
+function BoundingBox(points: Point[]) {
+    let minX = +Infinity;
+    let maxX = -Infinity;
+    let minY = +Infinity;
+    let maxY = -Infinity;
+    points.forEach((X: any, Y: any) => {
         minX = Math.min(minX, X);
         minY = Math.min(minY, Y);
         maxX = Math.max(maxX, X);
         maxY = Math.max(maxY, Y);
-    }
+    });
     return new Rectangle(minX, minY, maxX - minX, maxY - minY);
 }
 function PathDistance(pts1: any, pts2: any) {
     // average distance between corresponding points in two paths
-    var d = 0.0;
-    for (var i = 0; i < pts1.length; i++) {
+    let d = 0.0;
+    for (let i = 0; i < pts1.length; i++) {
         // assumes pts1.length == pts2.length
         d += Distance(pts1[i], pts2[i]);
     }
@@ -558,8 +580,8 @@ function PathDistance(pts1: any, pts2: any) {
 }
 function PathLength(points: any) {
     // length traversed by a point path
-    var d = 0.0;
-    for (var i = 1; i < points.length; i++) {
+    let d = 0.0;
+    for (let i = 1; i < points.length; i++) {
         d += Distance(points[i - 1], points[i]);
     }
     return d;
@@ -570,9 +592,9 @@ function Distance(p1: any, p2: any) {
     const dy = p2.Y - p1.Y;
     return Math.sqrt(dx * dx + dy * dy);
 }
-function CalcStartUnitVector(points: any, index: any) {
+function CalcStartUnitVector(points: Point[], index: any) {
     // start angle from points[0] to points[index] normalized as a unit vector
-    const v = new Point(points[index]?.X - points[0]?.X, points[index]?.Y - points[0]?.Y);
+    const v = new Point(points[index].X - points[0].X, points[index].Y - points[0].Y);
     const len = Math.sqrt(v.X * v.X + v.Y * v.Y);
     return new Point(v.X / len, v.Y / len);
 }
-- 
cgit v1.2.3-70-g09d2


From 08334824ad07dea1d0bb56d8ef91e3ad1a4573b0 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Thu, 25 Apr 2024 18:03:15 -0400
Subject: added toList() for Doc|Doc[]

---
 src/client/views/DocComponent.tsx                     | 16 ++++++++--------
 src/client/views/LightboxView.tsx                     |  4 ++--
 src/client/views/MainView.tsx                         | 12 ++++++------
 src/client/views/OverlayView.tsx                      | 10 +++-------
 src/client/views/collections/CollectionPileView.tsx   | 14 +++++++-------
 src/client/views/collections/CollectionTreeView.tsx   |  6 +++---
 src/client/views/collections/TabDocView.tsx           | 12 ++++++------
 src/client/views/collections/TreeView.tsx             | 19 ++++++++++---------
 .../collectionFreeForm/CollectionFreeFormView.tsx     |  8 ++++----
 .../collections/collectionSchema/SchemaTableCell.tsx  |  6 +++---
 src/client/views/nodes/ComparisonBox.tsx              | 12 ++++++------
 src/client/views/nodes/MapBox/MapBox.tsx              | 18 ++++++++++--------
 .../views/nodes/MapboxMapBox/MapboxContainer.tsx      | 10 +++++-----
 src/client/views/nodes/PDFBox.tsx                     |  4 ++--
 src/client/views/nodes/VideoBox.tsx                   |  4 ++--
 src/client/views/nodes/WebBox.tsx                     |  4 ++--
 src/client/views/nodes/trails/PresBox.tsx             |  7 +++++--
 src/fields/Doc.ts                                     |  8 ++++----
 src/fields/Types.ts                                   |  4 ++++
 19 files changed, 92 insertions(+), 86 deletions(-)

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

diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx
index fd29b1ca4..bbd902a96 100644
--- a/src/client/views/DocComponent.tsx
+++ b/src/client/views/DocComponent.tsx
@@ -6,6 +6,7 @@ import { Doc, DocListCast, FieldType, Opt } from '../../fields/Doc';
 import { AclAdmin, AclAugment, AclEdit, AclPrivate, AclReadonly, DocData, DocViews } from '../../fields/DocSymbols';
 import { List } from '../../fields/List';
 import { RefField } from '../../fields/RefField';
+import { toList } from '../../fields/Types';
 import { GetEffectiveAcl, inheritParentAcls } from '../../fields/util';
 import { DocumentType } from '../documents/DocumentTypes';
 import { DragManager } from '../util/DragManager';
@@ -246,10 +247,9 @@ export function ViewBoxAnnotatableComponent<P extends FieldViewProps>() {
         }
 
         @action.bound
-        removeDocument(doc: Doc | Doc[], annotationKey?: string, leavePushpin?: boolean, dontAddToRemoved?: boolean): boolean {
+        removeDocument(docIn: Doc | Doc[], annotationKey?: string, leavePushpin?: boolean, dontAddToRemoved?: boolean): boolean {
             const effectiveAcl = GetEffectiveAcl(this.dataDoc);
-            const indocs = doc instanceof Doc ? [doc] : doc;
-            const docs = indocs.filter(fdoc => [AclEdit, AclAdmin].includes(effectiveAcl) || GetEffectiveAcl(fdoc) === AclAdmin);
+            const docs = toList(docIn).filter(fdoc => [AclEdit, AclAdmin].includes(effectiveAcl) || GetEffectiveAcl(fdoc) === AclAdmin);
 
             // docs.forEach(doc => doc.annotationOn === this.Document && Doc.SetInPlace(doc, 'annotationOn', undefined, true));
             const targetDataDoc = this.Document[DocData]; // this.dataDoc;  // we want to write to the template, not the actual data doc
@@ -282,19 +282,19 @@ export function ViewBoxAnnotatableComponent<P extends FieldViewProps>() {
         // otherwise, the document being moved must be able to be removed from its container before
         // moving it into the target.
         @action.bound
-        moveDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (doc: Doc | Doc[], annotationKey?: string) => boolean, annotationKey?: string): boolean => {
+        moveDocument = (docs: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (doc: Doc | Doc[], annotationKey?: string) => boolean, annotationKey?: string): boolean => {
             if (Doc.AreProtosEqual(this._props.Document, targetCollection)) {
                 return true;
             }
-            const first = doc instanceof Doc ? doc : doc[0];
+            const first = toList(docs)[0];
             if (!first?._dragOnlyWithinContainer && addDocument !== returnFalse) {
-                return this.removeDocument(doc, annotationKey, false, true) && addDocument(doc, annotationKey);
+                return this.removeDocument(docs, annotationKey, false, true) && addDocument(docs, annotationKey);
             }
             return false;
         };
         @action.bound
-        addDocument = (doc: Doc | Doc[], annotationKey?: string): boolean => {
-            const docs = doc instanceof Doc ? [doc] : doc;
+        addDocument = (docIn: Doc | Doc[], annotationKey?: string): boolean => {
+            const docs = toList(docIn);
             if (this._props.filterAddDocument?.(docs) === false || docs.find(fdoc => Doc.AreProtosEqual(fdoc, this.Document) && Doc.LayoutField(fdoc) === Doc.LayoutField(this.Document))) {
                 return false;
             }
diff --git a/src/client/views/LightboxView.tsx b/src/client/views/LightboxView.tsx
index 24433cd01..0dd836f70 100644
--- a/src/client/views/LightboxView.tsx
+++ b/src/client/views/LightboxView.tsx
@@ -12,7 +12,7 @@ import { emptyFunction } from '../../Utils';
 import { Doc, DocListCast, FieldResult, Opt } from '../../fields/Doc';
 import { Id } from '../../fields/FieldSymbols';
 import { InkTool } from '../../fields/InkField';
-import { Cast, NumCast } from '../../fields/Types';
+import { Cast, NumCast, toList } from '../../fields/Types';
 import { DocUtils } from '../documents/Documents';
 import { DocumentManager } from '../util/DocumentManager';
 import { LinkManager } from '../util/LinkManager';
@@ -119,7 +119,7 @@ export class LightboxView extends ObservableReactComponent<LightboxViewProps> {
     }
 
     public AddDocTab = (docs: Doc | Doc[], location: OpenWhere, layoutTemplate?: Doc | string) => {
-        const doc = (docs instanceof Doc ? [docs] : docs).lastElement();
+        const doc = toList(docs).lastElement();
         return this.SetLightboxDoc(
             doc,
             undefined,
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 10d423c05..be13b14f5 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -13,7 +13,7 @@ import { ClientUtils, lightOrDark, returnEmptyDoclist, returnEmptyFilter, return
 import { emptyFunction } from '../../Utils';
 import { Doc, DocListCast, Opt } from '../../fields/Doc';
 import { DocData } from '../../fields/DocSymbols';
-import { DocCast, StrCast } from '../../fields/Types';
+import { DocCast, StrCast, toList } from '../../fields/Types';
 import { DocServer } from '../DocServer';
 import { GoogleAuthenticationManager } from '../apis/GoogleAuthenticationManager';
 import { CollectionViewType, DocumentType } from '../documents/DocumentTypes';
@@ -627,8 +627,8 @@ export class MainView extends ObservableReactComponent<{}> {
     waitForDoubleClick = () => (SnappingManager.ExploreMode ? 'never' : undefined);
     headerBarScreenXf = () => new Transform(-this.leftScreenOffsetOfMainDocView - this.leftMenuFlyoutWidth(), -this.headerBarDocHeight(), 1);
     mainScreenToLocalXf = () => new Transform(-this.leftScreenOffsetOfMainDocView - this.leftMenuFlyoutWidth(), -this.topOfMainDocContent, 1);
-    addHeaderDoc = (docs: Doc | Doc[]) => (docs instanceof Doc ? [docs] : docs).reduce((done, doc) => Doc.AddDocToList(this.headerBarDoc, 'data', doc), true);
-    removeHeaderDoc = (docs: Doc | Doc[]) => (docs instanceof Doc ? [docs] : docs).reduce((done, doc) => Doc.RemoveDocFromList(this.headerBarDoc, 'data', doc), true);
+    addHeaderDoc = (docs: Doc | Doc[]) => toList(docs).reduce((done, doc) => Doc.AddDocToList(this.headerBarDoc, 'data', doc), true);
+    removeHeaderDoc = (docs: Doc | Doc[]) => toList(docs).reduce((done, doc) => Doc.RemoveDocFromList(this.headerBarDoc, 'data', doc), true);
     @computed get headerBarDocView() {
         return (
             <div className="mainView-headerBar" style={{ height: this.headerBarDocHeight() }}>
@@ -748,7 +748,7 @@ export class MainView extends ObservableReactComponent<{}> {
     sidebarScreenToLocal = () => new Transform(0, -this.topOfSidebarDoc, 1);
     mainContainerXf = () => this.sidebarScreenToLocal().translate(-this.leftScreenOffsetOfMainDocView, 0);
     static addDocTabFunc_impl = (docs: Doc | Doc[], location: OpenWhere): boolean => {
-        const doc = (docs instanceof Doc ? [docs] : docs).lastElement();
+        const doc = toList(docs).lastElement();
         const whereFields = location.split(':');
         const keyValue = whereFields.includes(OpenWhereMod.keyvalue);
         const whereMods = whereFields.length > 1 ? (whereFields[1] as OpenWhereMod) : OpenWhereMod.none;
@@ -920,9 +920,9 @@ export class MainView extends ObservableReactComponent<{}> {
         this._leftMenuFlyoutWidth = 0;
     });
 
-    remButtonDoc = (docs: Doc | Doc[]) => (docs instanceof Doc ? [docs] : docs).reduce((flg: boolean, doc) => flg && !doc.dragOnlyWithinContainer && Doc.RemoveDocFromList(Doc.MyDockedBtns, 'data', doc), true);
+    remButtonDoc = (docs: Doc | Doc[]) => toList(docs).reduce((flg: boolean, doc) => flg && !doc.dragOnlyWithinContainer && Doc.RemoveDocFromList(Doc.MyDockedBtns, 'data', doc), true);
     moveButtonDoc = (docs: Doc | Doc[], targetCol: Doc | undefined, addDocument: (document: Doc | Doc[]) => boolean) => this.remButtonDoc(docs) && addDocument(docs);
-    addButtonDoc = (docs: Doc | Doc[]) => (docs instanceof Doc ? [docs] : docs).reduce((flg: boolean, doc) => flg && Doc.AddDocToList(Doc.MyDockedBtns, 'data', doc), true);
+    addButtonDoc = (docs: Doc | Doc[]) => toList(docs).reduce((flg: boolean, doc) => flg && Doc.AddDocToList(Doc.MyDockedBtns, 'data', doc), true);
 
     buttonBarXf = () => {
         if (!this._docBtnRef.current) return Transform.Identity();
diff --git a/src/client/views/OverlayView.tsx b/src/client/views/OverlayView.tsx
index e1c248a7a..e17da2680 100644
--- a/src/client/views/OverlayView.tsx
+++ b/src/client/views/OverlayView.tsx
@@ -8,7 +8,7 @@ import { Utils, emptyFunction } from '../../Utils';
 import { Doc } from '../../fields/Doc';
 import { Height, Width } from '../../fields/DocSymbols';
 import { Id } from '../../fields/FieldSymbols';
-import { NumCast } from '../../fields/Types';
+import { NumCast, toList } from '../../fields/Types';
 import { DocumentType } from '../documents/DocumentTypes';
 import { DragManager } from '../util/DragManager';
 import { dropActionType } from '../util/DropActionTypes';
@@ -176,17 +176,13 @@ export class OverlayView extends ObservableReactComponent<{}> {
         return remove;
     }
 
-    removeOverlayDoc = (doc: Doc | Doc[]) => {
-        (doc instanceof Doc ? [doc] : doc).forEach(Doc.RemFromMyOverlay);
-        return true;
-    };
+    removeOverlayDoc = (docs: Doc | Doc[]) => toList(docs).every(Doc.RemFromMyOverlay);
 
     docScreenToLocalXf = computedFn((doc: Doc) => () => new Transform(-NumCast(doc.overlayX), -NumCast(doc.overlayY), 1));
 
     @computed get overlayDocs() {
         return Doc.MyOverlayDocs.filter(d => !LightboxView.LightboxDoc || d.type === DocumentType.PRES).map(d => {
-            let offsetx = 0;
-            let offsety = 0;
+            let [offsetx, offsety] = [0, 0];
             const dref = React.createRef<HTMLDivElement>();
             const onPointerMove = action((e: PointerEvent, down: number[]) => {
                 if (e.cancelBubble) return false; // if the overlay doc processed the move event (e.g., to pan its contents), then the event should be marked as canceled since propagation can't be stopped
diff --git a/src/client/views/collections/CollectionPileView.tsx b/src/client/views/collections/CollectionPileView.tsx
index 6dfc38e2e..6ccbd6208 100644
--- a/src/client/views/collections/CollectionPileView.tsx
+++ b/src/client/views/collections/CollectionPileView.tsx
@@ -6,7 +6,7 @@ import * as React from 'react';
 import { returnFalse, setupMoveUpEvents } from '../../../ClientUtils';
 import { Doc, DocListCast } from '../../../fields/Doc';
 import { ScriptField } from '../../../fields/ScriptField';
-import { NumCast, StrCast } from '../../../fields/Types';
+import { NumCast, StrCast, toList } from '../../../fields/Types';
 import { emptyFunction } from '../../../Utils';
 import { DocUtils } from '../../documents/Documents';
 import { dropActionType } from '../../util/DropActionTypes';
@@ -43,15 +43,15 @@ export class CollectionPileView extends CollectionSubView() {
     layoutEngine = () => StrCast(this.Document._freeform_pileEngine);
 
     @undoBatch
-    addPileDoc = (doc: Doc | Doc[]) => {
-        (doc instanceof Doc ? [doc] : doc).map(d => DocUtils.iconify(d));
-        return this._props.addDocument?.(doc) || false;
+    addPileDoc = (docs: Doc | Doc[]) => {
+        toList(docs).map(doc => DocUtils.iconify(doc));
+        return this._props.addDocument?.(docs) || false;
     };
 
     @undoBatch
-    removePileDoc = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDoc: (doc: Doc | Doc[]) => boolean) => {
-        (doc instanceof Doc ? [doc] : doc).forEach(d => Doc.deiconifyView(d));
-        const ret = this._props.moveDocument?.(doc, targetCollection, addDoc) || false;
+    removePileDoc = (docs: Doc | Doc[], targetCollection: Doc | undefined, addDoc: (doc: Doc | Doc[]) => boolean) => {
+        toList(docs).forEach(doc => Doc.deiconifyView(doc));
+        const ret = this._props.moveDocument?.(docs, targetCollection, addDoc) || false;
         if (ret && !DocListCast(this.dataDoc[this.fieldKey ?? 'data']).length) this.DocumentView?.()._props.removeDocument?.(this.Document);
         return ret;
     };
diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx
index bf81bdc7f..365b5acfd 100644
--- a/src/client/views/collections/CollectionTreeView.tsx
+++ b/src/client/views/collections/CollectionTreeView.tsx
@@ -9,7 +9,7 @@ import { DocData } from '../../../fields/DocSymbols';
 import { Id } from '../../../fields/FieldSymbols';
 import { listSpec } from '../../../fields/Schema';
 import { ScriptField } from '../../../fields/ScriptField';
-import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
+import { BoolCast, Cast, NumCast, ScriptCast, StrCast, toList } from '../../../fields/Types';
 import { TraceMobx } from '../../../fields/util';
 import { emptyFunction, Utils } from '../../../Utils';
 import { Docs, DocUtils } from '../../documents/Documents';
@@ -178,7 +178,7 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree
 
     @action
     remove = (docIn: Doc | Doc[]): boolean => {
-        const docs = docIn instanceof Doc ? [docIn] : docIn;
+        const docs = toList(docIn);
         const targetDataDoc = this.Document[DocData];
         const value = DocListCast(targetDataDoc[this._props.fieldKey]);
         const result = value.filter(v => !docs.includes(v));
@@ -203,7 +203,7 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree
     addDoc = (docs: Doc | Doc[], relativeTo: Opt<Doc>, before?: boolean): boolean => {
         const addDocRelativeTo = (adocs: Doc | Doc[]) => (adocs as Doc[]).reduce((flg, doc) => flg && Doc.AddDocToList(this.Document[DocData], this._props.fieldKey, doc, relativeTo, before), true);
         if (this.Document.resolvedDataDoc instanceof Promise) return false;
-        const doclist = docs instanceof Doc ? [docs] : docs;
+        const doclist = toList(docs);
         const res = relativeTo === undefined ? this._props.addDocument?.(doclist) || false : addDocRelativeTo(doclist);
         res &&
             doclist.forEach(doc => {
diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx
index a661cf6a2..5c16fecd0 100644
--- a/src/client/views/collections/TabDocView.tsx
+++ b/src/client/views/collections/TabDocView.tsx
@@ -14,7 +14,7 @@ import { Id } from '../../../fields/FieldSymbols';
 import { List } from '../../../fields/List';
 import { FieldId } from '../../../fields/RefField';
 import { ComputedField } from '../../../fields/ScriptField';
-import { Cast, DocCast, NumCast, StrCast } from '../../../fields/Types';
+import { Cast, DocCast, NumCast, StrCast, toList } from '../../../fields/Types';
 import { DocServer } from '../../DocServer';
 import { CollectionViewType, DocumentType } from '../../documents/DocumentTypes';
 import { Docs } from '../../documents/Documents';
@@ -391,8 +391,8 @@ export class TabDocView extends ObservableReactComponent<TabDocViewProps> {
      * Adds a document to the presentation view
      * */
     @action
-    public static PinDoc(docs: Doc | Doc[], pinProps: PinProps) {
-        const docList = docs instanceof Doc ? [docs] : docs;
+    public static PinDoc(docIn: Doc | Doc[], pinProps: PinProps) {
+        const docs = toList(docIn);
 
         const batch = UndoManager.StartBatch('Pin doc to pres trail');
         const curPres = Doc.ActivePresentation ?? Doc.MakeCopy(Doc.UserDoc().emptyTrail as Doc, true);
@@ -402,7 +402,7 @@ export class TabDocView extends ObservableReactComponent<TabDocViewProps> {
             Doc.ActivePresentation = curPres;
         }
 
-        docList.forEach(doc => {
+        docs.forEach(doc => {
             // Edge Case 1: Cannot pin document to itself
             if (doc === curPres) {
                 alert('Cannot pin presentation document to itself');
@@ -459,7 +459,7 @@ export class TabDocView extends ObservableReactComponent<TabDocViewProps> {
         ) {
             if (Doc.IsInMyOverlay(curPres)) Doc.RemFromMyOverlay(curPres);
             CollectionDockingView.AddSplit(curPres, OpenWhereMod.right);
-            setTimeout(() => DocumentManager.Instance.showDocument(docList.lastElement(), { willPan: true }), 100); // keeps the pinned doc in view since the sidebar shifts things
+            setTimeout(() => DocumentManager.Instance.showDocument(docs.lastElement(), { willPan: true }), 100); // keeps the pinned doc in view since the sidebar shifts things
         }
         setTimeout(batch.end, 500); // need to wait until dockingview (goldenlayout) updates all its structurs
     }
@@ -515,7 +515,7 @@ export class TabDocView extends ObservableReactComponent<TabDocViewProps> {
     //                                   "replace:monkeys" - will replace any tab that has the label 'monkeys', or a tab with that label will be created by default on the right
     //  lightbox - will add the document to any collection along the path from the document to the docking view that has a field isLightbox. if none is found, it adds to the full screen lightbox
     addDocTab = (docsIn: Doc | Doc[], location: OpenWhere) => {
-        const docs = docsIn instanceof Doc ? [docsIn] : docsIn;
+        const docs = toList(docsIn);
         SelectionManager.DeselectAll();
         const whereFields = location.split(':');
         const keyValue = whereFields.includes(OpenWhereMod.keyvalue);
diff --git a/src/client/views/collections/TreeView.tsx b/src/client/views/collections/TreeView.tsx
index fab8e3892..a8dd60ae3 100644
--- a/src/client/views/collections/TreeView.tsx
+++ b/src/client/views/collections/TreeView.tsx
@@ -9,14 +9,14 @@ import { observer } from 'mobx-react';
 import * as React from 'react';
 import { ClientUtils, lightOrDark, return18, returnEmptyDoclist, returnEmptyFilter, returnEmptyString, returnFalse, returnTrue, returnZero, setupMoveUpEvents, simulateMouseClick } from '../../../ClientUtils';
 import { emptyFunction } from '../../../Utils';
-import { Doc, DocListCast, Field, FieldType, FieldResult, Opt, StrListCast } from '../../../fields/Doc';
+import { Doc, DocListCast, Field, FieldResult, FieldType, Opt, StrListCast } from '../../../fields/Doc';
 import { DocData } from '../../../fields/DocSymbols';
 import { Id } from '../../../fields/FieldSymbols';
 import { List } from '../../../fields/List';
 import { RichTextField } from '../../../fields/RichTextField';
 import { listSpec } from '../../../fields/Schema';
 import { ComputedField, ScriptField } from '../../../fields/ScriptField';
-import { BoolCast, Cast, DocCast, FieldValue, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
+import { BoolCast, Cast, DocCast, FieldValue, NumCast, ScriptCast, StrCast, toList } from '../../../fields/Types';
 import { TraceMobx } from '../../../fields/util';
 import { CollectionViewType, DocumentType } from '../../documents/DocumentTypes';
 import { DocUtils, Docs } from '../../documents/Documents';
@@ -200,11 +200,12 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> {
         }
         return false;
     };
-    @undoBatch remove = (docs: Doc | Doc[], key: string) => {
+    @undoBatch remove = (docIn: Doc | Doc[], key: string) => {
+        const docs = toList(docIn);
         this.treeView._props.select(false);
-        const ind = DocListCast(this.dataDoc[key]).indexOf(docs instanceof Doc ? docs : docs.lastElement());
+        const ind = DocListCast(this.dataDoc[key]).indexOf(docs.lastElement());
 
-        const res = (docs instanceof Doc ? [docs] : docs).reduce((flg, doc) => flg && Doc.RemoveDocFromList(this.dataDoc, key, doc), true);
+        const res = docs.reduce((flg, doc) => flg && Doc.RemoveDocFromList(this.dataDoc, key, doc), true);
         res && ind > 0 && DocumentManager.Instance.getDocumentView(DocListCast(this.dataDoc[key])[ind - 1], this.treeView.DocumentView?.())?.select(false);
         return res;
     };
@@ -438,7 +439,7 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> {
             dataIsComputed && Doc.SetContainer(doc, DocCast(this.Document.embedContainer));
             return added;
         };
-        return (docs instanceof Doc ? [docs] : docs).reduce((flg, doc) => flg && innerAdd(doc), true as boolean);
+        return toList(docs).reduce((flg, doc) => flg && innerAdd(doc), true as boolean);
     };
 
     dropping: boolean = false;
@@ -522,10 +523,10 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> {
                         dataIsComputed && Doc.SetContainer(iDoc, DocCast(this.Document.embedContainer));
                         return added;
                     };
-                    return (docs instanceof Doc ? [docs] : docs).reduce((flg, iDoc) => flg && innerAdd(iDoc), true as boolean);
+                    return toList(docs).reduce((flg, iDoc) => flg && innerAdd(iDoc), true as boolean);
                 };
                 contentElement = TreeView.GetChildElements(
-                    contents instanceof Doc ? [contents] : DocListCast(contents),
+                    toList(contents as any),
                     this.treeView,
                     this,
                     doc,
@@ -643,7 +644,7 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> {
 
                 return added;
             };
-            const addDoc = (docs: Doc | Doc[], addBefore?: Doc, before?: boolean) => (docs instanceof Doc ? [docs] : docs).reduce((flg, doc) => flg && localAdd(doc, addBefore, before), true);
+            const addDoc = (docs: Doc | Doc[], addBefore?: Doc, before?: boolean) => toList(docs).reduce((flg, doc) => flg && localAdd(doc, addBefore, before), true);
             const docs = expandKey === 'embeddings' ? this.childEmbeddings : expandKey === 'links' ? this.childLinks : expandKey === 'annotations' ? this.childAnnos : this.childDocs;
             let downX = 0;
             let downY = 0;
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index a14dcb592..fa4dbf9a2 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -17,7 +17,7 @@ import { List } from '../../../../fields/List';
 import { RichTextField } from '../../../../fields/RichTextField';
 import { listSpec } from '../../../../fields/Schema';
 import { ScriptField } from '../../../../fields/ScriptField';
-import { BoolCast, Cast, DocCast, NumCast, ScriptCast, StrCast } from '../../../../fields/Types';
+import { BoolCast, Cast, DocCast, NumCast, ScriptCast, StrCast, toList } from '../../../../fields/Types';
 import { ImageField } from '../../../../fields/URLField';
 import { TraceMobx } from '../../../../fields/util';
 import { Gestures, PointData } from '../../../../pen-gestures/GestureTypes';
@@ -284,7 +284,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
             // bcz: deal with clusters
         }
         if (retVal) {
-            const newBoxes = newBox instanceof Doc ? [newBox] : newBox;
+            const newBoxes = toList(newBox);
             newBoxes.forEach(box => {
                 if (box.activeFrame !== undefined) {
                     const vals = CollectionFreeFormDocumentView.animFields.map(field => box[field.key]);
@@ -1281,7 +1281,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         );
     }
     addDocTab = action((docsIn: Doc | Doc[], where: OpenWhere) => {
-        const docs = docsIn instanceof Doc ? [docsIn] : docsIn;
+        const docs = toList(docsIn);
         if (this._props.isAnnotationOverlay) return this._props.addDocTab(docs, where);
         switch (where) {
             case OpenWhere.inParent:
@@ -1290,7 +1290,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                 const docContext = DocCast(docs[0]?.embedContainer);
                 return (
                     (this.addDocument?.(
-                        (docs instanceof Doc ? [docs] : docs).map(doc => {
+                        toList(docs).map(doc => {
                             [doc.x, doc.y] = this.screenToFreeformContentsXf.transformPoint(NumCast(doc.x), NumCast(doc.y));
                             return doc;
                         })
diff --git a/src/client/views/collections/collectionSchema/SchemaTableCell.tsx b/src/client/views/collections/collectionSchema/SchemaTableCell.tsx
index ee30006ae..b89ca3614 100644
--- a/src/client/views/collections/collectionSchema/SchemaTableCell.tsx
+++ b/src/client/views/collections/collectionSchema/SchemaTableCell.tsx
@@ -16,7 +16,7 @@ import { DateField } from '../../../../fields/DateField';
 import { Doc, DocListCast, Field } from '../../../../fields/Doc';
 import { RichTextField } from '../../../../fields/RichTextField';
 import { ColumnType } from '../../../../fields/SchemaHeaderField';
-import { BoolCast, Cast, DateCast, DocCast, FieldValue, StrCast } from '../../../../fields/Types';
+import { BoolCast, Cast, DateCast, DocCast, FieldValue, StrCast, toList } from '../../../../fields/Types';
 import { ImageField } from '../../../../fields/URLField';
 import { FInfo, FInfoFieldType } from '../../../documents/Documents';
 import { DocFocusOrOpen } from '../../../util/DocumentManager';
@@ -67,8 +67,8 @@ export class SchemaTableCell extends ObservableReactComponent<SchemaTableCellPro
         makeObservable(this);
     }
 
-    static addFieldDoc = (doc: Doc | Doc[] /* , where: OpenWhere */) => {
-        DocFocusOrOpen(doc instanceof Doc ? doc : doc[0]);
+    static addFieldDoc = (docs: Doc | Doc[] /* , where: OpenWhere */) => {
+        DocFocusOrOpen(toList(docs)[0]);
         return true;
     };
     public static renderProps(props: SchemaTableCellProps) {
diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx
index c1aa1c699..e6fd447ac 100644
--- a/src/client/views/nodes/ComparisonBox.tsx
+++ b/src/client/views/nodes/ComparisonBox.tsx
@@ -6,7 +6,7 @@ import { returnFalse, returnNone, returnZero, setupMoveUpEvents } from '../../..
 import { emptyFunction } from '../../../Utils';
 import { Doc, Opt } from '../../../fields/Doc';
 import { RichTextField } from '../../../fields/RichTextField';
-import { DocCast, NumCast, RTFCast, StrCast } from '../../../fields/Types';
+import { DocCast, NumCast, RTFCast, StrCast, toList } from '../../../fields/Types';
 import { DocUtils, Docs } from '../../documents/Documents';
 import { DragManager } from '../../util/DragManager';
 import { dropActionType } from '../../util/DropActionTypes';
@@ -53,7 +53,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
     private internalDrop = (e: Event, dropEvent: DragManager.DropEvent, fieldKey: string) => {
         if (dropEvent.complete.docDragData) {
             const { droppedDocuments } = dropEvent.complete.docDragData;
-            const added = dropEvent.complete.docDragData.moveDocument?.(droppedDocuments, this.Document, (doc: Doc | Doc[]) => this.addDoc(doc instanceof Doc ? doc : doc.lastElement(), fieldKey));
+            const added = dropEvent.complete.docDragData.moveDocument?.(droppedDocuments, this.Document, (doc: Doc | Doc[]) => this.addDoc(toList(doc).lastElement(), fieldKey));
             Doc.SetContainer(droppedDocuments.lastElement(), this.dataDoc);
             !added && e.preventDefault();
             e.stopPropagation(); // prevent parent Doc from registering new position so that it snaps back into place
@@ -158,10 +158,10 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
         if (property === StyleProp.PointerEvents) return 'none';
         return this._props.styleProvider?.(doc, props, property);
     };
-    moveDoc1 = (docs: Doc | Doc[], targetCol: Doc | undefined, addDoc: any) => (docs instanceof Doc ? [docs] : docs).reduce((res, doc: Doc) => res && this.moveDoc(doc, addDoc, this.fieldKey + '_1'), true);
-    moveDoc2 = (docs: Doc | Doc[], targetCol: Doc | undefined, addDoc: any) => (docs instanceof Doc ? [docs] : docs).reduce((res, doc: Doc) => res && this.moveDoc(doc, addDoc, this.fieldKey + '_2'), true);
-    remDoc1 = (docs: Doc | Doc[]) => (docs instanceof Doc ? [docs] : docs).reduce((res, doc) => res && this.remDoc(doc, this.fieldKey + '_1'), true);
-    remDoc2 = (docs: Doc | Doc[]) => (docs instanceof Doc ? [docs] : docs).reduce((res, doc) => res && this.remDoc(doc, this.fieldKey + '_2'), true);
+    moveDoc1 = (docs: Doc | Doc[], targetCol: Doc | undefined, addDoc: any) => toList(docs).reduce((res, doc: Doc) => res && this.moveDoc(doc, addDoc, this.fieldKey + '_1'), true);
+    moveDoc2 = (docs: Doc | Doc[], targetCol: Doc | undefined, addDoc: any) => toList(docs).reduce((res, doc: Doc) => res && this.moveDoc(doc, addDoc, this.fieldKey + '_2'), true);
+    remDoc1 = (docs: Doc | Doc[]) => toList(docs).reduce((res, doc) => res && this.remDoc(doc, this.fieldKey + '_1'), true);
+    remDoc2 = (docs: Doc | Doc[]) => toList(docs).reduce((res, doc) => res && this.remDoc(doc, this.fieldKey + '_2'), true);
 
     /**
      * Tests for whether a comparison box slot (ie, before or after) has renderable text content
diff --git a/src/client/views/nodes/MapBox/MapBox.tsx b/src/client/views/nodes/MapBox/MapBox.tsx
index 822485b8d..7009977d0 100644
--- a/src/client/views/nodes/MapBox/MapBox.tsx
+++ b/src/client/views/nodes/MapBox/MapBox.tsx
@@ -15,7 +15,7 @@ import { MarkerEvent } from 'react-map-gl/dist/esm/types';
 import { ClientUtils, setupMoveUpEvents } from '../../../../ClientUtils';
 import { emptyFunction } from '../../../../Utils';
 import { Doc, DocListCast, Field, LinkedTo, Opt } from '../../../../fields/Doc';
-import { DocCast, NumCast, StrCast } from '../../../../fields/Types';
+import { DocCast, NumCast, StrCast, toList } from '../../../../fields/Types';
 import { DocumentType } from '../../../documents/DocumentTypes';
 import { DocUtils, Docs } from '../../../documents/Documents';
 import { DocumentManager } from '../../../util/DocumentManager';
@@ -235,14 +235,13 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
 
     /**
      * Called when dragging documents into map sidebar or directly into infowindow; to create a map marker, ref to MapMarkerDocument in Documents.ts
-     * @param doc
+     * @param docs
      * @param sidebarKey
      * @returns
      */
-    sidebarAddDocument = (doc: Doc | Doc[], sidebarKey?: string) => {
+    sidebarAddDocument = (docs: Doc | Doc[], sidebarKey?: string) => {
         if (!this.layoutDoc._layout_showSidebar) this.toggleSidebar();
-        const docs = doc instanceof Doc ? [doc] : doc;
-        docs.forEach(doc => {
+        toList(docs).forEach(doc => {
             let existingPin = this.allPushpins.find(pin => pin.latitude === doc.latitude && pin.longitude === doc.longitude) ?? this._selectedPinOrRoute;
             if (doc.latitude !== undefined && doc.longitude !== undefined && !existingPin) {
                 existingPin = this.createPushpin(NumCast(doc.latitude), NumCast(doc.longitude), StrCast(doc.map));
@@ -260,12 +259,15 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
             }
         }); // add to annotation list
 
-        return this.addDocument(doc, sidebarKey); // add to sidebar list
+        return this.addDocument(docs, sidebarKey); // add to sidebar list
     };
 
     removeMapDocument = (doc: Doc | Doc[], annotationKey?: string) => {
-        const docs = doc instanceof Doc ? [doc] : doc;
-        this.allAnnotations.filter(anno => docs.includes(DocCast(anno.mapPin))).forEach(anno => (anno.mapPin = undefined));
+        this.allAnnotations
+            .filter(anno => toList(doc).includes(DocCast(anno.mapPin)))
+            .forEach(anno => {
+                anno.mapPin = undefined;
+            });
         return this.removeDocument(doc, annotationKey, undefined);
     };
 
diff --git a/src/client/views/nodes/MapboxMapBox/MapboxContainer.tsx b/src/client/views/nodes/MapboxMapBox/MapboxContainer.tsx
index fce52ef4b..67872efc3 100644
--- a/src/client/views/nodes/MapboxMapBox/MapboxContainer.tsx
+++ b/src/client/views/nodes/MapboxMapBox/MapboxContainer.tsx
@@ -9,7 +9,7 @@ import { emptyFunction } from '../../../../Utils';
 import { Doc, DocListCast, Field, LinkedTo, Opt } from '../../../../fields/Doc';
 import { DocCss, Highlight } from '../../../../fields/DocSymbols';
 import { Id } from '../../../../fields/FieldSymbols';
-import { DocCast, NumCast, StrCast } from '../../../../fields/Types';
+import { DocCast, NumCast, StrCast, toList } from '../../../../fields/Types';
 import { DocumentType } from '../../../documents/DocumentTypes';
 import { DocUtils, Docs } from '../../../documents/Documents';
 import { DocumentManager } from '../../../util/DocumentManager';
@@ -120,7 +120,7 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent<FieldViewProps>
      */
     sidebarAddDocument = (docsIn: Doc | Doc[], sidebarKey?: string) => {
         if (!this.layoutDoc._layout_showSidebar) this.toggleSidebar();
-        const docs = docsIn instanceof Doc ? [docsIn] : docsIn;
+        const docs = toList(docsIn);
         docs.forEach(doc => {
             let existingPin = this.allPushpins.find(pin => pin.latitude === doc.latitude && pin.longitude === doc.longitude) ?? this.selectedPin;
             if (doc.latitude !== undefined && doc.longitude !== undefined && !existingPin) {
@@ -142,14 +142,14 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent<FieldViewProps>
         return this.addDocument(docs, sidebarKey); // add to sidebar list
     };
 
-    removeMapDocument = (doc: Doc | Doc[], annotationKey?: string) => {
-        const docs = doc instanceof Doc ? [doc] : doc;
+    removeMapDocument = (docsIn: Doc | Doc[], annotationKey?: string) => {
+        const docs = toList(docsIn);
         this.allAnnotations
             .filter(anno => docs.includes(DocCast(anno.mapPin)))
             .forEach(anno => {
                 anno.mapPin = undefined;
             });
-        return this.removeDocument(doc, annotationKey, undefined);
+        return this.removeDocument(docsIn, annotationKey, undefined);
     };
 
     /**
diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx
index fdba9ff49..780ac3acb 100644
--- a/src/client/views/nodes/PDFBox.tsx
+++ b/src/client/views/nodes/PDFBox.tsx
@@ -12,7 +12,7 @@ import { DocData } from '../../../fields/DocSymbols';
 import { Id } from '../../../fields/FieldSymbols';
 import { InkTool } from '../../../fields/InkField';
 import { ComputedField } from '../../../fields/ScriptField';
-import { Cast, FieldValue, ImageCast, NumCast, StrCast } from '../../../fields/Types';
+import { Cast, FieldValue, ImageCast, NumCast, StrCast, toList } from '../../../fields/Types';
 import { ImageField, PdfField } from '../../../fields/URLField';
 import { TraceMobx } from '../../../fields/util';
 import { emptyFunction } from '../../../Utils';
@@ -231,7 +231,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
     }
 
     sidebarAddDocTab = (docIn: Doc | Doc[], where: OpenWhere) => {
-        const docs = docIn instanceof Doc ? [docIn] : docIn;
+        const docs = toList(docIn);
         if (docs.some(doc => DocListCast(this.Document[this._props.fieldKey + '_sidebar']).includes(doc)) && !this.SidebarShown) {
             this.toggleSidebar(false);
             return true;
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index 1121134cf..4b80f2485 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -10,7 +10,7 @@ import { DocData } from '../../../fields/DocSymbols';
 import { InkTool } from '../../../fields/InkField';
 import { List } from '../../../fields/List';
 import { ObjectField } from '../../../fields/ObjectField';
-import { Cast, NumCast, StrCast } from '../../../fields/Types';
+import { Cast, NumCast, StrCast, toList } from '../../../fields/Types';
 import { AudioField, ImageField, VideoField } from '../../../fields/URLField';
 import { emptyFunction, formatTime } from '../../../Utils';
 import { Docs, DocUtils } from '../../documents/Documents';
@@ -670,7 +670,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl
     // for annotating, adds doc with time info
     @action.bound
     addDocWithTimecode(docIn: Doc | Doc[]): boolean {
-        const docs = docIn instanceof Doc ? [docIn] : docIn;
+        const docs = toList(docIn);
         const curTime = NumCast(this.layoutDoc._layout_currentTimecode);
         docs.forEach(doc => {
             doc._timecodeToHide = (doc._timecodeToShow = curTime) + 1;
diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx
index 73afd66f6..725c5bab4 100644
--- a/src/client/views/nodes/WebBox.tsx
+++ b/src/client/views/nodes/WebBox.tsx
@@ -14,7 +14,7 @@ import { InkTool } from '../../../fields/InkField';
 import { List } from '../../../fields/List';
 import { RefField } from '../../../fields/RefField';
 import { listSpec } from '../../../fields/Schema';
-import { Cast, NumCast, StrCast, WebCast } from '../../../fields/Types';
+import { Cast, NumCast, StrCast, toList, WebCast } from '../../../fields/Types';
 import { ImageField, WebField } from '../../../fields/URLField';
 import { TraceMobx } from '../../../fields/util';
 import { emptyFunction, stringHash } from '../../../Utils';
@@ -895,7 +895,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
 
     addDocumentWrapper = (docs: Doc | Doc[], annotationKey?: string) => {
         this._url &&
-            (docs instanceof Doc ? [docs] : docs).forEach(doc => {
+            toList(docs).forEach(doc => {
                 doc.config_data = new WebField(this._url);
             });
         return this.addDocument(docs, annotationKey);
diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx
index 0a4efbfe4..ed9635bdd 100644
--- a/src/client/views/nodes/trails/PresBox.tsx
+++ b/src/client/views/nodes/trails/PresBox.tsx
@@ -14,7 +14,7 @@ import { List } from '../../../../fields/List';
 import { ObjectField } from '../../../../fields/ObjectField';
 import { listSpec } from '../../../../fields/Schema';
 import { ComputedField, ScriptField } from '../../../../fields/ScriptField';
-import { BoolCast, Cast, DocCast, NumCast, StrCast } from '../../../../fields/Types';
+import { BoolCast, Cast, DocCast, NumCast, StrCast, toList } from '../../../../fields/Types';
 import { emptyFunction, emptyPath, stringHash } from '../../../../Utils';
 import { DocServer } from '../../../DocServer';
 import { Docs } from '../../../documents/Documents';
@@ -1086,7 +1086,10 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
     };
 
     childLayoutTemplate = () => Docs.Create.PresElementBoxDocument();
-    removeDocument = (doc: Doc | Doc[]) => !(doc instanceof Doc ? [doc] : doc).map(d => Doc.RemoveDocFromList(this.Document, this.fieldKey, d)).some(p => !p);
+    removeDocument = (doc: Doc | Doc[]) =>
+        !toList(doc)
+            .map(d => Doc.RemoveDocFromList(this.Document, this.fieldKey, d))
+            .some(p => !p);
     getTransform = () => this.ScreenToLocalBoxXf().translate(-5, -65); // listBox padding-left and pres-box-cont minHeight
     panelHeight = () => this._props.PanelHeight() - 40;
     /**
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index 7b5e21ab7..c9aaa5158 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -23,7 +23,7 @@ import { FieldId, RefField } from './RefField';
 import { RichTextField } from './RichTextField';
 import { listSpec } from './Schema';
 import { ComputedField, ScriptField } from './ScriptField';
-import { BoolCast, Cast, DocCast, FieldValue, NumCast, StrCast, ToConstructor } from './Types';
+import { BoolCast, Cast, DocCast, FieldValue, NumCast, StrCast, ToConstructor, toList } from './Types';
 import { containedFieldChangedHandler, deleteProperty, GetEffectiveAcl, getField, getter, makeEditable, makeReadOnly, setter, SharingPermissions } from './util';
 
 export const LinkedTo = '-linkedTo';
@@ -221,8 +221,8 @@ export class Doc extends RefField {
     public static set ActiveDashboard(val: Opt<Doc>) {  Doc.UserDoc().activeDashboard = val; } // prettier-ignore
 
     public static IsInMyOverlay(doc: Doc)     { return Doc.MyOverlayDocs.includes(doc); } // prettier-ignore
-    public static AddToMyOverlay(doc: Doc)    { Doc.ActiveDashboard?.myOverlayDocs ? Doc.AddDocToList(Doc.ActiveDashboard, 'myOverlayDocs', doc) : Doc.AddDocToList(DocCast(Doc.UserDoc().myOverlayDocs), undefined, doc); } // prettier-ignore
-    public static RemFromMyOverlay(doc: Doc)  { Doc.ActiveDashboard?.myOverlayDocs ? Doc.RemoveDocFromList(Doc.ActiveDashboard,'myOverlayDocs', doc) : Doc.RemoveDocFromList(DocCast(Doc.UserDoc().myOverlayDocs), undefined, doc); } // prettier-ignore
+    public static AddToMyOverlay(doc: Doc)    { return Doc.ActiveDashboard?.myOverlayDocs ? Doc.AddDocToList(Doc.ActiveDashboard, 'myOverlayDocs', doc) : Doc.AddDocToList(DocCast(Doc.UserDoc().myOverlayDocs), undefined, doc); } // prettier-ignore
+    public static RemFromMyOverlay(doc: Doc)  { return Doc.ActiveDashboard?.myOverlayDocs ? Doc.RemoveDocFromList(Doc.ActiveDashboard,'myOverlayDocs', doc) : Doc.RemoveDocFromList(DocCast(Doc.UserDoc().myOverlayDocs), undefined, doc); } // prettier-ignore
     public static AddToMyPublished(doc: Doc)  {
         doc[DocData].title_custom = true;
         doc[DocData].layout_showTitle = 'title'; 
@@ -1211,7 +1211,7 @@ export namespace Doc {
     }
     export function linkFollowHighlight(destDoc: Doc | Doc[], dataAndDisplayDocs = true, presentationEffect?: Doc) {
         linkFollowUnhighlight();
-        (destDoc instanceof Doc ? [destDoc] : destDoc).forEach(doc => Doc.HighlightDoc(doc, dataAndDisplayDocs, presentationEffect));
+        toList(destDoc).forEach(doc => Doc.HighlightDoc(doc, dataAndDisplayDocs, presentationEffect));
         document.removeEventListener('pointerdown', linkFollowUnhighlight);
         document.addEventListener('pointerdown', linkFollowUnhighlight);
         if (UnhighlightTimer) clearTimeout(UnhighlightTimer);
diff --git a/src/fields/Types.ts b/src/fields/Types.ts
index 57a310f6d..26196d15d 100644
--- a/src/fields/Types.ts
+++ b/src/fields/Types.ts
@@ -86,6 +86,10 @@ export function Cast<T extends CastCtor>(field: FieldResult, ctor: T, defaultVal
     return defaultVal === null ? undefined : defaultVal;
 }
 
+export function toList(doc: Doc | Doc[]) {
+    return doc instanceof Doc ? [doc] : doc;
+}
+
 export function DocCast(field: FieldResult, defaultVal?: Doc) {
     const doc = Cast(field, Doc, null);
     return doc && !(doc instanceof Promise) ? doc : (defaultVal as Doc);
-- 
cgit v1.2.3-70-g09d2


From 729114c0867e3cc8d8e0668bae451976b387cb34 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Sun, 28 Apr 2024 13:08:09 -0400
Subject: simplified explore mode implementation

---
 src/client/views/DocComponent.tsx                  |  1 +
 src/client/views/LightboxView.tsx                  |  1 -
 .../views/collections/CollectionNoteTakingView.tsx |  3 +-
 .../views/collections/CollectionStackingView.tsx   |  2 -
 src/client/views/collections/TabDocView.tsx        |  1 -
 .../collectionFreeForm/CollectionFreeFormView.tsx  | 44 +++++++---------------
 .../collectionSchema/SchemaTableCell.tsx           |  2 +-
 .../views/newlightbox/Header/LightboxHeader.tsx    | 12 +-----
 src/client/views/newlightbox/NewLightboxView.tsx   | 23 ++++++-----
 src/client/views/nodes/DocumentView.tsx            | 35 ++++++++++-------
 src/client/views/nodes/FieldView.tsx               |  1 -
 src/client/views/nodes/ImageBox.tsx                | 18 ++++-----
 .../views/nodes/MapboxMapBox/MapboxContainer.tsx   |  1 -
 src/client/views/nodes/VideoBox.tsx                |  4 ++
 .../views/nodes/formattedText/FormattedTextBox.tsx |  2 +-
 src/client/views/topbar/TopBar.tsx                 |  2 +-
 16 files changed, 68 insertions(+), 84 deletions(-)

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

diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx
index bbd902a96..fd0b4e83f 100644
--- a/src/client/views/DocComponent.tsx
+++ b/src/client/views/DocComponent.tsx
@@ -68,6 +68,7 @@ export interface ViewBoxInterface {
     removeDocument?: (doc: Doc | Doc[], annotationKey?: string, leavePushpin?: boolean, dontAddToRemoved?: boolean) => boolean; // add a document (used only by collections)
     select?: (ctrlKey: boolean, shiftKey: boolean) => void;
     focus?: (textAnchor: Doc, options: FocusViewOptions) => Opt<number>;
+    browseTo?: (clientX: number, clientY: number, browseTransitionTime: number) => boolean; // when in explore mode, this allows a component view to display itself in an easy to read way (ie, a document in a freeformview will zoom itself to mostly full screen)
     viewTransition?: () => Opt<string>; // duration of a view transition animation
     isAnyChildContentActive?: () => boolean; // is any child content of the document active
     onClickScriptDisable?: () => 'never' | 'always'; // disable click scripts : never, always, or undefined = only when selected
diff --git a/src/client/views/LightboxView.tsx b/src/client/views/LightboxView.tsx
index 0dd836f70..7bf6bb9e5 100644
--- a/src/client/views/LightboxView.tsx
+++ b/src/client/views/LightboxView.tsx
@@ -293,7 +293,6 @@ export class LightboxView extends ObservableReactComponent<LightboxViewProps> {
                             whenChildContentsActiveChanged={emptyFunction}
                             addDocTab={this.AddDocTab}
                             pinToPres={TabDocView.PinDoc}
-                            onBrowseClickScript={DocumentView.exploreMode}
                             focus={emptyFunction}
                         />
                     </GestureOverlay>
diff --git a/src/client/views/collections/CollectionNoteTakingView.tsx b/src/client/views/collections/CollectionNoteTakingView.tsx
index 2ba6f5bf4..9212a6609 100644
--- a/src/client/views/collections/CollectionNoteTakingView.tsx
+++ b/src/client/views/collections/CollectionNoteTakingView.tsx
@@ -59,7 +59,7 @@ export class CollectionNoteTakingView extends CollectionSubView() {
     }
 
     @computed get chromeHidden() {
-        return !!(BoolCast(this.layoutDoc.chromeHidden) || this._props.onBrowseClickScript?.());
+        return BoolCast(this.layoutDoc.chromeHidden) || SnappingManager.ExploreMode;
     }
     // columnHeaders returns the list of SchemaHeaderFields currently being used by the layout doc to render the columns
     @computed get colHeaderData() {
@@ -283,7 +283,6 @@ export class CollectionNoteTakingView extends CollectionSubView() {
                 layout_showTitle={this._props.childlayout_showTitle}
                 dragAction={StrCast(this.layoutDoc.childDragAction) as dropActionType}
                 onClickScript={this.onChildClickHandler}
-                onBrowseClickScript={this._props.onBrowseClickScript}
                 onDoubleClickScript={this.onChildDoubleClickHandler}
                 ScreenToLocalTransform={noteTakingDocTransform}
                 focus={this.focusDocument}
diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx
index b79d660c6..c47fe915a 100644
--- a/src/client/views/collections/CollectionStackingView.tsx
+++ b/src/client/views/collections/CollectionStackingView.tsx
@@ -338,7 +338,6 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
                 isContentActive={doc.onClick ? this.isChildButtonContentActive : this.isChildContentActive}
                 onKey={this.onKeyDown}
                 DataTransition={trans}
-                onBrowseClickScript={this._props.onBrowseClickScript}
                 isDocumentActive={this.isContentActive}
                 LayoutTemplate={this._props.childLayoutTemplate}
                 LayoutTemplateString={this._props.childLayoutString}
@@ -654,7 +653,6 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
                     addDocument={this._props.addDocument}
                     moveDocument={this._props.moveDocument}
                     addDocTab={this._props.addDocTab}
-                    onBrowseClickScript={this._props.onBrowseClickScript}
                     pinToPres={emptyFunction}
                     rootSelected={this.rootSelected}
                     removeDocument={this._props.removeDocument}
diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx
index 5c16fecd0..e98e444ed 100644
--- a/src/client/views/collections/TabDocView.tsx
+++ b/src/client/views/collections/TabDocView.tsx
@@ -594,7 +594,6 @@ export class TabDocView extends ObservableReactComponent<TabDocViewProps> {
                     hideTitle={this._props.keyValue}
                     Document={this._document}
                     TemplateDataDocument={!Doc.AreProtosEqual(this._document[DocData], this._document) ? this._document[DocData] : undefined}
-                    onBrowseClickScript={DocumentView.exploreMode}
                     waitForDoubleClickToClick={this.waitForDoubleClick}
                     isContentActive={this.isContentActive}
                     isDocumentActive={returnFalse}
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index fa4dbf9a2..d3998aee8 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -241,7 +241,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         this._keyframeEditing = set;
     };
     getKeyFrameEditing = () => this._keyframeEditing;
-    onBrowseClickHandler = () => this._props.onBrowseClickScript?.() || ScriptCast(this.layoutDoc.onBrowseClick);
     onChildClickHandler = () => this._props.childClickScript || ScriptCast(this.Document.onChildClick);
     onChildDoubleClickHandler = () => this._props.childDoubleClickScript || ScriptCast(this.Document.onChildDoubleClick);
     elementFunc = () => this._layoutElements;
@@ -726,11 +725,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     onClick = (e: React.MouseEvent) => {
         if (this._lightboxDoc) this._lightboxDoc = undefined;
         if (ClientUtils.isClick(e.pageX, e.pageY, this._downX, this._downY, this._downTime)) {
-            if (this.onBrowseClickHandler()) {
-                this.onBrowseClickHandler().script.run({ documentView: this.DocumentView?.(), clientX: e.clientX, clientY: e.clientY });
-                e.stopPropagation();
-                e.preventDefault();
-            } else if (this.isContentActive() && e.shiftKey) {
+            if (this.isContentActive() && e.shiftKey) {
                 // reset zoom of freeform view to 1-to-1 on a shift + double click
                 this.zoomSmoothlyAboutPt(this.screenToFreeformContentsXf.transformPoint(e.clientX, e.clientY), 1);
                 e.stopPropagation();
@@ -1256,7 +1251,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                 onClickScript={this.onChildClickHandler}
                 onKey={this.onKeyDown}
                 onDoubleClickScript={this.onChildDoubleClickHandler}
-                onBrowseClickScript={this.onBrowseClickHandler}
                 bringToFront={this.bringToFront}
                 ScreenToLocalTransform={childLayout.z ? this.ScreenToLocalBoxXf : this.ScreenToContentsXf}
                 PanelWidth={childLayout[Width]}
@@ -1962,7 +1956,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                             onClickScript={this.onChildClickHandler}
                             onKey={this.onKeyDown}
                             onDoubleClickScript={this.onChildDoubleClickHandler}
-                            onBrowseClickScript={this.onBrowseClickHandler}
                             childFilters={this.childDocFilters}
                             childFiltersByRanges={this.childDocRangeFilters}
                             searchFilterDocs={this.searchFilterDocs}
@@ -1987,6 +1980,19 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
             </div>
         );
     }
+    browseTo = (clientX: number, clientY: number, browseTransitionTime: number): boolean => {
+        if (!this.Document.isGroup) {
+            const dfltScale = this.isAnnotationOverlay ? 1 : 0.5;
+            if (this.layoutDoc[this.scaleFieldKey] !== dfltScale) {
+                this.zoomSmoothlyAboutPt(this.screenToFreeformContentsXf.transformPoint(clientX, clientY), dfltScale, browseTransitionTime);
+                return true;
+            }
+            return !!this.DocumentView?.()
+                .containerViewPath?.()
+                ?.some(cont => cont.ComponentView?.browseTo?.(clientX, clientY, browseTransitionTime));
+        }
+        return false;
+    };
 }
 
 @observer
@@ -1995,28 +2001,6 @@ class CollectionFreeFormOverlayView extends React.Component<{ elements: () => Vi
         return this.props.elements().filter(ele => ele.bounds?.z).map(ele => ele.ele); // prettier-ignore
     }
 }
-
-export function CollectionBrowseClick(dv: DocumentView, clientX: number, clientY: number) {
-    const browseTransitionTime = 500;
-    SelectionManager.DeselectAll();
-    dv &&
-        DocumentManager.Instance.showDocument(dv.Document, { zoomScale: 0.8, willZoomCentered: true }, (focused: boolean) => {
-            if (!focused) {
-                const selfFfview = !dv.Document.isGroup && dv.ComponentView instanceof CollectionFreeFormView ? dv.ComponentView : undefined;
-                const containers = dv.containerViewPath?.() ?? [];
-                let parFfview = dv.CollectionFreeFormView;
-                containers.forEach(cont => {
-                    parFfview = parFfview ?? cont.CollectionFreeFormView;
-                });
-
-                while (parFfview?.Document.isGroup) parFfview = parFfview.DocumentView?.().CollectionFreeFormView;
-                const ffview = selfFfview && selfFfview.layoutDoc[selfFfview.scaleFieldKey] !== 0.5 ? selfFfview : parFfview; // if focus doc is a freeform that is not at it's default 0.5 scale, then zoom out on it.  Otherwise, zoom out on the parent ffview
-                ffview?.zoomSmoothlyAboutPt(ffview.screenToFreeformContentsXf.transformPoint(clientX, clientY), ffview?.isAnnotationOverlay ? 1 : 0.5, browseTransitionTime);
-                Doc.linkFollowHighlight(dv?.Document, false);
-            }
-        });
-}
-ScriptingGlobals.add(CollectionBrowseClick);
 // eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function nextKeyFrame(readOnly: boolean) {
     !readOnly && (SelectionManager.Views[0].ComponentView as CollectionFreeFormView)?.changeKeyFrame();
diff --git a/src/client/views/collections/collectionSchema/SchemaTableCell.tsx b/src/client/views/collections/collectionSchema/SchemaTableCell.tsx
index b89ca3614..dfef3aa48 100644
--- a/src/client/views/collections/collectionSchema/SchemaTableCell.tsx
+++ b/src/client/views/collections/collectionSchema/SchemaTableCell.tsx
@@ -179,7 +179,7 @@ export class SchemaTableCell extends ObservableReactComponent<SchemaTableCellPro
             case ColumnType.Image: return <SchemaImageCell {...this._props} />;
             case ColumnType.Boolean: return <SchemaBoolCell {...this._props} />;
             case ColumnType.RTF: return <SchemaRTFCell {...this._props} />;
-            case ColumnType.Enumeration: return <SchemaEnumerationCell {...this._props} options={this._props.getFinfo(this._props.fieldKey)?.values?.map(val => val.toString())} />;
+            case ColumnType.Enumeration: return <SchemaEnumerationCell {...this._props} options={this._props.getFinfo(this._props.fieldKey)?.values?.map(val => Field.toString(val))} />;
             case ColumnType.Date: return <SchemaDateCell {...this._props} />;
             default:  return this.defaultCellContent;
         }
diff --git a/src/client/views/newlightbox/Header/LightboxHeader.tsx b/src/client/views/newlightbox/Header/LightboxHeader.tsx
index 76efe0185..f89d3173a 100644
--- a/src/client/views/newlightbox/Header/LightboxHeader.tsx
+++ b/src/client/views/newlightbox/Header/LightboxHeader.tsx
@@ -53,17 +53,7 @@ export const NewLightboxHeader = (props: INewLightboxHeader) => {
                     <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 />}
-                    />
+                    <Button onClick={() => 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/NewLightboxView.tsx b/src/client/views/newlightbox/NewLightboxView.tsx
index dcbc9fc50..6de4efc0d 100644
--- a/src/client/views/newlightbox/NewLightboxView.tsx
+++ b/src/client/views/newlightbox/NewLightboxView.tsx
@@ -6,7 +6,7 @@ import { returnEmptyDoclist, returnEmptyFilter, returnTrue } from '../../../Clie
 import { emptyFunction } from '../../../Utils';
 import { Doc, DocListCast, Opt } from '../../../fields/Doc';
 import { InkTool } from '../../../fields/InkField';
-import { Cast, NumCast, StrCast } from '../../../fields/Types';
+import { Cast, NumCast, StrCast, toList } from '../../../fields/Types';
 import { DocUtils } from '../../documents/Documents';
 import { DocumentManager } from '../../util/DocumentManager';
 import { LinkManager } from '../../util/LinkManager';
@@ -204,15 +204,19 @@ export class NewLightboxView extends React.Component<LightboxViewProps> {
             this._docFilters = (f => (this._docFilters ? [this._docFilters.push(f) as any, this._docFilters][1] : [f]))(`cookies:${cookie}:provide`);
         }
     }
-    public static AddDocTab = (doc: Doc, location: OpenWhere, layoutTemplate?: Doc | string) => {
+    public static AddDocTab = (docsIn: Doc | Doc[], location: OpenWhere, layoutTemplate?: Doc | string) => {
         SelectionManager.DeselectAll();
-        return NewLightboxView.SetNewLightboxDoc(
-            doc,
-            undefined,
-            [...DocListCast(doc[Doc.LayoutFieldKey(doc)]), ...DocListCast(doc[Doc.LayoutFieldKey(doc) + '_annotations']).filter(anno => anno.annotationOn !== doc), ...(NewLightboxView._future ?? [])].sort(
-                (a: Doc, b: Doc) => NumCast(b._timecodeToShow) - NumCast(a._timecodeToShow)
-            ),
-            layoutTemplate
+        const doc = toList(docsIn).lastElement();
+        return (
+            doc &&
+            NewLightboxView.SetNewLightboxDoc(
+                doc,
+                undefined,
+                [...DocListCast(doc[Doc.LayoutFieldKey(doc)]), ...DocListCast(doc[Doc.LayoutFieldKey(doc) + '_annotations']).filter(anno => anno.annotationOn !== doc), ...(NewLightboxView._future ?? [])].sort(
+                    (a: Doc, b: Doc) => NumCast(b._timecodeToShow) - NumCast(a._timecodeToShow)
+                ),
+                layoutTemplate
+            )
         );
     };
     docFilters = () => NewLightboxView._docFilters || [];
@@ -312,7 +316,6 @@ export class NewLightboxView extends React.Component<LightboxViewProps> {
                         whenChildContentsActiveChanged={emptyFunction}
                         addDocTab={this.addDocTab}
                         pinToPres={TabDocView.PinDoc}
-                        onBrowseClickScript={DocumentView.exploreMode}
                         focus={emptyFunction}
                     />
                 </GestureOverlay>
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 5962cd09f..555b4ba92 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -165,7 +165,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
     @computed get borderPath()         { return this.style(this.Document, StyleProp.BorderPath); } // prettier-ignore
 
     @computed get onClickHandler() {
-        return this._props.onClickScript?.() ?? this._props.onBrowseClickScript?.() ?? ScriptCast(this.Document.onClick, ScriptCast(this.layoutDoc.onClick));
+        return this._props.onClickScript?.() ?? ScriptCast(this.Document.onClick, ScriptCast(this.layoutDoc.onClick));
     }
     @computed get onDoubleClickHandler() {
         return this._props.onDoubleClickScript?.() ?? ScriptCast(this.layoutDoc.onDoubleClick, ScriptCast(this.Document.onDoubleClick));
@@ -189,13 +189,13 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
     @computed get _rootSelected() {
         return this._props.isSelected() || BoolCast(this._props.TemplateDataDocument && this._props.rootSelected?.());
     }
-    /// disable pointer events on content when there's an enabled onClick script (but not the browse script) and the contents aren't forced active, or if contents are marked inactive
+    /// disable pointer events on content when there's an enabled onClick script (and not in explore mode) and the contents aren't forced active, or if contents are marked inactive
     @computed get _contentPointerEvents() {
         TraceMobx();
         return this._props.contentPointerEvents ??
             ((!this.disableClickScriptFunc && //
                 this.onClickHandler &&
-                !this._props.onBrowseClickScript?.() &&
+                !SnappingManager.ExploreMode &&
                 !this.layoutDoc.layout_isSvg &&
                 this.isContentActive() !== true) ||
                 this.isContentActive() === false)
@@ -310,6 +310,20 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
         if (!StrCast(this.layoutDoc._layout_showTitle)) this.layoutDoc._layout_showTitle = 'title';
         setTimeout(() => this._titleRef.current?.setIsFocused(true)); // use timeout in case title wasn't shown to allow re-render so that titleref will be defined
     };
+    onBrowseClick = (e: React.MouseEvent) => {
+        const browseTransitionTime = 500;
+        SelectionManager.DeselectAll();
+        DocumentManager.Instance.showDocument(this.Document, { zoomScale: 0.8, willZoomCentered: true }, (focused: boolean) => {
+            if (!focused && this._docView) {
+                this._docView
+                    .docViewPath()
+                    .reverse()
+                    .find(cont => cont.ComponentView?.browseTo?.(e.clientX, e.clientY, browseTransitionTime));
+                Doc.linkFollowHighlight(this.Document, false);
+            }
+        });
+        e.stopPropagation();
+    };
     onClick = action((e: React.MouseEvent | React.PointerEvent) => {
         if (this._props.isGroupActive?.() === 'child' && !this._props.isDocumentActive?.()) return;
         const documentView = this._docView;
@@ -393,10 +407,8 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
         if ((Doc.ActiveTool === InkTool.None || this._props.addDocTab === returnFalse) && !(this._props.TemplateDataDocument && !(e.ctrlKey || e.button > 0))) {
             // click events stop here if the document is active and no modes are overriding it
             // if this is part of a template, let the event go up to the template root unless right/ctrl clicking
-            if (
-                // prettier-ignore
-                (this._props.isDocumentActive?.() || this._props.isContentActive?.()) &&
-                !this._props.onBrowseClickScript?.() &&
+            if ((this._props.isDocumentActive?.() || this._props.isContentActive?.()) &&
+                !SnappingManager.ExploreMode &&
                 !this.Document.ignoreClick &&
                 e.button === 0 &&
                 !Doc.IsInMyOverlay(this.layoutDoc)
@@ -408,7 +420,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
                 if (!this.layoutDoc._lockedPosition && (!this.isContentActive() || BoolCast(this.layoutDoc._dragWhenActive, this._props.dragWhenActive))) {
                     document.addEventListener('pointermove', this.onPointerMove);
                 }
-            }
+            } // prettier-ignore
             document.addEventListener('pointerup', this.onPointerUp);
         }
     };
@@ -981,7 +993,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
                 ref={this._mainCont}
                 onContextMenu={this.onContextMenu}
                 onPointerDown={this.onPointerDown}
-                onClick={this.onClick}
+                onClick={SnappingManager.ExploreMode ? this.onBrowseClick : this.onClick}
                 onPointerEnter={() => (!SnappingManager.IsDragging || SnappingManager.CanEmbed) && Doc.BrushDoc(this.Document)}
                 onPointerOver={() => (!SnappingManager.IsDragging || SnappingManager.CanEmbed) && Doc.BrushDoc(this.Document)}
                 onPointerLeave={e => !isParentOf(this._contentDiv, document.elementFromPoint(e.nativeEvent.x, e.nativeEvent.y)) && Doc.UnBrushDoc(this.Document)}
@@ -1055,7 +1067,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
             recorder.start();
             const stopFunc = () => {
                 recorder.stop();
-                DictationManager.Controls.stop(false);
+                DictationManager.Controls.stop(/* false */);
                 dataDoc.audioAnnoState = AudioAnnoState.stopped;
                 gumStream.getAudioTracks()[0].stop();
             };
@@ -1086,9 +1098,6 @@ export class DocumentView extends DocComponent<DocumentViewProps & { CollectionF
     }
     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);
-    }
 
     constructor(props: DocumentViewProps & { CollectionFreeFormDocumentView?: () => CollectionFreeFormDocumentView }) {
         super(props);
diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx
index 3f453eb93..c2f946a88 100644
--- a/src/client/views/nodes/FieldView.tsx
+++ b/src/client/views/nodes/FieldView.tsx
@@ -74,7 +74,6 @@ export interface FieldViewSharedProps {
     onDoubleClickScript?: () => ScriptField;
     onPointerDownScript?: () => ScriptField;
     onPointerUpScript?: () => ScriptField;
-    onBrowseClickScript?: () => ScriptField | undefined;
     // eslint-disable-next-line no-use-before-define
     onKey?: (e: React.KeyboardEvent, fieldProps: FieldViewProps) => boolean | undefined;
     layout_fitWidth?: (doc: Doc) => boolean | undefined;
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 90b4a6740..617c45715 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -70,7 +70,13 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl
     private _getAnchor: (savedAnnotations: Opt<ObservableMap<number, HTMLDivElement[]>>, addAsAnnotation: boolean) => Opt<Doc> = () => undefined;
     private _overlayIconRef = React.createRef<HTMLDivElement>();
     private _marqueeref = React.createRef<MarqueeAnnotator>();
+    private _mainCont: React.RefObject<HTMLDivElement> = React.createRef();
+    private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef();
+    @observable _savedAnnotations = new ObservableMap<number, HTMLDivElement[]>();
     @observable _curSuffix = '';
+    @observable _error = '';
+    @observable _isHovering = false; // flag to switch between primary and alternate images on hover
+    _ffref = React.createRef<CollectionFreeFormView>();
 
     constructor(props: FieldViewProps) {
         super(props);
@@ -274,7 +280,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl
         }
     };
 
-    choosePath(url: URL) {
+    choosePath = (url: URL) => {
         if (!url?.href) return '';
         const lower = url.href.toLowerCase();
         if (url.protocol === 'data') return url.href;
@@ -283,7 +289,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl
 
         const ext = extname(url.href);
         return url.href.replace(ext, (this._error ? '_o' : this._curSuffix) + ext);
-    }
+    };
     getScrollHeight = () => (this._props.layout_fitWidth?.(this.Document) !== false && NumCast(this.layoutDoc._freeform_scale, 1) === NumCast(this.dataDoc._freeform_scaleMin, 1) ? this.nativeSize.nativeHeight : undefined);
 
     @computed get nativeSize() {
@@ -346,9 +352,6 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl
         return paths.length ? paths : [defaultUrl.href];
     }
 
-    @observable _error = '';
-
-    @observable _isHovering = false; // flag to switch between primary and alternate images on hover
     @computed get content() {
         TraceMobx();
 
@@ -407,13 +410,11 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl
         );
     }
 
-    private _mainCont: React.RefObject<HTMLDivElement> = React.createRef();
-    private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef();
-    @observable _savedAnnotations = new ObservableMap<number, HTMLDivElement[]>();
     @computed get annotationLayer() {
         TraceMobx();
         return <div className="imageBox-annotationLayer" style={{ height: this._props.PanelHeight() }} ref={this._annotationLayer} />;
     }
+    browseTo = (clientX: number, clientY: number, browseTransitionTime: number) => !!this._ffref.current?.browseTo?.(clientX, clientY, browseTransitionTime);
     screenToLocalTransform = () => this.ScreenToLocalBoxXf().translate(0, NumCast(this.layoutDoc._layout_scrollTop) * this.ScreenToLocalBoxXf().Scale);
     marqueeDown = (e: React.PointerEvent) => {
         if (!this.dataDoc[this.fieldKey]) {
@@ -447,7 +448,6 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl
     };
     focus = (anchor: Doc, options: FocusViewOptions) => (anchor.type === DocumentType.CONFIG ? undefined : this._ffref.current?.focus(anchor, options));
 
-    _ffref = React.createRef<CollectionFreeFormView>();
     savedAnnotations = () => this._savedAnnotations;
     render() {
         TraceMobx();
diff --git a/src/client/views/nodes/MapboxMapBox/MapboxContainer.tsx b/src/client/views/nodes/MapboxMapBox/MapboxContainer.tsx
index 67872efc3..c19528af6 100644
--- a/src/client/views/nodes/MapboxMapBox/MapboxContainer.tsx
+++ b/src/client/views/nodes/MapboxMapBox/MapboxContainer.tsx
@@ -802,7 +802,6 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent<FieldViewProps>
                                           NativeHeight={returnOne}
                                           onKey={undefined}
                                           onDoubleClickScript={undefined}
-                                          onBrowseClickScript={undefined}
                                           childFilters={returnEmptyFilter}
                                           childFiltersByRanges={returnEmptyFilter}
                                           searchFilterDocs={returnEmptyDoclist}
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index 4b80f2485..fd7997248 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -64,6 +64,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl
     private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef();
     private _playRegionTimer: any = null; // timeout for playback
     private _controlsFadeTimer: any = null; // timeout for controls fade
+    private _ffref = React.createRef<CollectionFreeFormView>();
 
     constructor(props: FieldViewProps) {
         super(props);
@@ -978,6 +979,8 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl
         this._props.bringToFront?.(cropping);
         return cropping;
     };
+    browseTo = (clientX: number, clientY: number, browseTransitionTime: number) => !!this._ffref.current?.browseTo(clientX, clientY, browseTransitionTime);
+    focus = (anchor: Doc, options: FocusViewOptions) => (anchor.type === DocumentType.CONFIG ? undefined : this._ffref.current?.focus(anchor, options));
     savedAnnotations = () => this._savedAnnotations;
     render() {
         const borderRad = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BorderRounding);
@@ -1005,6 +1008,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl
                         <CollectionFreeFormView
                             // eslint-disable-next-line react/jsx-props-no-spreading
                             {...this._props}
+                            ref={this._ffref}
                             setContentViewBox={emptyFunction}
                             NativeWidth={returnZero}
                             NativeHeight={returnZero}
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 3cd8b8b58..52d9c3b31 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -2095,7 +2095,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
                         cursor: this._props.isContentActive() ? 'text' : undefined,
                         height: this._props.height ? 'max-content' : undefined,
                         overflow: this.layout_autoHeight ? 'hidden' : undefined,
-                        pointerEvents: Doc.ActiveTool === InkTool.None && !this._props.onBrowseClickScript?.() ? undefined : 'none',
+                        pointerEvents: Doc.ActiveTool === InkTool.None && !SnappingManager.ExploreMode ? undefined : 'none',
                     }}
                     onContextMenu={this.specificContextMenu}
                     onKeyDown={this.onKeyDown}
diff --git a/src/client/views/topbar/TopBar.tsx b/src/client/views/topbar/TopBar.tsx
index 1ab0932a3..f82ae34c5 100644
--- a/src/client/views/topbar/TopBar.tsx
+++ b/src/client/views/topbar/TopBar.tsx
@@ -13,6 +13,7 @@ import { GetEffectiveAcl } from '../../../fields/util';
 import { emptyFunction } from '../../../Utils';
 import { CurrentUserUtils } from '../../util/CurrentUserUtils';
 import { DocumentManager } from '../../util/DocumentManager';
+import { dropActionType } from '../../util/DropActionTypes';
 import { PingManager } from '../../util/PingManager';
 import { ReportManager } from '../../util/reportManager/ReportManager';
 import { ServerStats } from '../../util/ServerStats';
@@ -28,7 +29,6 @@ import { DocumentViewInternal, returnEmptyDocViewList } from '../nodes/DocumentV
 import { ObservableReactComponent } from '../ObservableReactComponent';
 import { DefaultStyleProvider } from '../StyleProvider';
 import './TopBar.scss';
-import { dropActionType } from '../../util/DropActionTypes';
 
 /**
  * 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


From ab873e90112f2cac204a57a1b405cc241f7e8381 Mon Sep 17 00:00:00 2001
From: geireann <geireann.lindfield@gmail.com>
Date: Sun, 28 Apr 2024 16:01:49 -0400
Subject: cleaned up explore mode to use focus().  fixed focusing on group doc
 elements to not focus on group first.

---
 src/client/util/DocumentManager.ts                 |  7 +++--
 src/client/views/DocComponent.tsx                  |  1 -
 .../collectionFreeForm/CollectionFreeFormView.tsx  | 31 ++++++++++++----------
 src/client/views/nodes/DocumentView.tsx            |  5 ++--
 src/client/views/nodes/FieldView.tsx               |  3 ++-
 src/client/views/nodes/ImageBox.tsx                |  1 -
 src/client/views/nodes/VideoBox.tsx                |  1 -
 7 files changed, 27 insertions(+), 22 deletions(-)

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

diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts
index ac3721436..5a6ce528f 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -272,7 +272,8 @@ export class DocumentManager {
         docContextPath.shift();
         const childViewIterator = async (docView: DocumentView) => {
             const innerDoc = docContextPath.shift();
-            return { focused: false, viewSpec: innerDoc, childDocView: innerDoc && !innerDoc.layout_unrendered ? (await docView.ComponentView?.getView?.(innerDoc, options)) ?? this.getDocumentView(innerDoc) : undefined };
+            const viewSpec = innerDoc?.isGroup ? (docContextPath[0] ?? innerDoc) : innerDoc;
+            return { focused: false, viewSpec: viewSpec, childDocView: innerDoc && !innerDoc.layout_unrendered ? (await docView.ComponentView?.getView?.(innerDoc, options)) ?? this.getDocumentView(innerDoc) : undefined };
         };
 
         if (rootContextView) {
@@ -294,6 +295,7 @@ export class DocumentManager {
         let contextView: DocumentView | undefined; // view containing context that contains target
         let focused = false;
         let docView = docViewIn;
+        let anchor = docView.Document;
         const options = optionsIn;
         const maxFocusLength = 100; // want to keep focusing until we get to target, but avoid an infinite loop
         for (let i = 0; i < maxFocusLength; i++) {
@@ -306,13 +308,14 @@ export class DocumentManager {
                 await prom;
                 options.didMove = true;
             }
-            const nextFocus = docView._props.focus(docView.Document, options); // focus the view within its container
+            const nextFocus = docView._props.focus(anchor, options); // focus the view within its container
             focused = focused || nextFocus !== undefined; // keep track of whether focusing on a view needed to actually change anything
             // eslint-disable-next-line no-await-in-loop
             const { childDocView, viewSpec } = await iterator(docView);
             if (!childDocView) return { viewSpec: viewSpec ?? docView.Document, docView, contextView, focused };
             contextView = !childDocView.Document.layout_unrendered ? childDocView : docView;
             docView = childDocView;
+            anchor = viewSpec ?? docView.Document;
         }
         return undefined;
     };
diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx
index fd0b4e83f..bbd902a96 100644
--- a/src/client/views/DocComponent.tsx
+++ b/src/client/views/DocComponent.tsx
@@ -68,7 +68,6 @@ export interface ViewBoxInterface {
     removeDocument?: (doc: Doc | Doc[], annotationKey?: string, leavePushpin?: boolean, dontAddToRemoved?: boolean) => boolean; // add a document (used only by collections)
     select?: (ctrlKey: boolean, shiftKey: boolean) => void;
     focus?: (textAnchor: Doc, options: FocusViewOptions) => Opt<number>;
-    browseTo?: (clientX: number, clientY: number, browseTransitionTime: number) => boolean; // when in explore mode, this allows a component view to display itself in an easy to read way (ie, a document in a freeformview will zoom itself to mostly full screen)
     viewTransition?: () => Opt<string>; // duration of a view transition animation
     isAnyChildContentActive?: () => boolean; // is any child content of the document active
     onClickScriptDisable?: () => 'never' | 'always'; // disable click scripts : never, always, or undefined = only when selected
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index d3998aee8..8a859a3fd 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -309,8 +309,23 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         return dispTime === -1 || curTime === -1 || (curTime - dispTime >= -1e-4 && curTime <= endTime);
     }
 
+    focusOnPoint = (options: FocusViewOptions) => {
+        const {pointFocus, zoomTime, didMove} = options;
+        if (!this.Document.isGroup && pointFocus && !didMove) {
+            const dfltScale = this.isAnnotationOverlay ? 1 : 0.5;
+            if (this.layoutDoc[this.scaleFieldKey] !== dfltScale) {
+                this.zoomSmoothlyAboutPt(this.screenToFreeformContentsXf.transformPoint(pointFocus.X, pointFocus.Y), dfltScale, zoomTime);
+                options.didMove = true;
+                return zoomTime;
+            }
+        }
+        return undefined;
+    };
+
     groupFocus = (anchor: Doc, options: FocusViewOptions) => {
-        options.docTransform = new Transform(-NumCast(this.layoutDoc[this.panXFieldKey]) + NumCast(anchor.x), -NumCast(this.layoutDoc[this.panYFieldKey]) + NumCast(anchor.y), 1);
+        if (options.pointFocus) return this.focusOnPoint(options);
+        options.docTransform = new Transform(NumCast(anchor.x) + NumCast(anchor._width)/2 - NumCast(this.layoutDoc[this.panXFieldKey]),
+                                             NumCast(anchor.y) + NumCast(anchor._height)/2- NumCast(this.layoutDoc[this.panYFieldKey]), 1); // prettier-ignore
         const res = this._props.focus(this.Document, options);
         options.docTransform = undefined;
         return res;
@@ -318,6 +333,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
 
     focus = (anchor: Doc, options: FocusViewOptions) => {
         if (this._lightboxDoc) return undefined;
+        if (options.pointFocus) return this.focusOnPoint(options);
         if (anchor === this.Document) {
             // if (options.willZoomCentered && options.zoomScale) {
             //     this.fitContentOnce();
@@ -1980,19 +1996,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
             </div>
         );
     }
-    browseTo = (clientX: number, clientY: number, browseTransitionTime: number): boolean => {
-        if (!this.Document.isGroup) {
-            const dfltScale = this.isAnnotationOverlay ? 1 : 0.5;
-            if (this.layoutDoc[this.scaleFieldKey] !== dfltScale) {
-                this.zoomSmoothlyAboutPt(this.screenToFreeformContentsXf.transformPoint(clientX, clientY), dfltScale, browseTransitionTime);
-                return true;
-            }
-            return !!this.DocumentView?.()
-                .containerViewPath?.()
-                ?.some(cont => cont.ComponentView?.browseTo?.(clientX, clientY, browseTransitionTime));
-        }
-        return false;
-    };
 }
 
 @observer
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 555b4ba92..626e3d899 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -50,7 +50,7 @@ import { CollectionFreeFormDocumentView } from './CollectionFreeFormDocumentView
 import { DocumentContentsView, ObserverJsxParser } from './DocumentContentsView';
 import { DocumentLinksButton } from './DocumentLinksButton';
 import './DocumentView.scss';
-import { FieldViewProps, FieldViewSharedProps } from './FieldView';
+import { FieldViewProps, FieldViewSharedProps, FocusViewOptions } from './FieldView';
 import { KeyValueBox } from './KeyValueBox';
 import { LinkAnchorBox } from './LinkAnchorBox';
 import { FormattedTextBox } from './formattedText/FormattedTextBox';
@@ -314,11 +314,12 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
         const browseTransitionTime = 500;
         SelectionManager.DeselectAll();
         DocumentManager.Instance.showDocument(this.Document, { zoomScale: 0.8, willZoomCentered: true }, (focused: boolean) => {
+            const options:FocusViewOptions = { pointFocus: {X: e.clientX, Y :e.clientY}, zoomTime: browseTransitionTime};
             if (!focused && this._docView) {
                 this._docView
                     .docViewPath()
                     .reverse()
-                    .find(cont => cont.ComponentView?.browseTo?.(e.clientX, e.clientY, browseTransitionTime));
+                    .forEach(cont => cont.ComponentView?.focus?.(cont.Document, options));
                 Doc.linkFollowHighlight(this.Document, false);
             }
         });
diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx
index c2f946a88..52dc632ae 100644
--- a/src/client/views/nodes/FieldView.tsx
+++ b/src/client/views/nodes/FieldView.tsx
@@ -22,7 +22,7 @@ export interface FocusViewOptions {
     docTransform?: Transform; // when a document can't be panned and zoomed within its own container (say a group), then we need to continue to move up the render hierarchy to find something that can pan and zoom.  when this happens the docTransform must accumulate all the transforms of each level of the hierarchy
     instant?: boolean; // whether focus should happen instantly (as opposed to smooth zoom)
     preview?: boolean; // whether changes should be previewed by the componentView or written to the document
-    effect?: Doc; // animation effect for focus
+    effect?: Doc; // animation effect for focus // bcz: needs to be changed to something more generic than a Doc
     noSelect?: boolean; // whether target should be selected after focusing
     playAudio?: boolean; // whether to play audio annotation on focus
     playMedia?: boolean; // whether to play start target videos
@@ -30,6 +30,7 @@ export interface FocusViewOptions {
     zoomTextSelections?: boolean; // whether to display a zoomed overlay of anchor text selections
     toggleTarget?: boolean; // whether to toggle target on and off
     easeFunc?: 'linear' | 'ease'; // transition method for scrolling
+    pointFocus?: {X:number, Y: number }; // clientX and clientY coordinates to focus on instead of a document target (used by explore mode)
 }
 export type FocusFuncType = (doc: Doc, options: FocusViewOptions) => Opt<number>;
 // eslint-disable-next-line no-use-before-define
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 617c45715..db28d0a48 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -414,7 +414,6 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl
         TraceMobx();
         return <div className="imageBox-annotationLayer" style={{ height: this._props.PanelHeight() }} ref={this._annotationLayer} />;
     }
-    browseTo = (clientX: number, clientY: number, browseTransitionTime: number) => !!this._ffref.current?.browseTo?.(clientX, clientY, browseTransitionTime);
     screenToLocalTransform = () => this.ScreenToLocalBoxXf().translate(0, NumCast(this.layoutDoc._layout_scrollTop) * this.ScreenToLocalBoxXf().Scale);
     marqueeDown = (e: React.PointerEvent) => {
         if (!this.dataDoc[this.fieldKey]) {
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index fd7997248..b8ec0081a 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -979,7 +979,6 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl
         this._props.bringToFront?.(cropping);
         return cropping;
     };
-    browseTo = (clientX: number, clientY: number, browseTransitionTime: number) => !!this._ffref.current?.browseTo(clientX, clientY, browseTransitionTime);
     focus = (anchor: Doc, options: FocusViewOptions) => (anchor.type === DocumentType.CONFIG ? undefined : this._ffref.current?.focus(anchor, options));
     savedAnnotations = () => this._savedAnnotations;
     render() {
-- 
cgit v1.2.3-70-g09d2


From 776c9cd88fc0799426ced87f36cb215dfdc1854b Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Tue, 30 Apr 2024 17:59:40 -0400
Subject: unwinding some import cycles.  added PinFuncs and
 .from(dv:DocumentView) for CollectionFreeForm stiuff

---
 package-lock.json                                  |   1 +
 package.json                                       |   1 +
 src/client/util/SnappingManager.ts                 |   3 +
 src/client/views/DocComponent.tsx                  |  34 +----
 src/client/views/DocumentDecorations.tsx           |  11 +-
 src/client/views/InkingStroke.tsx                  |   6 +-
 src/client/views/MainView.tsx                      |   3 +-
 src/client/views/PinFuncs.ts                       | 139 +++++++++++++++++++++
 .../views/collections/CollectionStackingView.tsx   |   1 +
 .../views/collections/CollectionTimeView.tsx       |   6 +-
 src/client/views/collections/TabDocView.tsx        |   4 +-
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  20 +--
 src/client/views/nodes/AudioBox.tsx                |   6 +-
 .../views/nodes/CollectionFreeFormDocumentView.tsx |   5 +
 src/client/views/nodes/ComparisonBox.tsx           |   6 +-
 .../nodes/DataVizBox/components/Histogram.tsx      |   5 +-
 .../nodes/DataVizBox/components/LineChart.tsx      |   6 +-
 .../views/nodes/DataVizBox/components/PieChart.tsx |   5 +-
 src/client/views/nodes/DocumentView.tsx            |  18 +--
 src/client/views/nodes/FunctionPlotBox.tsx         |   6 +-
 src/client/views/nodes/ImageBox.tsx                |   8 +-
 src/client/views/nodes/LabelBox.tsx                |   6 +-
 src/client/views/nodes/MapBox/AnimationUtility.ts  |   1 -
 src/client/views/nodes/MapBox/MapBox.tsx           |   7 +-
 .../views/nodes/MapboxMapBox/MapboxContainer.tsx   |   6 +-
 src/client/views/nodes/PDFBox.tsx                  |   8 +-
 src/client/views/nodes/VideoBox.tsx                |   6 +-
 src/client/views/nodes/WebBox.tsx                  |   6 +-
 .../views/nodes/formattedText/FormattedTextBox.tsx |   6 +-
 src/client/views/nodes/trails/PresBox.tsx          | 121 ++----------------
 src/client/views/nodes/trails/PresElementBox.tsx   |   3 +-
 src/server/server_Initialization.ts                |   1 -
 32 files changed, 236 insertions(+), 229 deletions(-)
 create mode 100644 src/client/views/PinFuncs.ts

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

diff --git a/package-lock.json b/package-lock.json
index 80aef5232..c53a3219e 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -82,6 +82,7 @@
                 "core-js": "^3.33.3",
                 "cors": "^2.8.5",
                 "css-loader": "^7.0.0",
+                "csstype": "^3.1.3",
                 "csv-parser": "^3.0.0",
                 "csv-stringify": "^6.4.4",
                 "D": "^1.0.0",
diff --git a/package.json b/package.json
index d826c855e..0e7955d54 100644
--- a/package.json
+++ b/package.json
@@ -167,6 +167,7 @@
         "core-js": "^3.33.3",
         "cors": "^2.8.5",
         "css-loader": "^7.0.0",
+        "csstype": "^3.1.3",
         "csv-parser": "^3.0.0",
         "csv-stringify": "^6.4.4",
         "D": "^1.0.0",
diff --git a/src/client/util/SnappingManager.ts b/src/client/util/SnappingManager.ts
index 3da85191f..5cd6ecfe1 100644
--- a/src/client/util/SnappingManager.ts
+++ b/src/client/util/SnappingManager.ts
@@ -18,6 +18,7 @@ export class SnappingManager {
     @observable _horizSnapLines: number[] = [];
     @observable _vertSnapLines: number[] = [];
     @observable _exploreMode = false;
+    @observable _userPanned = false;
 
     private constructor() {
         SnappingManager._manager = this;
@@ -43,6 +44,7 @@ export class SnappingManager {
     public static get IsResizing()     { return this.Instance._isResizing; } // prettier-ignore
     public static get CanEmbed()       { return this.Instance._canEmbed; } // prettier-ignore
     public static get ExploreMode()    { return this.Instance._exploreMode; } // prettier-ignore
+    public static get UserPanned()     { return this.Instance._userPanned; } // prettier-ignore
     public static SetShiftKey       = (down: boolean) => runInAction(() => {this.Instance._shiftKey = down}); // prettier-ignore
     public static SetCtrlKey        = (down: boolean) => runInAction(() => {this.Instance._ctrlKey = down}); // prettier-ignore
     public static SetMetaKey        = (down: boolean) => runInAction(() => {this.Instance._metaKey = down}); // prettier-ignore
@@ -52,6 +54,7 @@ export class SnappingManager {
     public static SetIsResizing     = (docid?:string) => runInAction(() => {this.Instance._isResizing = docid}); // prettier-ignore
     public static SetCanEmbed       = (embed:boolean) => runInAction(() => {this.Instance._canEmbed = embed}); // prettier-ignore
     public static SetExploreMode    = (state:boolean) => runInAction(() => {this.Instance._exploreMode = state}); // prettier-ignore
+    public static TriggerUserPanned = () => runInAction(() => {this.Instance._userPanned = !this.Instance._userPanned}); // prettier-ignore
 
     public static userColor: string | undefined;
     public static userVariantColor: string | undefined;
diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx
index bbd902a96..391c2f694 100644
--- a/src/client/views/DocComponent.tsx
+++ b/src/client/views/DocComponent.tsx
@@ -11,43 +11,11 @@ import { GetEffectiveAcl, inheritParentAcls } from '../../fields/util';
 import { DocumentType } from '../documents/DocumentTypes';
 import { DragManager } from '../util/DragManager';
 import { ObservableReactComponent } from './ObservableReactComponent';
+import { PinProps } from './PinFuncs';
 import { DocumentView, OpenWhere } from './nodes/DocumentView';
 import { FieldViewProps, FocusViewOptions } from './nodes/FieldView';
 // import { DocUtils } from '../documents/Documents';
 
-export interface pinDataTypes {
-    scrollable?: boolean;
-    dataviz?: number[];
-    pannable?: boolean;
-    type_collection?: boolean;
-    inkable?: boolean;
-    filters?: boolean;
-    pivot?: boolean;
-    temporal?: boolean;
-    clippable?: boolean;
-    datarange?: boolean;
-    dataview?: boolean;
-    poslayoutview?: boolean;
-    dataannos?: boolean;
-    map?: boolean;
-}
-
-export interface MarqueeViewBounds {
-    left: number;
-    top: number;
-    width: number;
-    height: number;
-}
-export interface PinProps {
-    audioRange?: boolean;
-    activeFrame?: number;
-    currentFrame?: number;
-    hidePresBox?: boolean;
-    pinViewport?: MarqueeViewBounds; // pin a specific viewport on a freeform view (use MarqueeView.CurViewBounds to compute if no region has been selected)
-    pinDocLayout?: boolean; // pin layout info (width/height/x/y)
-    pinAudioPlay?: boolean; // pin audio annotation
-    pinData?: pinDataTypes;
-}
 /**
  * Shared interface among all viewBox'es (ie, react classes that render the contents of a Doc)
  * Many of these methods only make sense for specific viewBox'es, but they should be written to
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 3083b9be0..1e6eb1aeb 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -6,10 +6,11 @@ import { observer } from 'mobx-react';
 import * as React from 'react';
 import { FaUndo } from 'react-icons/fa';
 import { lightOrDark, returnFalse, setupMoveUpEvents } from '../../ClientUtils';
-import { Utils, numberValue, emptyFunction } from '../../Utils';
+import { Utils, emptyFunction, numberValue } from '../../Utils';
 import { DateField } from '../../fields/DateField';
 import { Doc, DocListCast, Field, FieldType, HierarchyMapping, ReverseHierarchyMap } from '../../fields/Doc';
 import { AclAdmin, AclAugment, AclEdit, DocData } from '../../fields/DocSymbols';
+import { Id } from '../../fields/FieldSymbols';
 import { InkField } from '../../fields/InkField';
 import { ScriptField } from '../../fields/ScriptField';
 import { BoolCast, Cast, DocCast, NumCast, StrCast } from '../../fields/Types';
@@ -31,11 +32,11 @@ import { ObservableReactComponent } from './ObservableReactComponent';
 import { CollectionDockingView } from './collections/CollectionDockingView';
 import { CollectionFreeFormView } from './collections/collectionFreeForm';
 import { Colors } from './global/globalEnums';
+import { CollectionFreeFormDocumentView } from './nodes/CollectionFreeFormDocumentView';
 import { DocumentView, OpenWhereMod } from './nodes/DocumentView';
 import { ImageBox } from './nodes/ImageBox';
 import { KeyValueBox } from './nodes/KeyValueBox';
 import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox';
-import { Id } from '../../fields/FieldSymbols';
 
 interface DocumentDecorationsProps {
     PanelWidth: number;
@@ -447,7 +448,7 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
         };
         this._resizeUndo = UndoManager.StartBatch('drag resizing');
         this._snapPt = { x: e.pageX, y: e.pageY };
-        SelectionManager.Views.forEach(docView => docView.CollectionFreeFormView?.dragStarting(false, false));
+        SelectionManager.Views.forEach(docView => CollectionFreeFormDocumentView.from(docView)?.CollectionFreeFormView?.dragStarting(false, false));
     };
 
     projectDragToAspect = (e: PointerEvent, docView: DocumentView, fixedAspect: number) => {
@@ -700,7 +701,7 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
 
         const bounds = this.ClippedBounds;
         const useLock = bounds.r - bounds.x > 135;
-        const useRotation = !hideResizers && seldocview.Document.type !== DocumentType.EQUATION && seldocview.CollectionFreeFormDocumentView; // when do we want an object to not rotate?
+        const useRotation = !hideResizers && seldocview.Document.type !== DocumentType.EQUATION && CollectionFreeFormDocumentView.from(seldocview); // when do we want an object to not rotate?
         const rotation = SelectionManager.Views.length === 1 ? seldocview.screenToContentsTransform().inverse().RotateDeg : 0;
 
         // Radius constants
@@ -771,7 +772,7 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
         );
         const centery = hideTitle ? 0 : this._titleHeight;
         const transformOrigin = `${50}% calc(50% + ${centery / 2}px)`;
-        const freeformDoc = SelectionManager.Views.some(v => v.CollectionFreeFormDocumentView);
+        const freeformDoc = SelectionManager.Views.some(v => CollectionFreeFormDocumentView.from(v));
         return (
             <div className="documentDecorations" style={{ display: this._showNothing && !freeformDoc ? 'none' : undefined }}>
                 <div
diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx
index 35067047b..cf609d8f9 100644
--- a/src/client/views/InkingStroke.tsx
+++ b/src/client/views/InkingStroke.tsx
@@ -34,7 +34,7 @@ import { InteractionUtils } from '../util/InteractionUtils';
 import { SnappingManager } from '../util/SnappingManager';
 import { UndoManager } from '../util/UndoManager';
 import { ContextMenu } from './ContextMenu';
-import { PinProps, ViewBoxAnnotatableComponent, ViewBoxInterface } from './DocComponent';
+import { ViewBoxAnnotatableComponent, ViewBoxInterface } from './DocComponent';
 import { Colors } from './global/globalEnums';
 import { InkControlPtHandles, InkEndPtHandles } from './InkControlPtHandles';
 import './InkStroke.scss';
@@ -42,7 +42,7 @@ import { InkStrokeProperties } from './InkStrokeProperties';
 import { InkTangentHandles } from './InkTangentHandles';
 import { FieldView, FieldViewProps } from './nodes/FieldView';
 import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox';
-import { PresBox } from './nodes/trails';
+import { PinDocView, PinProps } from './PinFuncs';
 import { StyleProp } from './StyleProvider';
 
 const { INK_MASK_SIZE } = require('./global/globalCssVariables.module.scss'); // prettier-ignore
@@ -92,7 +92,7 @@ export class InkingStroke extends ViewBoxAnnotatableComponent<FieldViewProps>()
         if (anchor) {
             anchor.backgroundColor = 'transparent';
             addAsAnnotation && this.addDocument(anchor);
-            PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), inkable: true } }, this.Document);
+            PinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), inkable: true } }, this.Document);
             return anchor;
         }
         return this.Document;
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index be13b14f5..d3dd51ac6 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -76,6 +76,7 @@ import { PresBox } from './nodes/trails';
 import { AnchorMenu } from './pdf/AnchorMenu';
 import { GPTPopup } from './pdf/GPTPopup/GPTPopup';
 import { TopBar } from './topbar/TopBar';
+import { CollectionFreeFormView } from './collections/collectionFreeForm';
 
 const _global = (window /* browser */ || global) /* node */ as any;
 const { LEFT_MENU_WIDTH, TOPBAR_HEIGHT } = require('./global/globalCssVariables.module.scss'); // prettier-ignore
@@ -964,7 +965,7 @@ export class MainView extends ObservableReactComponent<{}> {
     }
     @computed get snapLines() {
         const dragged = DragManager.docsBeingDragged.lastElement() ?? SelectionManager.Docs.lastElement();
-        const dragPar = dragged ? DocumentManager.Instance.getDocumentView(dragged)?.CollectionFreeFormView : undefined;
+        const dragPar = dragged ? CollectionFreeFormView.from(DocumentManager.Instance.getDocumentView(dragged)) : undefined;
         return !dragPar?.layoutDoc.freeform_snapLines ? null : (
             <div className="mainView-snapLines">
                 <svg style={{ width: '100%', height: '100%' }}>
diff --git a/src/client/views/PinFuncs.ts b/src/client/views/PinFuncs.ts
new file mode 100644
index 000000000..3d998ecaf
--- /dev/null
+++ b/src/client/views/PinFuncs.ts
@@ -0,0 +1,139 @@
+import { Doc, DocListCast } from '../../fields/Doc';
+import { DocData } from '../../fields/DocSymbols';
+import { Copy, Id } from '../../fields/FieldSymbols';
+import { List } from '../../fields/List';
+import { ObjectField } from '../../fields/ObjectField';
+import { NumCast, StrCast } from '../../fields/Types';
+import { SerializationHelper } from '../util/SerializationHelper';
+
+export interface MarqueeViewBounds {
+    left: number;
+    top: number;
+    width: number;
+    height: number;
+}
+export interface pinDataTypes {
+    scrollable?: boolean;
+    dataviz?: number[];
+    pannable?: boolean;
+    type_collection?: boolean;
+    inkable?: boolean;
+    filters?: boolean;
+    pivot?: boolean;
+    temporal?: boolean;
+    clippable?: boolean;
+    datarange?: boolean;
+    dataview?: boolean;
+    poslayoutview?: boolean;
+    dataannos?: boolean;
+    map?: boolean;
+}
+export interface PinProps {
+    audioRange?: boolean;
+    activeFrame?: number;
+    currentFrame?: number;
+    hidePresBox?: boolean;
+    pinViewport?: MarqueeViewBounds; // pin a specific viewport on a freeform view (use MarqueeView.CurViewBounds to compute if no region has been selected)
+    pinDocLayout?: boolean; // pin layout info (width/height/x/y)
+    pinAudioPlay?: boolean; // pin audio annotation
+    pinData?: pinDataTypes;
+}
+
+/// copies values from the targetDoc (which is the prototype of the pinDoc) to
+/// reserved fields on the pinDoc so that those values can be restored to the
+/// target doc when navigating to it.
+export function PinDocView(pinDocIn: Doc, pinProps: PinProps, targetDoc: Doc) {
+    const pinDoc = pinDocIn;
+    pinDoc.presentation = true;
+    pinDoc.config = '';
+    if (pinProps.pinDocLayout) {
+        pinDoc.config_pinLayout = true;
+        pinDoc.config_x = NumCast(targetDoc.x);
+        pinDoc.config_y = NumCast(targetDoc.y);
+        pinDoc.config_rotation = NumCast(targetDoc.rotation);
+        pinDoc.config_width = NumCast(targetDoc.width);
+        pinDoc.config_height = NumCast(targetDoc.height);
+    }
+    if (pinProps.pinAudioPlay) pinDoc.presPlayAudio = true;
+    if (pinProps.pinData) {
+        pinDoc.config_pinData =
+            pinProps.pinData.scrollable ||
+            pinProps.pinData.temporal ||
+            pinProps.pinData.pannable ||
+            pinProps.pinData.type_collection ||
+            pinProps.pinData.clippable ||
+            pinProps.pinData.datarange ||
+            pinProps.pinData.dataview ||
+            pinProps.pinData.poslayoutview ||
+            pinProps?.activeFrame !== undefined;
+        const fkey = Doc.LayoutFieldKey(targetDoc);
+        if (pinProps.pinData.dataview) {
+            pinDoc.config_usePath = targetDoc[fkey + '_usePath'];
+            pinDoc.config_data = targetDoc[fkey] instanceof ObjectField ? (targetDoc[fkey] as ObjectField)[Copy]() : targetDoc.data;
+        }
+        if (pinProps.pinData.dataannos) {
+            const fieldKey = Doc.LayoutFieldKey(targetDoc);
+            pinDoc.config_annotations = new List<Doc>(DocListCast(targetDoc[DocData][fieldKey + '_annotations']).filter(doc => !doc.layout_unrendered));
+        }
+        if (pinProps.pinData.inkable) {
+            pinDoc.config_fillColor = targetDoc.fillColor;
+            pinDoc.config_color = targetDoc.color;
+            pinDoc.config_width = targetDoc._width;
+            pinDoc.config_height = targetDoc._height;
+        }
+        if (pinProps.pinData.scrollable) pinDoc.config_scrollTop = targetDoc._layout_scrollTop;
+        if (pinProps.pinData.clippable) {
+            const fieldKey = Doc.LayoutFieldKey(targetDoc);
+            pinDoc.config_clipWidth = targetDoc[fieldKey + '_clipWidth'];
+        }
+        if (pinProps.pinData.datarange) {
+            pinDoc.config_xRange = undefined; // targetDoc?.xrange;
+            pinDoc.config_yRange = undefined; // targetDoc?.yrange;
+        }
+        if (pinProps.pinData.map) {
+            // pinDoc.config_latitude = targetDoc?.latitude;
+            // pinDoc.config_longitude = targetDoc?.longitude;
+            pinDoc.config_map_zoom = targetDoc?.map_zoom;
+            pinDoc.config_map_type = targetDoc?.map_type;
+            // ...
+        }
+        if (pinProps.pinData.poslayoutview)
+            pinDoc.config_pinLayoutData = new List<string>(
+                DocListCast(targetDoc[fkey] as ObjectField).map(d =>
+                    JSON.stringify({
+                        id: d[Id],
+                        x: NumCast(d.x),
+                        y: NumCast(d.y),
+                        w: NumCast(d._width),
+                        h: NumCast(d._height),
+                        fill: StrCast(d._fillColor),
+                        back: StrCast(d._backgroundColor),
+                        data: SerializationHelper.Serialize(d.data instanceof ObjectField ? d.data[Copy]() : ''),
+                        text: SerializationHelper.Serialize(d.text instanceof ObjectField ? d.text[Copy]() : ''),
+                    })
+                )
+            );
+        if (pinProps.pinData.type_collection) pinDoc.config_viewType = targetDoc._type_collection;
+        if (pinProps.pinData.filters) pinDoc.config_docFilters = ObjectField.MakeCopy(targetDoc.childFilters as ObjectField);
+        if (pinProps.pinData.pivot) pinDoc.config_pivotField = targetDoc._pivotField;
+        if (pinProps.pinData.pannable) {
+            pinDoc.config_panX = NumCast(targetDoc._freeform_panX);
+            pinDoc.config_panY = NumCast(targetDoc._freeform_panY);
+            pinDoc.config_viewScale = NumCast(targetDoc._freeform_scale, 1);
+        }
+        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(pinDoc.config_clipStart) + NumCast(targetDoc.clipEnd, duration);
+        }
+    }
+    if (pinProps?.pinViewport) {
+        // If pinWithView option set then update scale and x / y props of slide
+        const bounds = pinProps.pinViewport;
+        pinDoc.config_pinView = true;
+        pinDoc.config_viewScale = NumCast(targetDoc._freeform_scale, 1);
+        pinDoc.config_panX = bounds.left + bounds.width / 2;
+        pinDoc.config_panY = bounds.top + bounds.height / 2;
+        pinDoc.config_viewBounds = new List<number>([bounds.left, bounds.top, bounds.left + bounds.width, bounds.top + bounds.height]);
+    }
+}
diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx
index c47fe915a..ad9960989 100644
--- a/src/client/views/collections/CollectionStackingView.tsx
+++ b/src/client/views/collections/CollectionStackingView.tsx
@@ -1,5 +1,6 @@
 /* eslint-disable react/jsx-props-no-spreading */
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+// eslint-disable-next-line import/no-extraneous-dependencies
 import * as CSS from 'csstype';
 import { action, computed, IReactionDisposer, makeObservable, observable, ObservableMap, reaction, runInAction } from 'mobx';
 import { observer } from 'mobx-react';
diff --git a/src/client/views/collections/CollectionTimeView.tsx b/src/client/views/collections/CollectionTimeView.tsx
index e1d2e3c40..b3760d4af 100644
--- a/src/client/views/collections/CollectionTimeView.tsx
+++ b/src/client/views/collections/CollectionTimeView.tsx
@@ -3,8 +3,8 @@
 import { action, computed, makeObservable, observable, runInAction } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
-import { emptyFunction } from '../../../Utils';
 import { returnFalse, returnTrue, setupMoveUpEvents } from '../../../ClientUtils';
+import { emptyFunction } from '../../../Utils';
 import { Doc, Opt, StrListCast } from '../../../fields/Doc';
 import { List } from '../../../fields/List';
 import { ObjectField } from '../../../fields/ObjectField';
@@ -17,8 +17,8 @@ import { ScriptingGlobals } from '../../util/ScriptingGlobals';
 import { ContextMenu } from '../ContextMenu';
 import { ContextMenuProps } from '../ContextMenuItem';
 import { FieldsDropdown } from '../FieldsDropdown';
+import { PinDocView } from '../PinFuncs';
 import { DocumentView } from '../nodes/DocumentView';
-import { PresBox } from '../nodes/trails';
 import { CollectionSubView } from './CollectionSubView';
 import './CollectionTimeView.scss';
 import { ViewDefBounds, computePivotLayout, computeTimelineLayout } from './collectionFreeForm/CollectionFreeFormLayoutEngines';
@@ -55,7 +55,7 @@ export class CollectionTimeView extends CollectionSubView() {
             title: ComputedField.MakeFunction(`"${this.pivotField}"])`) as any,
             annotationOn: this.Document,
         });
-        PresBox.pinDocView(anchor, { pinData: { type_collection: true, pivot: true, filters: true } }, this.Document);
+        PinDocView(anchor, { pinData: { type_collection: true, pivot: true, filters: true } }, this.Document);
 
         if (addAsAnnotation) {
             // when added as an annotation, links to anchors can be found as links to the document even if the anchors are not rendered
diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx
index e98e444ed..e7b8237a5 100644
--- a/src/client/views/collections/TabDocView.tsx
+++ b/src/client/views/collections/TabDocView.tsx
@@ -26,9 +26,9 @@ import { SnappingManager } from '../../util/SnappingManager';
 import { Transform } from '../../util/Transform';
 import { UndoManager, undoable } from '../../util/UndoManager';
 import { DashboardView } from '../DashboardView';
-import { PinProps } from '../DocComponent';
 import { LightboxView } from '../LightboxView';
 import { ObservableReactComponent } from '../ObservableReactComponent';
+import { PinProps, PinDocView } from '../PinFuncs';
 import { DefaultStyleProvider, StyleProp } from '../StyleProvider';
 import { Colors } from '../global/globalEnums';
 import { DocumentView, OpenWhere, OpenWhereMod, returnEmptyDocViewList } from '../nodes/DocumentView';
@@ -426,7 +426,7 @@ export class TabDocView extends ObservableReactComponent<TabDocViewProps> {
             pinDoc.treeView_HideHeaderIfTemplate = true; // this will force the document to render itself as the tree view header
             const duration = NumCast(doc[`${Doc.LayoutFieldKey(pinDoc)}_duration`], null);
 
-            if (pinProps.pinViewport) PresBox.pinDocView(pinDoc, pinProps, anchorDoc ?? doc);
+            if (pinProps.pinViewport) PinDocView(pinDoc, pinProps, anchorDoc ?? doc);
             if (!pinProps?.audioRange && duration !== undefined) {
                 pinDoc.presentation_mediaStart = 'manual';
                 pinDoc.presentation_mediaStop = 'manual';
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 69cb52233..653a01a04 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -38,7 +38,6 @@ import { Transform } from '../../../util/Transform';
 import { undoable, undoBatch, UndoManager } from '../../../util/UndoManager';
 import { Timeline } from '../../animationtimeline/Timeline';
 import { ContextMenu } from '../../ContextMenu';
-import { PinProps } from '../../DocComponent';
 import { GestureOverlay } from '../../GestureOverlay';
 import { ActiveInkWidth, InkingStroke, SetActiveInkColor, SetActiveInkWidth } from '../../InkingStroke';
 import { LightboxView } from '../../LightboxView';
@@ -47,13 +46,13 @@ import { SchemaCSVPopUp } from '../../nodes/DataVizBox/SchemaCSVPopUp';
 import { DocumentView, OpenWhere } from '../../nodes/DocumentView';
 import { FieldViewProps, FocusViewOptions } from '../../nodes/FieldView';
 import { FormattedTextBox } from '../../nodes/formattedText/FormattedTextBox';
-import { PresBox } from '../../nodes/trails/PresBox';
+import { PinDocView, PinProps } from '../../PinFuncs';
 import { StyleProp } from '../../StyleProvider';
 import { CollectionSubView } from '../CollectionSubView';
 import { TreeViewType } from '../CollectionTreeView';
 import { CollectionFreeFormBackgroundGrid } from './CollectionFreeFormBackgroundGrid';
-import { CollectionFreeFormInfoUI } from './CollectionFreeFormInfoUI';
 import { CollectionFreeFormClusters } from './CollectionFreeFormClusters';
+import { CollectionFreeFormInfoUI } from './CollectionFreeFormInfoUI';
 import { computePassLayout, computePivotLayout, computeStarburstLayout, computeTimelineLayout, PoolData, ViewDefBounds, ViewDefResult } from './CollectionFreeFormLayoutEngines';
 import { CollectionFreeFormPannableContents } from './CollectionFreeFormPannableContents';
 import { CollectionFreeFormRemoteCursors } from './CollectionFreeFormRemoteCursors';
@@ -85,6 +84,9 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     } // this makes mobx trace() statements more descriptive
     public unprocessedDocs: Doc[] = [];
     public static collectionsWithUnprocessedInk = new Set<CollectionFreeFormView>();
+    public static from(dv?: DocumentView) {
+        return CollectionFreeFormDocumentView.from(dv)?.CollectionFreeFormView;
+    }
 
     _oldWheel: any;
     _clusters = new CollectionFreeFormClusters(this);
@@ -592,7 +594,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     };
 
     scrollPan = (e: WheelEvent | { deltaX: number; deltaY: number }): void => {
-        PresBox.Instance?.pauseAutoPres();
+        SnappingManager.TriggerUserPanned();
         this.setPan(NumCast(this.Document[this.panXFieldKey]) - e.deltaX, NumCast(this.Document[this.panYFieldKey]) - e.deltaY, 0, true);
     };
 
@@ -600,7 +602,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     pan = (e: PointerEvent): void => {
         const ctrlKey = e.ctrlKey && !e.shiftKey;
         const shiftKey = e.shiftKey && !e.ctrlKey;
-        PresBox.Instance?.pauseAutoPres();
+        SnappingManager.TriggerUserPanned();
         this.DocumentView?.().clearViewTransition();
         const [dxi, dyi] = this.screenToFreeformContentsXf.transformDirection(e.clientX - this._lastX, e.clientY - this._lastY);
         const { x: dx, y: dy } = Utils.rotPt(dxi, dyi, this.ScreenToLocalBoxXf().Rotate);
@@ -834,7 +836,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     @action
     onPointerWheel = (e: React.WheelEvent): void => {
         if (this.Document.isGroup || !this.isContentActive()) return; // group style collections neither pan nor zoom
-        PresBox.Instance?.pauseAutoPres();
+        SnappingManager.TriggerUserPanned();
         if (this.layoutDoc._Transform || this.Document.treeView_OutlineMode === TreeViewType.outline) return;
         e.stopPropagation();
         const docHeight = NumCast(this.Document[Doc.LayoutFieldKey(this.Document) + '_nativeHeight'], this.nativeHeight);
@@ -1289,7 +1291,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
             presentation_transition: 500,
             annotationOn: this.Document,
         });
-        PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), pannable: !this.Document.isGroup, type_collection: true, filters: true } }, this.Document);
+        PinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), pannable: !this.Document.isGroup, type_collection: true, filters: true } }, this.Document);
 
         if (addAsAnnotation) {
             if (Cast(this.dataDoc[this._props.fieldKey + '_annotations'], listSpec(Doc), null) !== undefined) {
@@ -1767,11 +1769,11 @@ ScriptingGlobals.add(function pinWithView(pinContent: boolean) {
 });
 // eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function bringToFront() {
-    SelectionManager.Views.forEach(view => view.CollectionFreeFormView?.bringToFront(view.Document));
+    SelectionManager.Views.forEach(view => CollectionFreeFormView.from(view)?.bringToFront(view.Document));
 });
 // eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function sendToBack() {
-    SelectionManager.Views.forEach(view => view.CollectionFreeFormView?.bringToFront(view.Document, true));
+    SelectionManager.Views.forEach(view => CollectionFreeFormView.from(view)?.bringToFront(view.Document, true));
 });
 // eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function datavizFromSchema() {
diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx
index 9251dca6d..1f618135f 100644
--- a/src/client/views/nodes/AudioBox.tsx
+++ b/src/client/views/nodes/AudioBox.tsx
@@ -21,11 +21,11 @@ import { undoBatch } from '../../util/UndoManager';
 import { CollectionStackedTimeline, TrimScope } from '../collections/CollectionStackedTimeline';
 import { ContextMenu } from '../ContextMenu';
 import { ContextMenuProps } from '../ContextMenuItem';
-import { PinProps, ViewBoxAnnotatableComponent } from '../DocComponent';
+import { ViewBoxAnnotatableComponent } from '../DocComponent';
+import { PinDocView, PinProps } from '../PinFuncs';
 import './AudioBox.scss';
 import { OpenWhere } from './DocumentView';
 import { FieldView, FieldViewProps } from './FieldView';
-import { PresBox } from './trails';
 
 /**
  * AudioBox
@@ -159,7 +159,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
               ) || this.Document
             : Docs.Create.ConfigDocument({ title: '#' + timecode, _timecodeToShow: timecode, annotationOn: this.Document });
 
-        PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), temporal: true } }, this.Document);
+        PinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), temporal: true } }, this.Document);
         return anchor;
     };
 
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
index 7b1847ae4..9c4d748bd 100644
--- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
@@ -38,6 +38,7 @@ interface freeFormProps {
     highlight?: boolean;
     transition?: string;
 }
+
 export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps {
     RenderCutoffProvider: (doc: Doc) => boolean;
     CollectionFreeFormView: CollectionFreeFormView;
@@ -63,6 +64,9 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
     ]; // fields that are configured to be animatable using animation frames
     public static animStringFields = ['backgroundColor', 'color', 'fillColor']; // fields that are configured to be animatable using animation frames
     public static animDataFields = (doc: Doc) => (Doc.LayoutFieldKey(doc) ? [Doc.LayoutFieldKey(doc)] : []); // fields that are configured to be animatable using animation frames
+    public static from(dv?: DocumentView) {
+        return dv?._props.parent instanceof CollectionFreeFormDocumentView ? dv._props.parent : undefined;
+    }
 
     constructor(props: CollectionFreeFormDocumentViewProps & freeFormProps) {
         super(props);
@@ -273,6 +277,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
                     <div style={{ position: 'absolute', width: this.PanelWidth(), height: this.PanelHeight(), background: 'lightGreen' }} />
                 ) : (
                     <DocumentView
+                        parent={this}
                         // eslint-disable-next-line react/jsx-props-no-spreading
                         {...OmitKeys(this._props,this.WrapperKeys.map(val => val.lower)).omit} // prettier-ignore
                         DataTransition={this.DataTransition}
diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx
index e6fd447ac..bc151f6a5 100644
--- a/src/client/views/nodes/ComparisonBox.tsx
+++ b/src/client/views/nodes/ComparisonBox.tsx
@@ -11,14 +11,14 @@ import { DocUtils, Docs } from '../../documents/Documents';
 import { DragManager } from '../../util/DragManager';
 import { dropActionType } from '../../util/DropActionTypes';
 import { undoBatch } from '../../util/UndoManager';
-import { PinProps, ViewBoxAnnotatableComponent, ViewBoxInterface } from '../DocComponent';
+import { ViewBoxAnnotatableComponent, ViewBoxInterface } from '../DocComponent';
+import { PinProps, PinDocView } from '../PinFuncs';
 import { StyleProp } from '../StyleProvider';
 import './ComparisonBox.scss';
 import { DocumentView } from './DocumentView';
 import { FieldView, FieldViewProps } from './FieldView';
 import { KeyValueBox } from './KeyValueBox';
 import { FormattedTextBox } from './formattedText/FormattedTextBox';
-import { PresBox } from './trails';
 
 @observer
 export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implements ViewBoxInterface {
@@ -113,7 +113,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
         if (anchor) {
             if (!addAsAnnotation) anchor.backgroundColor = 'transparent';
             /* addAsAnnotation && */ this.addDocument(anchor);
-            PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), clippable: true } }, this.Document);
+            PinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), clippable: true } }, this.Document);
             return anchor;
         }
         return this.Document;
diff --git a/src/client/views/nodes/DataVizBox/components/Histogram.tsx b/src/client/views/nodes/DataVizBox/components/Histogram.tsx
index f0ffdbdcf..79b3e9541 100644
--- a/src/client/views/nodes/DataVizBox/components/Histogram.tsx
+++ b/src/client/views/nodes/DataVizBox/components/Histogram.tsx
@@ -11,9 +11,8 @@ import { listSpec } from '../../../../../fields/Schema';
 import { Cast, DocCast, StrCast } from '../../../../../fields/Types';
 import { Docs } from '../../../../documents/Documents';
 import { undoable } from '../../../../util/UndoManager';
-import { PinProps } from '../../../DocComponent';
 import { ObservableReactComponent } from '../../../ObservableReactComponent';
-import { PresBox } from '../../trails';
+import { PinProps, PinDocView } from '../../../PinFuncs';
 import { scaleCreatorNumerical, yAxisCreator } from '../utils/D3Utils';
 import './Chart.scss';
 
@@ -117,7 +116,7 @@ export class Histogram extends ObservableReactComponent<HistogramProps> {
         const anchor = Docs.Create.ConfigDocument({
             title: 'histogram doc selection' + this._currSelected,
         });
-        PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: pinProps?.pinData }, this._props.Document);
+        PinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: pinProps?.pinData }, this._props.Document);
         return anchor;
     };
 
diff --git a/src/client/views/nodes/DataVizBox/components/LineChart.tsx b/src/client/views/nodes/DataVizBox/components/LineChart.tsx
index 8105adf1e..1c3134185 100644
--- a/src/client/views/nodes/DataVizBox/components/LineChart.tsx
+++ b/src/client/views/nodes/DataVizBox/components/LineChart.tsx
@@ -10,12 +10,12 @@ import { Cast, DocCast, StrCast } from '../../../../../fields/Types';
 import { Docs } from '../../../../documents/Documents';
 import { DocumentManager } from '../../../../util/DocumentManager';
 import { undoable } from '../../../../util/UndoManager';
+import {} from '../../../DocComponent';
 import { ObservableReactComponent } from '../../../ObservableReactComponent';
-import { PresBox } from '../../trails';
+import { PinProps, PinDocView } from '../../../PinFuncs';
 import { DataVizBox } from '../DataVizBox';
 import { createLineGenerator, drawLine, minMaxRange, scaleCreatorNumerical, xAxisCreator, xGrid, yAxisCreator, yGrid } from '../utils/D3Utils';
 import './Chart.scss';
-import { PinProps } from '../../../DocComponent';
 
 export interface DataPoint {
     x: number;
@@ -177,7 +177,7 @@ export class LineChart extends ObservableReactComponent<LineChartProps> {
             //
             title: 'line doc selection' + (this._currSelected?.x ?? ''),
         });
-        PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: pinProps?.pinData }, this._props.Document);
+        PinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: pinProps?.pinData }, this._props.Document);
         anchor.config_dataVizSelection = this._currSelected ? new List<number>([this._currSelected.x, this._currSelected.y]) : undefined;
         return anchor;
     };
diff --git a/src/client/views/nodes/DataVizBox/components/PieChart.tsx b/src/client/views/nodes/DataVizBox/components/PieChart.tsx
index f57070f92..ffb2f528a 100644
--- a/src/client/views/nodes/DataVizBox/components/PieChart.tsx
+++ b/src/client/views/nodes/DataVizBox/components/PieChart.tsx
@@ -12,9 +12,8 @@ import { Cast, DocCast, StrCast } from '../../../../../fields/Types';
 import { Docs } from '../../../../documents/Documents';
 import { undoable } from '../../../../util/UndoManager';
 import { ObservableReactComponent } from '../../../ObservableReactComponent';
-import { PresBox } from '../../trails';
+import { PinProps, PinDocView } from '../../../PinFuncs';
 import './Chart.scss';
-import { PinProps } from '../../../DocComponent';
 
 export interface PieChartProps {
     Document: Doc;
@@ -109,7 +108,7 @@ export class PieChart extends ObservableReactComponent<PieChartProps> {
             //
             title: 'piechart doc selection' + this._currSelected,
         });
-        PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: pinProps?.pinData }, this._props.Document);
+        PinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: pinProps?.pinData }, this._props.Document);
         return anchor;
     };
 
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 626e3d899..a603de10b 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -22,7 +22,6 @@ import { AudioField } from '../../../fields/URLField';
 import { GetEffectiveAcl, TraceMobx } from '../../../fields/util';
 import { DocServer } from '../../DocServer';
 import { Networking } from '../../Network';
-import { GooglePhotos } from '../../apis/google_docs/GooglePhotosClientUtils';
 import { CollectionViewType, DocumentType } from '../../documents/DocumentTypes';
 import { DocUtils, Docs } from '../../documents/Documents';
 import { DictationManager } from '../../util/DictationManager';
@@ -46,7 +45,6 @@ import { FieldsDropdown } from '../FieldsDropdown';
 import { GestureOverlay } from '../GestureOverlay';
 import { LightboxView } from '../LightboxView';
 import { AudioAnnoState, StyleProp } from '../StyleProvider';
-import { CollectionFreeFormDocumentView } from './CollectionFreeFormDocumentView';
 import { DocumentContentsView, ObserverJsxParser } from './DocumentContentsView';
 import { DocumentLinksButton } from './DocumentLinksButton';
 import './DocumentView.scss';
@@ -108,6 +106,8 @@ export interface DocumentViewProps extends FieldViewSharedProps {
     dragConfig?: (data: DragManager.DocumentDragData) => void;
     dragStarting?: () => void;
     dragEnding?: () => void;
+
+    parent?: any;
 }
 @observer
 export class DocumentViewInternal extends DocComponent<FieldViewProps & DocumentViewProps>() {
@@ -314,7 +314,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
         const browseTransitionTime = 500;
         SelectionManager.DeselectAll();
         DocumentManager.Instance.showDocument(this.Document, { zoomScale: 0.8, willZoomCentered: true }, (focused: boolean) => {
-            const options:FocusViewOptions = { pointFocus: {X: e.clientX, Y :e.clientY}, zoomTime: browseTransitionTime};
+            const options: FocusViewOptions = { pointFocus: { X: e.clientX, Y: e.clientY }, zoomTime: browseTransitionTime };
             if (!focused && this._docView) {
                 this._docView
                     .docViewPath()
@@ -645,12 +645,6 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
                 if (!Doc.noviceMode) {
                     moreItems.push({ description: 'Make View of Metadata Field', event: () => Doc.MakeMetadataFieldTemplate(this.Document, this._props.TemplateDataDocument), icon: 'concierge-bell' });
                     moreItems.push({ description: `${this.Document._chromeHidden ? 'Show' : 'Hide'} Chrome`, event: () => { this.Document._chromeHidden = !this.Document._chromeHidden; }, icon: 'project-diagram' }); // prettier-ignore
-
-                    if (Cast(Doc.GetProto(this.Document).data, listSpec(Doc))) {
-                        moreItems.push({ description: 'Export to Google Photos Album', event: () => GooglePhotos.Export.CollectionToAlbum({ collection: this.Document }).then(console.log), icon: 'caret-square-right' });
-                        moreItems.push({ description: 'Tag Child Images via Google Photos', event: () => GooglePhotos.Query.TagChildImages(this.Document), icon: 'caret-square-right' });
-                        moreItems.push({ description: 'Write Back Link to Album', event: () => GooglePhotos.Transactions.AddTextEnrichment(this.Document), icon: 'caret-square-right' });
-                    }
                     moreItems.push({ description: 'Copy ID', event: () => ClientUtils.CopyText(Doc.globalServerPath(this.Document)), icon: 'fingerprint' });
                 }
             }
@@ -1079,7 +1073,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
 }
 
 @observer
-export class DocumentView extends DocComponent<DocumentViewProps & { CollectionFreeFormDocumentView?: () => CollectionFreeFormDocumentView }>() {
+export class DocumentView extends DocComponent<DocumentViewProps>() {
     public static ROOT_DIV = 'documentView-effectsWrapper';
     public get displayName() {  return 'DocumentView(' + (this.Document?.title??"") + ')';  } // prettier-ignore
     public ContentRef = React.createRef<HTMLDivElement>();
@@ -1100,7 +1094,7 @@ export class DocumentView extends DocComponent<DocumentViewProps & { CollectionF
     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]);
 
-    constructor(props: DocumentViewProps & { CollectionFreeFormDocumentView?: () => CollectionFreeFormDocumentView }) {
+    constructor(props: DocumentViewProps) {
         super(props);
         makeObservable(this);
     }
@@ -1227,8 +1221,6 @@ export class DocumentView extends DocComponent<DocumentViewProps & { CollectionF
      * path of DocumentViews hat contains this DocumentView (does not includes this DocumentView thouhg)
      */
     public get containerViewPath() { return this._props.containerViewPath; } // prettier-ignore
-    public get CollectionFreeFormView() { return this.CollectionFreeFormDocumentView?.CollectionFreeFormView; } // prettier-ignore
-    public get CollectionFreeFormDocumentView() { return this._props.CollectionFreeFormDocumentView?.(); } // prettier-ignore
     public get LocalRotation() { return this._props.LocalRotation?.(); } // prettier-ignore
 
     public clearViewTransition = () => {
diff --git a/src/client/views/nodes/FunctionPlotBox.tsx b/src/client/views/nodes/FunctionPlotBox.tsx
index 180c651fb..31faa7ac3 100644
--- a/src/client/views/nodes/FunctionPlotBox.tsx
+++ b/src/client/views/nodes/FunctionPlotBox.tsx
@@ -11,9 +11,9 @@ import { DocUtils, Docs } from '../../documents/Documents';
 import { DragManager } from '../../util/DragManager';
 import { LinkManager } from '../../util/LinkManager';
 import { undoBatch } from '../../util/UndoManager';
-import { PinProps, ViewBoxAnnotatableComponent } from '../DocComponent';
+import { ViewBoxAnnotatableComponent } from '../DocComponent';
+import { PinProps, PinDocView } from '../PinFuncs';
 import { FieldView, FieldViewProps } from './FieldView';
-import { PresBox } from './trails';
 
 @observer
 export class FunctionPlotBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
@@ -40,7 +40,7 @@ export class FunctionPlotBox extends ViewBoxAnnotatableComponent<FieldViewProps>
     }
     getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => {
         const anchor = Docs.Create.ConfigDocument({ annotationOn: this.Document });
-        PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), datarange: true } }, this.Document);
+        PinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), datarange: true } }, this.Document);
         anchor.config_xRange = new List<number>(Array.from(this._plot.options.xAxis.domain));
         anchor.config_yRange = new List<number>(Array.from(this._plot.options.yAxis.domain));
         if (addAsAnnotation) this.addDocument(anchor);
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 6d3d4be06..8bd5ab03d 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -21,18 +21,18 @@ import { Networking } from '../../Network';
 import { DocumentManager } from '../../util/DocumentManager';
 import { DragManager } from '../../util/DragManager';
 import { undoBatch } from '../../util/UndoManager';
-import { ContextMenu } from '../ContextMenu';
 import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView';
+import { ContextMenu } from '../ContextMenu';
 import { ContextMenuProps } from '../ContextMenuItem';
-import { PinProps, ViewBoxAnnotatableComponent, ViewBoxInterface } from '../DocComponent';
+import { ViewBoxAnnotatableComponent, ViewBoxInterface } from '../DocComponent';
 import { MarqueeAnnotator } from '../MarqueeAnnotator';
 import { OverlayView } from '../OverlayView';
 import { AnchorMenu } from '../pdf/AnchorMenu';
+import { PinDocView, PinProps } from '../PinFuncs';
 import { StyleProp } from '../StyleProvider';
 import { OpenWhere } from './DocumentView';
 import { FieldView, FieldViewProps, FocusViewOptions } from './FieldView';
 import './ImageBox.scss';
-import { PresBox } from './trails';
 
 export class ImageEditorData {
     // eslint-disable-next-line no-use-before-define
@@ -102,7 +102,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl
         if (anchor) {
             if (!addAsAnnotation) anchor.backgroundColor = 'transparent';
             addAsAnnotation && this.addDocument(anchor);
-            PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), pannable: !visibleAnchor } }, this.Document);
+            PinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), pannable: !visibleAnchor } }, this.Document);
             return anchor;
         }
         return this.Document;
diff --git a/src/client/views/nodes/LabelBox.tsx b/src/client/views/nodes/LabelBox.tsx
index 9df928581..89270652c 100644
--- a/src/client/views/nodes/LabelBox.tsx
+++ b/src/client/views/nodes/LabelBox.tsx
@@ -10,12 +10,12 @@ import { DragManager } from '../../util/DragManager';
 import { undoBatch } from '../../util/UndoManager';
 import { ContextMenu } from '../ContextMenu';
 import { ContextMenuProps } from '../ContextMenuItem';
-import { PinProps, ViewBoxBaseComponent } from '../DocComponent';
+import { ViewBoxBaseComponent } from '../DocComponent';
+import { PinProps, PinDocView } from '../PinFuncs';
 import { StyleProp } from '../StyleProvider';
 import { FieldView, FieldViewProps } from './FieldView';
 import BigText from './LabelBigText';
 import './LabelBox.scss';
-import { PresBox } from './trails';
 
 @observer
 export class LabelBox extends ViewBoxBaseComponent<FieldViewProps>() {
@@ -96,7 +96,7 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps>() {
         if (anchor) {
             if (!addAsAnnotation) anchor.backgroundColor = 'transparent';
             //  addAsAnnotation && this.addDocument(anchor);
-            PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}) } }, this.Document);
+            PinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}) } }, this.Document);
             return anchor;
         }
         return anchor;
diff --git a/src/client/views/nodes/MapBox/AnimationUtility.ts b/src/client/views/nodes/MapBox/AnimationUtility.ts
index 3a2679ecf..f4bae66bb 100644
--- a/src/client/views/nodes/MapBox/AnimationUtility.ts
+++ b/src/client/views/nodes/MapBox/AnimationUtility.ts
@@ -1,7 +1,6 @@
 import * as turf from '@turf/turf';
 import { Position } from '@turf/turf';
 import * as d3 from 'd3';
-// eslint-disable-next-line import/no-extraneous-dependencies
 import { Feature, GeoJsonProperties, Geometry } from 'geojson';
 import mapboxgl, { MercatorCoordinate } from 'mapbox-gl';
 import { action, computed, makeObservable, observable, runInAction } from 'mobx';
diff --git a/src/client/views/nodes/MapBox/MapBox.tsx b/src/client/views/nodes/MapBox/MapBox.tsx
index 6495cf440..60dad314f 100644
--- a/src/client/views/nodes/MapBox/MapBox.tsx
+++ b/src/client/views/nodes/MapBox/MapBox.tsx
@@ -6,7 +6,6 @@ import { Checkbox, FormControlLabel, TextField } from '@mui/material';
 import * as turf from '@turf/turf';
 import { IconButton, Size, Type } from 'browndash-components';
 import * as d3 from 'd3';
-// eslint-disable-next-line import/no-extraneous-dependencies
 import { Feature, FeatureCollection, GeoJsonProperties, Geometry, LineString, Position } from 'geojson';
 import mapboxgl, { LngLatBoundsLike, MapLayerMouseEvent } from 'mapbox-gl';
 import { IReactionDisposer, ObservableMap, action, autorun, computed, makeObservable, observable, runInAction } from 'mobx';
@@ -25,14 +24,14 @@ import { DocumentManager } from '../../../util/DocumentManager';
 import { DragManager } from '../../../util/DragManager';
 import { LinkManager } from '../../../util/LinkManager';
 import { UndoManager, undoable } from '../../../util/UndoManager';
-import { PinProps, ViewBoxAnnotatableComponent, ViewBoxInterface } from '../../DocComponent';
+import { ViewBoxAnnotatableComponent, ViewBoxInterface } from '../../DocComponent';
+import { PinProps, PinDocView } from '../../PinFuncs';
 import { SidebarAnnos } from '../../SidebarAnnos';
 import { MarqueeOptionsMenu } from '../../collections/collectionFreeForm';
 import { Colors } from '../../global/globalEnums';
 import { DocumentView } from '../DocumentView';
 import { FieldView, FieldViewProps, FocusViewOptions } from '../FieldView';
 import { FormattedTextBox } from '../formattedText/FormattedTextBox';
-import { PresBox } from '../trails';
 import { fastSpeedIcon, mediumSpeedIcon, slowSpeedIcon } from './AnimationSpeedIcons';
 import { AnimationSpeed, AnimationStatus, AnimationUtility } from './AnimationUtility';
 import { MapAnchorMenu } from './MapAnchorMenu';
@@ -462,7 +461,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
         if (anchor) {
             if (!addAsAnnotation) anchor.backgroundColor = 'transparent';
             addAsAnnotation && this.addDocument(anchor);
-            PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), map: true } }, this.Document);
+            PinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), map: true } }, this.Document);
             return anchor;
         }
         return this.Document;
diff --git a/src/client/views/nodes/MapboxMapBox/MapboxContainer.tsx b/src/client/views/nodes/MapboxMapBox/MapboxContainer.tsx
index c19528af6..d899fcb9a 100644
--- a/src/client/views/nodes/MapboxMapBox/MapboxContainer.tsx
+++ b/src/client/views/nodes/MapboxMapBox/MapboxContainer.tsx
@@ -17,7 +17,8 @@ import { DragManager } from '../../../util/DragManager';
 import { LinkManager } from '../../../util/LinkManager';
 import { Transform } from '../../../util/Transform';
 import { UndoManager, undoable } from '../../../util/UndoManager';
-import { PinProps, ViewBoxAnnotatableComponent } from '../../DocComponent';
+import { ViewBoxAnnotatableComponent } from '../../DocComponent';
+import { PinProps, PinDocView } from '../../PinFuncs';
 import { SidebarAnnos } from '../../SidebarAnnos';
 import { MarqueeOptionsMenu } from '../../collections/collectionFreeForm';
 import { Colors } from '../../global/globalEnums';
@@ -26,7 +27,6 @@ import { FieldView, FieldViewProps, FocusViewOptions } from '../FieldView';
 import { MapAnchorMenu } from '../MapBox/MapAnchorMenu';
 import '../MapBox/MapBox.scss';
 import { FormattedTextBox } from '../formattedText/FormattedTextBox';
-import { PresBox } from '../trails';
 
 /**
  * MapBox architecture:
@@ -487,7 +487,7 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent<FieldViewProps>
         if (anchor) {
             if (!addAsAnnotation) anchor.backgroundColor = 'transparent';
             addAsAnnotation && this.addDocument(anchor);
-            PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), map: true } }, this.Document);
+            PinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), map: true } }, this.Document);
             return anchor;
         }
         return this.Document;
diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx
index 4d5013437..fbf5e018c 100644
--- a/src/client/views/nodes/PDFBox.tsx
+++ b/src/client/views/nodes/PDFBox.tsx
@@ -26,16 +26,16 @@ import { CollectionFreeFormView } from '../collections/collectionFreeForm';
 import { CollectionStackingView } from '../collections/CollectionStackingView';
 import { ContextMenu } from '../ContextMenu';
 import { ContextMenuProps } from '../ContextMenuItem';
-import { PinProps, ViewBoxAnnotatableComponent, ViewBoxInterface } from '../DocComponent';
+import { ViewBoxAnnotatableComponent, ViewBoxInterface } from '../DocComponent';
 import { Colors } from '../global/globalEnums';
-import { CreateImage } from './WebBoxRenderer';
 import { PDFViewer } from '../pdf/PDFViewer';
+import { PinDocView, PinProps } from '../PinFuncs';
 import { SidebarAnnos } from '../SidebarAnnos';
 import { DocumentView, OpenWhere } from './DocumentView';
 import { FieldView, FieldViewProps, FocusViewOptions } from './FieldView';
 import { ImageBox } from './ImageBox';
 import './PDFBox.scss';
-import { PresBox } from './trails';
+import { CreateImage } from './WebBoxRenderer';
 
 @observer
 export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implements ViewBoxInterface {
@@ -265,7 +265,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
             });
         const visibleAnchor = this._pdfViewer?._getAnchor?.(this._pdfViewer.savedAnnotations(), true);
         const anchor = visibleAnchor ?? docAnchor();
-        PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), scrollable: true, pannable: true } }, this.Document);
+        PinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), scrollable: true, pannable: true } }, this.Document);
         anchor.text = ele?.textContent ?? '';
         anchor.text_html = ele?.innerHTML;
         addAsAnnotation && this.addDocument(anchor);
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index b8ec0081a..13ee3250e 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -25,14 +25,14 @@ import { CollectionFreeFormView } from '../collections/collectionFreeForm/Collec
 import { CollectionStackedTimeline, TrimScope } from '../collections/CollectionStackedTimeline';
 import { ContextMenu } from '../ContextMenu';
 import { ContextMenuProps } from '../ContextMenuItem';
-import { PinProps, ViewBoxAnnotatableComponent, ViewBoxInterface } from '../DocComponent';
+import { ViewBoxAnnotatableComponent, ViewBoxInterface } from '../DocComponent';
 import { MarqueeAnnotator } from '../MarqueeAnnotator';
 import { AnchorMenu } from '../pdf/AnchorMenu';
+import { PinDocView, PinProps } from '../PinFuncs';
 import { StyleProp } from '../StyleProvider';
 import { DocumentView } from './DocumentView';
 import { FieldView, FieldViewProps, FocusViewOptions } from './FieldView';
 import { RecordingBox } from './RecordingBox';
-import { PresBox } from './trails';
 import './VideoBox.scss';
 
 /**
@@ -357,7 +357,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl
             addAsAnnotation && marquee
                 ? CollectionStackedTimeline.createAnchor(this.Document, this.dataDoc, this.annotationKey, timecode || undefined, undefined, marquee, addAsAnnotation) || this.Document
                 : Docs.Create.ConfigDocument({ title: '#' + timecode, _timecodeToShow: timecode, annotationOn: this.Document });
-        PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), temporal: true, pannable: true } }, this.Document);
+        PinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), temporal: true, pannable: true } }, this.Document);
         return anchor;
     };
 
diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx
index 725c5bab4..dfe237a86 100644
--- a/src/client/views/nodes/WebBox.tsx
+++ b/src/client/views/nodes/WebBox.tsx
@@ -27,19 +27,19 @@ import { MarqueeOptionsMenu } from '../collections/collectionFreeForm';
 import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView';
 import { ContextMenu } from '../ContextMenu';
 import { ContextMenuProps } from '../ContextMenuItem';
-import { PinProps, ViewBoxAnnotatableComponent, ViewBoxInterface } from '../DocComponent';
+import { ViewBoxAnnotatableComponent, ViewBoxInterface } from '../DocComponent';
 import { Colors } from '../global/globalEnums';
 import { LightboxView } from '../LightboxView';
 import { MarqueeAnnotator } from '../MarqueeAnnotator';
 import { AnchorMenu } from '../pdf/AnchorMenu';
 import { Annotation } from '../pdf/Annotation';
 import { GPTPopup } from '../pdf/GPTPopup/GPTPopup';
+import { PinDocView, PinProps } from '../PinFuncs';
 import { SidebarAnnos } from '../SidebarAnnos';
 import { StyleProp } from '../StyleProvider';
 import { DocumentView, OpenWhere } from './DocumentView';
 import { FieldView, FieldViewProps, FocusViewOptions } from './FieldView';
 import { LinkInfo } from './LinkDocPreview';
-import { PresBox } from './trails';
 import './WebBox.scss';
 
 const { CreateImage } = require('./WebBoxRenderer');
@@ -350,7 +350,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
                 y: NumCast(this.layoutDoc._layout_scrollTop),
                 annotationOn: this.Document,
             });
-        PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), scrollable: !!pinProps?.pinData, pannable: true } }, this.Document);
+        PinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), scrollable: !!pinProps?.pinData, pannable: true } }, this.Document);
         anchor.text = ele?.textContent ?? '';
         anchor.text_html = ele?.innerHTML ?? this._selectionText;
         addAsAnnotation && this.addDocumentWrapper(anchor);
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index d659d5e3c..9bcf5027f 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -45,18 +45,18 @@ import { CollectionStackingView } from '../../collections/CollectionStackingView
 import { CollectionTreeView } from '../../collections/CollectionTreeView';
 import { ContextMenu } from '../../ContextMenu';
 import { ContextMenuProps } from '../../ContextMenuItem';
-import { PinProps, ViewBoxAnnotatableComponent, ViewBoxInterface } from '../../DocComponent';
+import { ViewBoxAnnotatableComponent, ViewBoxInterface } from '../../DocComponent';
 import { Colors } from '../../global/globalEnums';
 import { LightboxView } from '../../LightboxView';
 import { AnchorMenu } from '../../pdf/AnchorMenu';
 import { GPTPopup } from '../../pdf/GPTPopup/GPTPopup';
+import { PinDocView, PinProps } from '../../PinFuncs';
 import { SidebarAnnos } from '../../SidebarAnnos';
 import { styleFromLayoutString, StyleProp } from '../../StyleProvider';
 import { mediaState } from '../AudioBox';
 import { DocumentView, DocumentViewInternal, OpenWhere } from '../DocumentView';
 import { FieldView, FieldViewProps, FocusViewOptions } from '../FieldView';
 import { LinkInfo } from '../LinkDocPreview';
-import { PresBox } from '../trails';
 import { DashDocCommentView } from './DashDocCommentView';
 import { DashDocView } from './DashDocView';
 import { DashFieldView } from './DashFieldView';
@@ -252,7 +252,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
         this._finishingLink = true;
         this.makeLinkAnchor(anchor, OpenWhere.addRight, undefined, 'Anchored Selection', false, addAsAnnotation);
         this._finishingLink = false;
-        PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), scrollable: true } }, this.Document);
+        PinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), scrollable: true } }, this.Document);
         return anchor;
     };
 
diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx
index 4fe7ec993..4cf9e99fa 100644
--- a/src/client/views/nodes/trails/PresBox.tsx
+++ b/src/client/views/nodes/trails/PresBox.tsx
@@ -6,9 +6,9 @@ import { action, computed, IReactionDisposer, makeObservable, observable, Observ
 import { observer } from 'mobx-react';
 import * as React from 'react';
 import { lightOrDark, returnFalse, returnOne, setupMoveUpEvents, StopEvent } from '../../../../ClientUtils';
-import { Doc, DocListCast, Field, FieldType, FieldResult, NumListCast, Opt, StrListCast } from '../../../../fields/Doc';
+import { Doc, DocListCast, Field, FieldResult, FieldType, NumListCast, Opt, StrListCast } from '../../../../fields/Doc';
 import { Animation, DocData, TransitionTimer } from '../../../../fields/DocSymbols';
-import { Copy, Id } from '../../../../fields/FieldSymbols';
+import { Copy } from '../../../../fields/FieldSymbols';
 import { InkField } from '../../../../fields/InkField';
 import { List } from '../../../../fields/List';
 import { ObjectField } from '../../../../fields/ObjectField';
@@ -24,6 +24,7 @@ import { dropActionType } from '../../../util/DropActionTypes';
 import { ScriptingGlobals } from '../../../util/ScriptingGlobals';
 import { SelectionManager } from '../../../util/SelectionManager';
 import { SerializationHelper } from '../../../util/SerializationHelper';
+import { SettingsManager } from '../../../util/SettingsManager';
 import { SnappingManager } from '../../../util/SnappingManager';
 import { undoBatch, UndoManager } from '../../../util/UndoManager';
 import { CollectionDockingView } from '../../collections/CollectionDockingView';
@@ -31,15 +32,15 @@ import { CollectionFreeFormView } from '../../collections/collectionFreeForm';
 import { CollectionStackedTimeline } from '../../collections/CollectionStackedTimeline';
 import { CollectionView } from '../../collections/CollectionView';
 import { TreeView } from '../../collections/TreeView';
-import { pinDataTypes as dataTypes, PinProps, ViewBoxBaseComponent } from '../../DocComponent';
+import { ViewBoxBaseComponent } from '../../DocComponent';
 import { Colors } from '../../global/globalEnums';
 import { LightboxView } from '../../LightboxView';
+import { pinDataTypes as dataTypes } from '../../PinFuncs';
 import { DocumentView, OpenWhere, OpenWhereMod } from '../DocumentView';
 import { FieldView, FieldViewProps, FocusViewOptions } from '../FieldView';
 import { ScriptingBox } from '../ScriptingBox';
 import './PresBox.scss';
 import { PresEffect, PresEffectDirection, PresMovement, PresStatus } from './PresEnums';
-import { SettingsManager } from '../../../util/SettingsManager';
 
 @observer
 export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
@@ -144,6 +145,10 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
     }
 
     componentDidMount() {
+        this._disposers.pause = reaction(
+            () => SnappingManager.UserPanned,
+            () => this.pauseAutoPres()
+        );
         this._disposers.keyboard = reaction(
             () => this.selectedDoc,
             selected => {
@@ -618,114 +623,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         return undefined;
     }
 
-    /// copies values from the targetDoc (which is the prototype of the pinDoc) to
-    /// reserved fields on the pinDoc so that those values can be restored to the
-    /// target doc when navigating to it.
-    @action
-    static pinDocView(pinDocIn: Doc, pinProps: PinProps, targetDoc: Doc) {
-        const pinDoc = pinDocIn;
-        pinDoc.presentation = true;
-        pinDoc.config = '';
-        if (pinProps.pinDocLayout) {
-            pinDoc.config_pinLayout = true;
-            pinDoc.config_x = NumCast(targetDoc.x);
-            pinDoc.config_y = NumCast(targetDoc.y);
-            pinDoc.config_rotation = NumCast(targetDoc.rotation);
-            pinDoc.config_width = NumCast(targetDoc.width);
-            pinDoc.config_height = NumCast(targetDoc.height);
-        }
-        if (pinProps.pinAudioPlay) pinDoc.presPlayAudio = true;
-        if (pinProps.pinData) {
-            pinDoc.config_pinData =
-                pinProps.pinData.scrollable ||
-                pinProps.pinData.temporal ||
-                pinProps.pinData.pannable ||
-                pinProps.pinData.type_collection ||
-                pinProps.pinData.clippable ||
-                pinProps.pinData.datarange ||
-                pinProps.pinData.dataview ||
-                pinProps.pinData.poslayoutview ||
-                pinProps?.activeFrame !== undefined;
-            const fkey = Doc.LayoutFieldKey(targetDoc);
-            if (pinProps.pinData.dataview) {
-                pinDoc.config_usePath = targetDoc[fkey + '_usePath'];
-                pinDoc.config_data = targetDoc[fkey] instanceof ObjectField ? (targetDoc[fkey] as ObjectField)[Copy]() : targetDoc.data;
-            }
-            if (pinProps.pinData.dataannos) {
-                const fieldKey = Doc.LayoutFieldKey(targetDoc);
-                pinDoc.config_annotations = new List<Doc>(DocListCast(targetDoc[DocData][fieldKey + '_annotations']).filter(doc => !doc.layout_unrendered));
-            }
-            if (pinProps.pinData.inkable) {
-                pinDoc.config_fillColor = targetDoc.fillColor;
-                pinDoc.config_color = targetDoc.color;
-                pinDoc.config_width = targetDoc._width;
-                pinDoc.config_height = targetDoc._height;
-            }
-            if (pinProps.pinData.scrollable) pinDoc.config_scrollTop = targetDoc._layout_scrollTop;
-            if (pinProps.pinData.clippable) {
-                const fieldKey = Doc.LayoutFieldKey(targetDoc);
-                pinDoc.config_clipWidth = targetDoc[fieldKey + '_clipWidth'];
-            }
-            if (pinProps.pinData.datarange) {
-                pinDoc.config_xRange = undefined; // targetDoc?.xrange;
-                pinDoc.config_yRange = undefined; // targetDoc?.yrange;
-            }
-            if (pinProps.pinData.map) {
-                // pinDoc.config_latitude = targetDoc?.latitude;
-                // pinDoc.config_longitude = targetDoc?.longitude;
-                pinDoc.config_map_zoom = targetDoc?.map_zoom;
-                pinDoc.config_map_type = targetDoc?.map_type;
-                // ...
-            }
-            if (pinProps.pinData.poslayoutview)
-                pinDoc.config_pinLayoutData = new List<string>(
-                    DocListCast(targetDoc[fkey] as ObjectField).map(d =>
-                        JSON.stringify({
-                            id: d[Id],
-                            x: NumCast(d.x),
-                            y: NumCast(d.y),
-                            w: NumCast(d._width),
-                            h: NumCast(d._height),
-                            fill: StrCast(d._fillColor),
-                            back: StrCast(d._backgroundColor),
-                            data: SerializationHelper.Serialize(d.data instanceof ObjectField ? d.data[Copy]() : ''),
-                            text: SerializationHelper.Serialize(d.text instanceof ObjectField ? d.text[Copy]() : ''),
-                        })
-                    )
-                );
-            if (pinProps.pinData.type_collection) pinDoc.config_viewType = targetDoc._type_collection;
-            if (pinProps.pinData.filters) pinDoc.config_docFilters = ObjectField.MakeCopy(targetDoc.childFilters as ObjectField);
-            if (pinProps.pinData.pivot) pinDoc.config_pivotField = targetDoc._pivotField;
-            if (pinProps.pinData.pannable) {
-                pinDoc.config_panX = NumCast(targetDoc._freeform_panX);
-                pinDoc.config_panY = NumCast(targetDoc._freeform_panY);
-                pinDoc.config_viewScale = NumCast(targetDoc._freeform_scale, 1);
-            }
-            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(pinDoc.config_clipStart) + NumCast(targetDoc.clipEnd, duration);
-            }
-        }
-        if (pinProps?.pinViewport) {
-            // If pinWithView option set then update scale and x / y props of slide
-            const bounds = pinProps.pinViewport;
-            pinDoc.config_pinView = true;
-            pinDoc.config_viewScale = NumCast(targetDoc._freeform_scale, 1);
-            pinDoc.config_panX = bounds.left + bounds.width / 2;
-            pinDoc.config_panY = bounds.top + bounds.height / 2;
-            pinDoc.config_viewBounds = new List<number>([bounds.left, bounds.top, bounds.left + bounds.width, bounds.top + bounds.height]);
-        }
-    }
-
-    @action
-    static reversePin(pinDoc: Doc, targetDoc: Doc) {
-        // const fkey = Doc.LayoutFieldKey(targetDoc);
-        pinDoc.config_data = targetDoc.data;
-
-        console.log(pinDoc.presData);
-    }
-
     /**
      * This method makes sure that cursor navigates to the element that
      * has the option open and last in the group.
diff --git a/src/client/views/nodes/trails/PresElementBox.tsx b/src/client/views/nodes/trails/PresElementBox.tsx
index 5fa32ad12..cf78a45b7 100644
--- a/src/client/views/nodes/trails/PresElementBox.tsx
+++ b/src/client/views/nodes/trails/PresElementBox.tsx
@@ -22,6 +22,7 @@ import { TreeView } from '../../collections/TreeView';
 import { ViewBoxBaseComponent } from '../../DocComponent';
 import { EditableView } from '../../EditableView';
 import { Colors } from '../../global/globalEnums';
+import { PinDocView } from '../../PinFuncs';
 import { StyleProp } from '../../StyleProvider';
 import { DocumentView } from '../DocumentView';
 import { FieldView, FieldViewProps } from '../FieldView';
@@ -335,7 +336,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
     updateCapturedViewContents = undoable(
         action((presTargetDoc: Doc, activeItem: Doc) => {
             const target = DocCast(presTargetDoc.annotationOn) ?? presTargetDoc;
-            PresBox.pinDocView(activeItem, { pinData: PresBox.pinDataTypes(target) }, target);
+            PinDocView(activeItem, { pinData: PresBox.pinDataTypes(target) }, target);
         }),
         'updated captured view contents'
     );
diff --git a/src/server/server_Initialization.ts b/src/server/server_Initialization.ts
index 5e6c855c9..9183688c6 100644
--- a/src/server/server_Initialization.ts
+++ b/src/server/server_Initialization.ts
@@ -12,7 +12,6 @@ import * as passport from 'passport';
 import * as request from 'request';
 import * as webpack from 'webpack';
 import * as wdm from 'webpack-dev-middleware';
-// eslint-disable-next-line import/no-extraneous-dependencies
 import * as whm from 'webpack-hot-middleware';
 import * as zlib from 'zlib';
 import * as config from '../../webpack.config';
-- 
cgit v1.2.3-70-g09d2


From 098deaa68c8b9bb781748fbe0c1bd0104bab3596 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Tue, 30 Apr 2024 23:35:18 -0400
Subject: unwinding more import loops by splitting up Documents.ts into
 DocUtils.ts and moving crate functions to <>Box functions

---
 .eslintrc.json                                     |    2 +-
 .../apis/google_docs/GooglePhotosClientUtils.ts    |    3 +-
 src/client/documents/DocFromField.ts               |   50 +
 src/client/documents/DocUtils.ts                   |  883 ++++++++++++++
 src/client/documents/Documents.ts                  | 1270 +-------------------
 src/client/util/CurrentUserUtils.ts                |    8 +-
 src/client/util/DictationManager.ts                |    5 +-
 src/client/util/DocumentManager.ts                 |    5 +-
 src/client/util/DragManager.ts                     |   21 +-
 src/client/util/GroupManager.tsx                   |    6 +-
 src/client/util/GroupMemberView.tsx                |    4 +-
 src/client/util/LinkFollower.ts                    |   16 +-
 src/client/util/RTFMarkup.tsx                      |   22 +-
 src/client/util/ReplayMovements.ts                 |    2 +-
 src/client/util/SettingsManager.tsx                |   17 +-
 src/client/util/SharingManager.tsx                 |   19 +-
 src/client/views/DashboardView.tsx                 |   47 +-
 src/client/views/DocComponent.tsx                  |    7 +-
 src/client/views/DocumentButtonBar.tsx             |    7 +-
 src/client/views/DocumentDecorations.tsx           |    5 +-
 src/client/views/GestureOverlay.tsx                |   43 +-
 src/client/views/GlobalKeyHandler.ts               |   11 +-
 src/client/views/InkStrokeProperties.ts            |    3 +-
 src/client/views/InkingStroke.tsx                  |   79 +-
 src/client/views/LightboxView.tsx                  |   10 +-
 src/client/views/MainView.tsx                      |   10 +-
 src/client/views/MarqueeAnnotator.tsx              |    8 +-
 src/client/views/PreviewCursor.tsx                 |    3 +-
 src/client/views/PropertiesButtons.tsx             |    5 +-
 .../views/PropertiesDocBacklinksSelector.tsx       |    2 +-
 src/client/views/PropertiesDocContextSelector.tsx  |    3 +-
 src/client/views/PropertiesView.tsx                |    5 +-
 src/client/views/SidebarAnnos.tsx                  |    5 +-
 src/client/views/TemplateMenu.tsx                  |    3 +-
 .../views/collections/CollectionCarousel3DView.tsx |    2 +-
 .../views/collections/CollectionDockingView.tsx    |   26 +-
 .../collections/CollectionMasonryViewFieldRow.tsx  |    2 +-
 .../views/collections/CollectionNoteTakingView.tsx |   12 +-
 .../collections/CollectionNoteTakingViewColumn.tsx |   12 +-
 .../views/collections/CollectionPileView.tsx       |    4 +-
 .../collections/CollectionStackedTimeline.tsx      |   12 +-
 .../views/collections/CollectionStackingView.tsx   |   11 +-
 .../CollectionStackingViewFieldColumn.tsx          |   10 +-
 src/client/views/collections/CollectionSubView.tsx |   12 +-
 .../views/collections/CollectionTreeView.tsx       |    5 +-
 src/client/views/collections/CollectionView.tsx    |   23 +-
 src/client/views/collections/TabDocView.tsx        |    5 +-
 src/client/views/collections/TreeView.tsx          |    8 +-
 .../collectionFreeForm/CollectionFreeFormView.tsx  |   17 +-
 .../collections/collectionFreeForm/MarqueeView.tsx |    9 +-
 .../collectionGrid/CollectionGridView.tsx          |    5 +-
 .../collectionSchema/CollectionSchemaView.tsx      |    6 +-
 .../collections/collectionSchema/SchemaRowBox.tsx  |    4 +-
 src/client/views/global/globalScripts.ts           |    6 +-
 src/client/views/linking/LinkMenuItem.tsx          |    3 +-
 .../views/newlightbox/ButtonMenu/ButtonMenu.tsx    |    2 +-
 src/client/views/newlightbox/NewLightboxView.tsx   |   10 +-
 src/client/views/nodes/AudioBox.tsx                |   11 +-
 .../views/nodes/CollectionFreeFormDocumentView.tsx |    6 +-
 src/client/views/nodes/ComparisonBox.tsx           |   21 +-
 src/client/views/nodes/DataVizBox/DataVizBox.tsx   |   27 +-
 src/client/views/nodes/DocumentLinksButton.tsx     |    4 +-
 src/client/views/nodes/DocumentView.tsx            |   39 +-
 src/client/views/nodes/EquationBox.tsx             |   18 +-
 src/client/views/nodes/FieldView.tsx               |   27 +-
 src/client/views/nodes/FocusViewOptions.ts         |   24 +
 src/client/views/nodes/FontIconBox/FontIconBox.tsx |   17 +-
 src/client/views/nodes/FunctionPlotBox.tsx         |   11 +-
 src/client/views/nodes/ImageBox.tsx                |   13 +-
 src/client/views/nodes/KeyValueBox.tsx             |   10 +-
 src/client/views/nodes/KeyValuePair.tsx            |    3 +-
 src/client/views/nodes/LabelBox.tsx                |   12 +-
 src/client/views/nodes/LinkBox.tsx                 |   18 +
 src/client/views/nodes/LinkDocPreview.tsx          |    3 +-
 src/client/views/nodes/LoadingBox.tsx              |    6 +
 src/client/views/nodes/MapBox/MapBox.tsx           |   16 +-
 src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx |    2 +-
 src/client/views/nodes/MapBox/MapPushpinBox.tsx    |    7 +
 .../views/nodes/MapboxMapBox/MapboxContainer.tsx   |   11 +-
 src/client/views/nodes/OpenWhere.ts                |   25 +
 src/client/views/nodes/PDFBox.tsx                  |   14 +-
 .../nodes/PhysicsBox/PhysicsSimulationBox.tsx      |  383 +++---
 .../views/nodes/RecordingBox/RecordingBox.tsx      |    5 +
 src/client/views/nodes/ScreenshotBox.tsx           |   10 +-
 src/client/views/nodes/ScriptingBox.tsx            |    7 +
 src/client/views/nodes/VideoBox.tsx                |   17 +-
 src/client/views/nodes/WebBox.tsx                  |   15 +-
 src/client/views/nodes/calendarBox/CalendarBox.tsx |    6 +
 .../views/nodes/formattedText/DashDocView.tsx      |    5 +-
 .../views/nodes/formattedText/DashFieldView.tsx    |    2 +-
 .../views/nodes/formattedText/FormattedTextBox.tsx |   56 +-
 .../formattedText/ProsemirrorExampleTransfer.ts    |    4 +-
 .../views/nodes/formattedText/RichTextRules.ts     |    3 +-
 .../views/nodes/generativeFill/GenerativeFill.tsx  |    5 +-
 src/client/views/nodes/trails/PresBox.tsx          |   15 +-
 src/client/views/nodes/trails/PresElementBox.tsx   |    7 +-
 src/client/views/pdf/Annotation.tsx                |    2 +-
 src/client/views/pdf/GPTPopup/GPTPopup.tsx         |    3 +-
 src/client/views/pdf/PDFViewer.tsx                 |   11 +-
 src/client/views/search/SearchBox.tsx              |   12 +-
 src/client/views/selectedDoc/SelectedDocView.tsx   |    2 +-
 src/fields/Doc.ts                                  |   76 ++
 src/fields/RichTextUtils.ts                        |    3 +-
 src/fields/ScriptField.ts                          |    4 +
 src/mobile/MobileInterface.tsx                     |    3 +-
 105 files changed, 1900 insertions(+), 1896 deletions(-)
 create mode 100644 src/client/documents/DocFromField.ts
 create mode 100644 src/client/documents/DocUtils.ts
 create mode 100644 src/client/views/nodes/FocusViewOptions.ts
 create mode 100644 src/client/views/nodes/OpenWhere.ts

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

diff --git a/.eslintrc.json b/.eslintrc.json
index 6d851a64a..2e4da56b8 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -57,7 +57,7 @@
         "react/destructuring-assignment": 0,
         "no-restricted-globals": ["error", "event"],
         "no-param-reassign": ["error", { "props": false }],
-        "import/no-cycle": 0,
+        // "import/no-cycle": 0,
         "no-alert": 0,
         "radix": "off"
     },
diff --git a/src/client/apis/google_docs/GooglePhotosClientUtils.ts b/src/client/apis/google_docs/GooglePhotosClientUtils.ts
index 1eb977813..fdc185a8e 100644
--- a/src/client/apis/google_docs/GooglePhotosClientUtils.ts
+++ b/src/client/apis/google_docs/GooglePhotosClientUtils.ts
@@ -10,7 +10,8 @@ import { RichTextUtils } from '../../../fields/RichTextUtils';
 import { Cast, ImageCast, StrCast } from '../../../fields/Types';
 import { MediaItem, NewMediaItemResult } from '../../../server/apis/google/SharedTypes';
 import { Networking } from '../../Network';
-import { DocUtils, Docs, DocumentOptions } from '../../documents/Documents';
+import { Docs, DocumentOptions } from '../../documents/Documents';
+import { DocUtils } from '../../documents/DocUtils';
 import { FormattedTextBox } from '../../views/nodes/formattedText/FormattedTextBox';
 import { GoogleAuthenticationManager } from '../GoogleAuthenticationManager';
 
diff --git a/src/client/documents/DocFromField.ts b/src/client/documents/DocFromField.ts
new file mode 100644
index 000000000..1c0a9755b
--- /dev/null
+++ b/src/client/documents/DocFromField.ts
@@ -0,0 +1,50 @@
+/* eslint-disable prefer-destructuring */
+/* eslint-disable default-param-last */
+/* eslint-disable no-use-before-define */
+import { Doc, DocListCast } from '../../fields/Doc';
+import { InkField } from '../../fields/InkField';
+import { List } from '../../fields/List';
+import { AudioField, ImageField, PdfField, VideoField } from '../../fields/URLField';
+import { InkingStroke } from '../views/InkingStroke';
+import { CollectionView } from '../views/collections/CollectionView';
+import { AudioBox } from '../views/nodes/AudioBox';
+import { ImageBox } from '../views/nodes/ImageBox';
+import { PDFBox } from '../views/nodes/PDFBox';
+import { VideoBox } from '../views/nodes/VideoBox';
+import { FormattedTextBox } from '../views/nodes/formattedText/FormattedTextBox';
+import { Docs, DocumentOptions } from './Documents';
+
+export function DocumentFromField(target: Doc, fieldKey: string, proto?: Doc, options?: DocumentOptions): Doc | undefined {
+    let created: Doc | undefined;
+    const field = target[fieldKey];
+    const resolved = options ?? {};
+    if (field instanceof ImageField) {
+        created = Docs.Create.ImageDocument(field.url.href, resolved);
+        created.layout = ImageBox.LayoutString(fieldKey);
+    } else if (field instanceof Doc) {
+        created = field;
+    } else if (field instanceof VideoField) {
+        created = Docs.Create.VideoDocument(field.url.href, resolved);
+        created.layout = VideoBox.LayoutString(fieldKey);
+    } else if (field instanceof PdfField) {
+        created = Docs.Create.PdfDocument(field.url.href, resolved);
+        created.layout = PDFBox.LayoutString(fieldKey);
+    } else if (field instanceof AudioField) {
+        created = Docs.Create.AudioDocument(field.url.href, resolved);
+        created.layout = AudioBox.LayoutString(fieldKey);
+    } else if (field instanceof InkField) {
+        created = Docs.Create.InkDocument(field.inkData, resolved);
+        created.layout = InkingStroke.LayoutString(fieldKey);
+    } else if (field instanceof List && field[0] instanceof Doc) {
+        created = Docs.Create.StackingDocument(DocListCast(field), resolved);
+        created.layout = CollectionView.LayoutString(fieldKey);
+    } else {
+        created = Docs.Create.TextDocument('', { ...{ _width: 200, _height: 25, _layout_autoHeight: true }, ...resolved });
+        created.layout = FormattedTextBox.LayoutString(fieldKey);
+    }
+    if (created) {
+        created.title = fieldKey;
+        proto && created.proto && (created.proto = Doc.GetProto(proto));
+    }
+    return created;
+}
diff --git a/src/client/documents/DocUtils.ts b/src/client/documents/DocUtils.ts
new file mode 100644
index 000000000..e93bbc41b
--- /dev/null
+++ b/src/client/documents/DocUtils.ts
@@ -0,0 +1,883 @@
+/* eslint-disable prefer-destructuring */
+/* eslint-disable default-param-last */
+/* eslint-disable no-use-before-define */
+import { IconProp } from '@fortawesome/fontawesome-svg-core';
+import { saveAs } from 'file-saver';
+import * as JSZip from 'jszip';
+import { action, runInAction } from 'mobx';
+import { ClientUtils } from '../../ClientUtils';
+import * as JSZipUtils from '../../JSZipUtils';
+import { decycle } from '../../decycler/decycler';
+import { DateField } from '../../fields/DateField';
+import { Doc, DocListCast, Field, FieldType, LinkedTo, Opt, SetActiveAudioLinker, StrListCast } from '../../fields/Doc';
+import { DocData } from '../../fields/DocSymbols';
+import { Id } from '../../fields/FieldSymbols';
+import { InkDataFieldName, InkField } from '../../fields/InkField';
+import { List, ListFieldName } from '../../fields/List';
+import { ProxyField } from '../../fields/Proxy';
+import { RichTextField } from '../../fields/RichTextField';
+import { ComputedField, FollowLinkScript, ScriptField } from '../../fields/ScriptField';
+import { BoolCast, Cast, DocCast, FieldValue, NumCast, ScriptCast, StrCast } from '../../fields/Types';
+import { AudioField, CsvField, ImageField, PdfField, VideoField, WebField } from '../../fields/URLField';
+import { SharingPermissions } from '../../fields/util';
+import { Upload } from '../../server/SharedMediaTypes';
+import { DocServer } from '../DocServer';
+import { Networking } from '../Network';
+import { LinkManager } from '../util/LinkManager';
+import { ScriptingGlobals } from '../util/ScriptingGlobals';
+import { SerializationHelper } from '../util/SerializationHelper';
+import { UndoManager, undoable } from '../util/UndoManager';
+import { ContextMenu } from '../views/ContextMenu';
+import { ContextMenuProps } from '../views/ContextMenuItem';
+import { FieldViewProps } from '../views/nodes/FieldView';
+import { LinkDescriptionPopup } from '../views/nodes/LinkDescriptionPopup';
+import { LoadingBox } from '../views/nodes/LoadingBox';
+import { OpenWhere } from '../views/nodes/OpenWhere';
+import { TaskCompletionBox } from '../views/nodes/TaskCompletedBox';
+import { DocumentType } from './DocumentTypes';
+import { Docs, DocumentOptions } from './Documents';
+
+const { DFLT_IMAGE_NATIVE_DIM } = require('../views/global/globalCssVariables.module.scss'); // prettier-ignore
+
+const defaultNativeImageDim = Number(DFLT_IMAGE_NATIVE_DIM.replace('px', ''));
+
+export namespace DocUtils {
+    function matchFieldValue(doc: Doc, key: string, valueIn: any): boolean {
+        let value = valueIn;
+        const hasFunctionFilter = ClientUtils.HasFunctionFilter(value);
+        if (hasFunctionFilter) {
+            return hasFunctionFilter(StrCast(doc[key]));
+        }
+        if (key === LinkedTo) {
+            // links are not a field value, so handled here.  value is an expression of form ([field=]idToDoc("..."))
+            const allLinks = LinkManager.Instance.getAllRelatedLinks(doc);
+            const matchLink = (val: string, anchor: Doc) => {
+                const linkedToExp = (val ?? '').split('=');
+                if (linkedToExp.length === 1) return Field.toScriptString(anchor) === val;
+                return Field.toScriptString(DocCast(anchor[linkedToExp[0]])) === linkedToExp[1];
+            };
+            // prettier-ignore
+            return  (value === Doc.FilterNone && !allLinks.length) || 
+                    (value === Doc.FilterAny && !!allLinks.length) ||
+                    (allLinks.some(link => matchLink(value,DocCast(link.link_anchor_1)) || 
+                                           matchLink(value,DocCast(link.link_anchor_2)) ));
+        }
+        if (typeof value === 'string') {
+            value = value.replace(`,${ClientUtils.noRecursionHack}`, '');
+        }
+        const fieldVal = doc[key];
+        // prettier-ignore
+        if ((value === Doc.FilterAny && fieldVal !== undefined) || 
+            (value === Doc.FilterNone && fieldVal === undefined)) {
+            return true;
+        }
+        const vals = StrListCast(fieldVal); // list typing is very imperfect.  casting to a string list doesn't mean that the entries will actually be strings
+        if (vals.length) {
+            return vals.some(v => typeof v === 'string' && v.includes(value)); // bcz: arghh: Todo: comparison should be parameterized as exact, or substring
+        }
+        return Field.toString(fieldVal as FieldType).includes(value); // bcz: arghh: Todo: comparison should be parameterized as exact, or substring
+    }
+    /**
+     * @param docs
+     * @param childFilters
+     * @param childFiltersByRanges
+     * @param parentCollection
+     * Given a list of docs and childFilters, @returns the list of Docs that match those filters
+     */
+    export function FilterDocs(childDocs: Doc[], childFilters: string[], childFiltersByRanges: string[], parentCollection?: Doc) {
+        if (!childFilters?.length && !childFiltersByRanges?.length) {
+            return childDocs.filter(d => !d.cookies); // remove documents that need a cookie if there are no filters to provide one
+        }
+
+        const filterFacets: { [key: string]: { [value: string]: string } } = {}; // maps each filter key to an object with value=>modifier fields
+        childFilters.forEach(filter => {
+            const fields = filter.split(Doc.FilterSep);
+            const key = fields[0];
+            const value = fields[1];
+            const modifiers = fields[2];
+            if (!filterFacets[key]) {
+                filterFacets[key] = {};
+            }
+            filterFacets[key][value] = modifiers;
+        });
+
+        const filteredDocs = childFilters.length
+            ? childDocs.filter(d => {
+                  if (d.z) return true;
+                  // if the document needs a cookie but no filter provides the cookie, then the document does not pass the filter
+                  if (d.cookies && (!filterFacets.cookies || !Object.keys(filterFacets.cookies).some(key => d.cookies === key))) {
+                      return false;
+                  }
+                  const facetKeys = Object.keys(filterFacets).filter(fkey => fkey !== 'cookies' && fkey !== ClientUtils.noDragDocsFilter.split(Doc.FilterSep)[0]);
+                  // eslint-disable-next-line no-restricted-syntax
+                  for (const facetKey of facetKeys) {
+                      const facet = filterFacets[facetKey];
+
+                      // facets that match some value in the field of the document (e.g. some text field)
+                      const matches = Object.keys(facet).filter(value => value !== 'cookies' && facet[value] === 'match');
+
+                      // facets that have a check next to them
+                      const checks = Object.keys(facet).filter(value => facet[value] === 'check');
+
+                      // metadata facets that exist
+                      const exists = Object.keys(facet).filter(value => facet[value] === 'exists');
+
+                      // facets that unset metadata (a hack for making cookies work)
+                      const unsets = Object.keys(facet).filter(value => facet[value] === 'unset');
+
+                      // facets that specify that a field must not match a specific value
+                      const xs = Object.keys(facet).filter(value => facet[value] === 'x');
+
+                      if (!unsets.length && !exists.length && !xs.length && !checks.length && !matches.length) return true;
+                      const failsNotEqualFacets = !xs.length ? false : xs.some(value => matchFieldValue(d, facetKey, value));
+                      const satisfiesCheckFacets = !checks.length ? true : checks.some(value => matchFieldValue(d, facetKey, value));
+                      const satisfiesExistsFacets = !exists.length ? true : facetKey !== LinkedTo ? d[facetKey] !== undefined : LinkManager.Instance.getAllRelatedLinks(d).length;
+                      const satisfiesUnsetsFacets = !unsets.length ? true : d[facetKey] === undefined;
+                      const satisfiesMatchFacets = !matches.length
+                          ? true
+                          : matches.some(value => {
+                                if (facetKey.startsWith('*')) {
+                                    //  fields starting with a '*' are used to match families of related fields.  ie, *modificationDate will match text_modificationDate, data_modificationDate, etc
+                                    const allKeys = Array.from(Object.keys(d));
+                                    allKeys.push(...Object.keys(Doc.GetProto(d)));
+                                    const keys = allKeys.filter(key => key.includes(facetKey.substring(1)));
+                                    return keys.some(key => Field.toString(d[key] as FieldType).includes(value));
+                                }
+                                return Field.toString(d[facetKey] as FieldType).includes(value);
+                            });
+                      // if we're ORing them together, the default return is false, and we return true for a doc if it satisfies any one set of criteria
+                      if (parentCollection?.childFilters_boolean === 'OR') {
+                          if (satisfiesUnsetsFacets && satisfiesExistsFacets && satisfiesCheckFacets && !failsNotEqualFacets && satisfiesMatchFacets) return true;
+                      }
+                      // if we're ANDing them together, the default return is true, and we return false for a doc if it doesn't satisfy any set of criteria
+                      else if (!satisfiesUnsetsFacets || !satisfiesExistsFacets || !satisfiesCheckFacets || failsNotEqualFacets || (matches.length && !satisfiesMatchFacets)) return false;
+                  }
+                  return parentCollection?.childFilters_boolean !== 'OR';
+              })
+            : childDocs;
+        const rangeFilteredDocs = filteredDocs.filter(d => {
+            for (let i = 0; i < childFiltersByRanges.length; i += 3) {
+                const key = childFiltersByRanges[i];
+                const min = Number(childFiltersByRanges[i + 1]);
+                const max = Number(childFiltersByRanges[i + 2]);
+                const val = typeof d[key] === 'string' ? (Number(StrCast(d[key])).toString() === StrCast(d[key]) ? Number(StrCast(d[key])) : undefined) : Cast(d[key], 'number', null);
+                if (val === undefined) {
+                    // console.log("Should 'undefined' pass range filter or not?")
+                } else if (val < min || val > max) return false;
+            }
+            return true;
+        });
+        return rangeFilteredDocs;
+    }
+
+    export const ActiveRecordings: { props: FieldViewProps; getAnchor: (addAsAnnotation: boolean) => Doc }[] = [];
+
+    export function MakeLinkToActiveAudio(getSourceDoc: () => Doc | undefined, broadcastEvent = true) {
+        broadcastEvent && runInAction(() => { Doc.RecordingEvent += 1; }); // prettier-ignore
+        return DocUtils.ActiveRecordings.map(audio => {
+            const sourceDoc = getSourceDoc();
+            return sourceDoc && DocUtils.MakeLink(sourceDoc, audio.getAnchor(true) || audio.props.Document, { link_displayLine: false, link_relationship: 'recording annotation:linked recording', link_description: 'recording timeline' });
+        });
+    }
+
+    SetActiveAudioLinker(MakeLinkToActiveAudio);
+
+    export function MakeLink(source: Doc, target: Doc, linkSettings: { link_relationship?: string; link_description?: string; link_displayLine?: boolean }, id?: string, showPopup?: number[]) {
+        if (!linkSettings.link_relationship) linkSettings.link_relationship = target.type === DocumentType.RTF ? 'Commentary:Comments On' : 'link';
+        if (target.doc === Doc.UserDoc()) return undefined;
+
+        const makeLink = action((linkDoc: Doc, showAt?: number[]) => {
+            if (showAt) {
+                LinkManager.Instance.currentLink = linkDoc;
+
+                TaskCompletionBox.textDisplayed = 'Link Created';
+                TaskCompletionBox.popupX = showAt[0];
+                TaskCompletionBox.popupY = showAt[1] - 33;
+                TaskCompletionBox.taskCompleted = true;
+
+                LinkDescriptionPopup.Instance.popupX = showAt[0];
+                LinkDescriptionPopup.Instance.popupY = showAt[1];
+                LinkDescriptionPopup.Instance.display = true;
+
+                const rect = document.body.getBoundingClientRect();
+                if (LinkDescriptionPopup.Instance.popupX + 200 > rect.width) {
+                    LinkDescriptionPopup.Instance.popupX -= 190;
+                    TaskCompletionBox.popupX -= 40;
+                }
+                if (LinkDescriptionPopup.Instance.popupY + 100 > rect.height) {
+                    LinkDescriptionPopup.Instance.popupY -= 40;
+                    TaskCompletionBox.popupY -= 40;
+                }
+
+                setTimeout(
+                    action(() => {
+                        TaskCompletionBox.taskCompleted = false;
+                    }),
+                    2500
+                );
+            }
+            return linkDoc;
+        });
+
+        const a = source.layout_unrendered ? 'link_anchor_1?.annotationOn' : 'link_anchor_1';
+        const b = target.layout_unrendered ? 'link_anchor_2?.annotationOn' : 'link_anchor_2';
+
+        return makeLink(
+            Docs.Create.LinkDocument(
+                source,
+                target,
+                {
+                    acl_Guest: SharingPermissions.Augment,
+                    _acl_Guest: SharingPermissions.Augment,
+                    title: ComputedField.MakeFunction('generateLinkTitle(this)') as any,
+                    link_anchor_1_useSmallAnchor: source.useSmallAnchor ? true : undefined,
+                    link_anchor_2_useSmallAnchor: target.useSmallAnchor ? true : undefined,
+                    link_displayLine: linkSettings.link_displayLine,
+                    link_relationship: linkSettings.link_relationship,
+                    link_description: linkSettings.link_description,
+                    x: ComputedField.MakeFunction(`((this.${a}?.x||0)+(this.${b}?.x||0))/2`) as any,
+                    y: ComputedField.MakeFunction(`((this.${a}?.y||0)+(this.${b}?.y||0))/2`) as any,
+                    link_autoMoveAnchors: true,
+                    _lockedPosition: true,
+                    _layout_showCaption: '', // removed since they conflict with showing a link with a LinkBox (ie, line, not comparison box)
+                    _layout_showTitle: '',
+                    // _layout_showCaption: 'link_description',
+                    // _layout_showTitle: 'link_relationship',
+                },
+                id
+            ),
+            showPopup
+        );
+    }
+
+    export function AssignScripts(doc: Doc, scripts?: { [key: string]: string | undefined }, funcs?: { [key: string]: string }) {
+        scripts &&
+            Object.keys(scripts).forEach(key => {
+                const script = scripts[key];
+                if (ScriptCast(doc[key])?.script.originalScript !== scripts[key] && script) {
+                    (key.startsWith('_') ? doc : Doc.GetProto(doc))[key] = ScriptField.MakeScript(script, {
+                        self: Doc.name,
+                        this: Doc.name,
+                        dragData: Doc.DocDragDataName,
+                        value: 'any',
+                        _readOnly_: 'boolean',
+                        scriptContext: 'any',
+                        documentView: Doc.name,
+                        heading: Doc.name,
+                        checked: 'boolean',
+                        containingTreeView: Doc.name,
+                        altKey: 'boolean',
+                        ctrlKey: 'boolean',
+                        shiftKey: 'boolean',
+                    });
+                }
+            });
+        funcs &&
+            Object.keys(funcs)
+                .filter(key => !key.endsWith('-setter'))
+                .forEach(key => {
+                    const cfield = ComputedField.WithoutComputed(() => FieldValue(doc[key]));
+                    if (ScriptCast(cfield)?.script.originalScript !== funcs[key]) {
+                        const setFunc = Cast(funcs[key + '-setter'], 'string', null);
+                        (key.startsWith('_') ? doc : Doc.GetProto(doc))[key] = funcs[key] ? ComputedField.MakeFunction(funcs[key], { dragData: Doc.DocDragDataName }, { _readOnly_: true }, setFunc) : undefined;
+                    }
+                });
+        return doc;
+    }
+    export function AssignOpts(doc: Doc | undefined, reqdOpts: DocumentOptions, items?: Doc[]) {
+        if (doc) {
+            const compareValues = (val1: any, val2: any) => {
+                if (val1 instanceof List && val2 instanceof List && val1.length === val2.length) {
+                    return !val1.some(v => !val2.includes(v)) || !val2.some(v => val1.includes(v));
+                }
+                return val1 === val2;
+            };
+            Object.entries(reqdOpts).forEach(pair => {
+                const targetDoc = pair[0].startsWith('_') ? doc : Doc.GetProto(doc as Doc);
+                if (!Object.getOwnPropertyNames(targetDoc).includes(pair[0].replace(/^_/, '')) || !compareValues(pair[1], targetDoc[pair[0]])) {
+                    targetDoc[pair[0]] = pair[1];
+                }
+            });
+            items?.forEach(item => !DocListCast(doc.data).includes(item) && Doc.AddDocToList(Doc.GetProto(doc), 'data', item));
+            items && DocListCast(doc.data).forEach(item => Doc.IsSystem(item) && !items.includes(item) && Doc.RemoveDocFromList(Doc.GetProto(doc), 'data', item));
+        }
+        return doc;
+    }
+    export function AssignDocField(doc: Doc, field: string, creator: (reqdOpts: DocumentOptions, items?: Doc[]) => Doc, reqdOpts: DocumentOptions, items?: Doc[], scripts?: { [key: string]: string }, funcs?: { [key: string]: string }) {
+        // eslint-disable-next-line no-return-assign
+        return DocUtils.AssignScripts(DocUtils.AssignOpts(DocCast(doc[field]), reqdOpts, items) ?? (doc[field] = creator(reqdOpts, items)), scripts, funcs);
+    }
+
+    /**
+     *
+     * @param type the type of file.
+     * @param path the path to the file.
+     * @param options the document options.
+     * @param overwriteDoc the placeholder loading doc.
+     * @returns
+     */
+    export async function DocumentFromType(type: string, path: string, options: DocumentOptions, overwriteDoc?: Doc): Promise<Opt<Doc>> {
+        let ctor: ((path: string, options: DocumentOptions, overwriteDoc?: Doc) => Doc | Promise<Doc | undefined>) | undefined;
+        if (type.indexOf('image') !== -1) {
+            ctor = Docs.Create.ImageDocument;
+            if (!options._width) options._width = 300;
+        }
+        if (type.indexOf('video') !== -1) {
+            ctor = Docs.Create.VideoDocument;
+            if (!options._width) options._width = 600;
+            if (!options._height) options._height = ((options._width as number) * 2) / 3;
+        }
+        if (type.indexOf('audio') !== -1) {
+            ctor = Docs.Create.AudioDocument;
+        }
+        if (type.indexOf('pdf') !== -1) {
+            ctor = Docs.Create.PdfDocument;
+            if (!options._width) options._width = 400;
+            if (!options._height) options._height = ((options._width as number) * 1200) / 927;
+        }
+        if (type.indexOf('csv') !== -1) {
+            ctor = Docs.Create.DataVizDocument;
+            if (!options._width) options._width = 400;
+            if (!options._height) options._height = ((options._width as number) * 1200) / 927;
+        }
+        // TODO:al+glr
+        // if (type.indexOf("map") !== -1) {
+        //     ctor = Docs.Create.MapDocument;
+        //     if (!options._width) options._width = 800;
+        //     if (!options._height) options._height = (options._width as number) * 3 / 4;
+        // }
+        if (type.indexOf('html') !== -1) {
+            if (path.includes(window.location.hostname)) {
+                const s = path.split('/');
+                const id = s[s.length - 1];
+                return DocServer.GetRefField(id).then(field => {
+                    if (field instanceof Doc) {
+                        const embedding = Doc.MakeEmbedding(field);
+                        embedding.x = (options.x as number) || 0;
+                        embedding.y = (options.y as number) || 0;
+                        embedding._width = (options._width as number) || 300;
+                        embedding._height = (options._height as number) || (options._width as number) || 300;
+                        return embedding;
+                    }
+                    return undefined;
+                });
+            }
+            ctor = Docs.Create.WebDocument;
+            // eslint-disable-next-line no-param-reassign
+            options = { ...options, _width: 400, _height: 512, title: path };
+        }
+
+        return ctor ? ctor(path, overwriteDoc ? { ...options, title: StrCast(overwriteDoc.title, path) } : options, overwriteDoc) : undefined;
+    }
+
+    export function addDocumentCreatorMenuItems(docTextAdder: (d: Doc) => void, docAdder: (d: Doc) => void, x: number, y: number, simpleMenu: boolean = false, pivotField?: string, pivotValue?: string): void {
+        const documentList: ContextMenuProps[] = DocListCast(DocListCast(Doc.MyTools?.data)[0]?.data)
+            .filter(btnDoc => !btnDoc.hidden)
+            .map(btnDoc => Cast(btnDoc?.dragFactory, Doc, null))
+            .filter(doc => doc && doc !== Doc.UserDoc().emptyTrail && doc.title)
+            .map(dragDoc => ({
+                description: ':' + StrCast(dragDoc.title).replace('Untitled ', ''),
+                event: undoable(() => {
+                    const newDoc = DocUtils.copyDragFactory(dragDoc);
+                    if (newDoc) {
+                        newDoc.author = ClientUtils.CurrentUserEmail();
+                        newDoc.x = x;
+                        newDoc.y = y;
+                        Doc.SetSelectOnLoad(newDoc);
+                        if (pivotField) {
+                            newDoc[pivotField] = pivotValue;
+                        }
+                        docAdder?.(newDoc);
+                    }
+                }, StrCast(dragDoc.title)),
+                icon: Doc.toIcon(dragDoc),
+            })) as ContextMenuProps[];
+        ContextMenu.Instance.addItem({
+            description: 'Create document',
+            subitems: documentList,
+            icon: 'file',
+        });
+        !simpleMenu &&
+            ContextMenu.Instance.addItem({
+                description: 'Styled Notes',
+                subitems: DocListCast((Doc.UserDoc().template_notes as Doc).data).map(note => ({
+                    description: ':' + StrCast(note.title),
+                    event: undoable(() => {
+                        const textDoc = Docs.Create.TextDocument('', {
+                            _width: 200,
+                            x,
+                            y,
+                            _layout_autoHeight: note._layout_autoHeight !== false,
+                            title: StrCast(note.title) + '#' + (note.embeddingCount = NumCast(note.embeddingCount) + 1),
+                        });
+                        textDoc.layout_fieldKey = 'layout_' + note.title;
+                        textDoc[textDoc.layout_fieldKey] = note;
+                        if (pivotField) {
+                            textDoc[pivotField] = pivotValue;
+                        }
+                        docTextAdder(textDoc);
+                    }, 'create quick note'),
+                    icon: StrCast(note.icon) as IconProp,
+                })) as ContextMenuProps[],
+                icon: 'sticky-note',
+            });
+        const userDocList: ContextMenuProps[] = DocListCast(DocListCast(Doc.MyTools?.data)[1]?.data)
+            .filter(btnDoc => !btnDoc.hidden)
+            .map(btnDoc => Cast(btnDoc?.dragFactory, Doc, null))
+            .filter(doc => doc && doc !== Doc.UserDoc().emptyTrail && doc !== Doc.UserDoc().emptyNote && doc.title)
+            .map(dragDoc => ({
+                description: ':' + StrCast(dragDoc.title).replace('Untitled ', ''),
+                event: undoable(() => {
+                    const newDoc = DocUtils.delegateDragFactory(dragDoc);
+                    if (newDoc) {
+                        newDoc.author = ClientUtils.CurrentUserEmail();
+                        newDoc.x = x;
+                        newDoc.y = y;
+                        Doc.SetSelectOnLoad(newDoc);
+                        if (pivotField) {
+                            newDoc[pivotField] = pivotValue;
+                        }
+                        docAdder?.(newDoc);
+                    }
+                }, StrCast(dragDoc.title)),
+                icon: Doc.toIcon(dragDoc),
+            })) as ContextMenuProps[];
+        ContextMenu.Instance.addItem({
+            description: 'User Templates',
+            subitems: userDocList,
+            icon: 'file',
+        });
+    }
+
+    // applies a custom template to a document.  the template is identified by it's short name (e.g, slideView not layout_slideView)
+
+    /**
+     * Applies a template to a Doc and logs the action with the UndoManager
+     * If the template already exists and has been registered, it can be specified by it's signature name (e.g., 'icon' not 'layout_icon').
+     * Alternatively, the signature can be omitted and the template can be provided.
+     * @param doc the Doc to apply the template to.
+     * @param creator a function that will create the template if it doesn't exist
+     * @param templateSignature the signature name for a template that has already been created and registered on the userDoc.  (can be "" if template is provide)
+     * @param template the template to use (optional if templateSignature is provided)
+     * @returns doc
+     */
+    export function makeCustomViewClicked(doc: Doc, creator: Opt<(documents: Array<Doc>, options: DocumentOptions, id?: string) => Doc>, templateSignature: string = 'custom', template?: Doc) {
+        const batch = UndoManager.StartBatch('makeCustomViewClicked');
+        createCustomView(doc, creator, templateSignature || StrCast(template?.title), template);
+        batch.end();
+        return doc;
+    }
+    export function findTemplate(templateName: string, type: string) {
+        let docLayoutTemplate: Opt<Doc>;
+        const iconViews = DocListCast(Cast(Doc.UserDoc().template_icons, Doc, null)?.data);
+        const templBtns = DocListCast(Cast(Doc.UserDoc().template_buttons, Doc, null)?.data);
+        const noteTypes = DocListCast(Cast(Doc.UserDoc().template_notes, Doc, null)?.data);
+        const userTypes = DocListCast(Cast(Doc.UserDoc().template_user, Doc, null)?.data);
+        const clickFuncs = DocListCast(Cast(Doc.UserDoc().template_clickFuncs, Doc, null)?.data);
+        const allTemplates = iconViews
+            .concat(templBtns)
+            .concat(noteTypes)
+            .concat(userTypes)
+            .concat(clickFuncs)
+            .map(btnDoc => (btnDoc.dragFactory as Doc) || btnDoc)
+            .filter(doc => doc.isTemplateDoc);
+        // bcz: this is hacky -- want to have different templates be applied depending on the "type" of a document.  but type is not reliable and there could be other types of template searches so this should be generalized
+        // first try to find a template that matches the specific document type (<typeName>_<templateName>).  otherwise, fallback to a general match on <templateName>
+        !docLayoutTemplate &&
+            allTemplates.forEach(tempDoc => {
+                StrCast(tempDoc.title) === templateName + '_' + type && (docLayoutTemplate = tempDoc);
+            });
+        !docLayoutTemplate &&
+            allTemplates.forEach(tempDoc => {
+                StrCast(tempDoc.title) === templateName && (docLayoutTemplate = tempDoc);
+            });
+        return docLayoutTemplate;
+    }
+    export function createCustomView(doc: Doc, creator: Opt<(documents: Array<Doc>, options: DocumentOptions, id?: string) => Doc>, templateSignature: string = 'custom', docLayoutTemplate?: Doc) {
+        const templateName = templateSignature.replace(/\(.*\)/, '');
+        doc.layout_fieldKey = 'layout_' + (templateSignature || (docLayoutTemplate?.title ?? ''));
+        // eslint-disable-next-line no-param-reassign
+        docLayoutTemplate = docLayoutTemplate || findTemplate(templateName, StrCast(doc.isGroup && doc.transcription ? 'transcription' : doc.type));
+
+        const customName = 'layout_' + templateSignature;
+        const _width = NumCast(doc._width);
+        const _height = NumCast(doc._height);
+        const options = { title: 'data', backgroundColor: StrCast(doc.backgroundColor), _layout_autoHeight: true, _width, x: -_width / 2, y: -_height / 2, _layout_showSidebar: false };
+
+        if (docLayoutTemplate) {
+            if (docLayoutTemplate !== doc[customName]) {
+                Doc.ApplyTemplateTo(docLayoutTemplate, doc, customName, undefined);
+            }
+        } else {
+            let fieldTemplate: Opt<Doc>;
+            if (doc.data instanceof RichTextField || typeof doc.data === 'string') {
+                fieldTemplate = Docs.Create.TextDocument('', options);
+            } else if (doc.data instanceof PdfField) {
+                fieldTemplate = Docs.Create.PdfDocument('http://www.msn.com', options);
+            } else if (doc.data instanceof VideoField) {
+                fieldTemplate = Docs.Create.VideoDocument('http://www.cs.brown.edu', options);
+            } else if (doc.data instanceof AudioField) {
+                fieldTemplate = Docs.Create.AudioDocument('http://www.cs.brown.edu', options);
+            } else if (doc.data instanceof ImageField) {
+                fieldTemplate = Docs.Create.ImageDocument('http://www.cs.brown.edu', options);
+            }
+            const docTemplate = creator?.(fieldTemplate ? [fieldTemplate] : [], { title: customName + '(' + doc.title + ')', isTemplateDoc: true, _width: _width + 20, _height: Math.max(100, _height + 45) });
+            fieldTemplate && Doc.MakeMetadataFieldTemplate(fieldTemplate, docTemplate ? Doc.GetProto(docTemplate) : docTemplate);
+            docTemplate && Doc.ApplyTemplateTo(docTemplate, doc, customName, undefined);
+        }
+    }
+    export function makeCustomView(doc: Doc, custom: boolean, layout: string) {
+        Doc.setNativeView(doc);
+        if (custom) {
+            makeCustomViewClicked(doc, Docs.Create.StackingDocument, layout, undefined);
+        }
+    }
+    export function iconify(doc: Doc) {
+        const layoutFieldKey = Cast(doc.layout_fieldKey, 'string', null);
+        DocUtils.makeCustomViewClicked(doc, Docs.Create.StackingDocument, 'icon', undefined);
+        if (layoutFieldKey && layoutFieldKey !== 'layout' && layoutFieldKey !== 'layout_icon') doc.deiconifyLayout = layoutFieldKey.replace('layout_', '');
+    }
+
+    export function pileup(docList: Doc[], x?: number, y?: number, size: number = 55, create: boolean = true) {
+        runInAction(() => {
+            docList.forEach((doc, i) => {
+                const d = doc;
+                DocUtils.iconify(d);
+                d.x = Math.cos((Math.PI * 2 * i) / docList.length) * size - size;
+                d.y = Math.sin((Math.PI * 2 * i) / docList.length) * size - size;
+                d._timecodeToShow = undefined; // bcz: this should be automatic somehow.. along with any other properties that were logically associated with the original collection
+            });
+        });
+        if (create) {
+            const newCollection = Docs.Create.PileDocument(docList, { title: 'pileup', _freeform_noZoom: true, x: (x || 0) - size, y: (y || 0) - size, _width: size * 2, _height: size * 2, dragWhenActive: true, _layout_fitWidth: false });
+            newCollection.x = NumCast(newCollection.x) + NumCast(newCollection._width) / 2 - size;
+            newCollection.y = NumCast(newCollection.y) + NumCast(newCollection._height) / 2 - size;
+            newCollection._width = newCollection._height = size * 2;
+            return newCollection;
+        }
+        return undefined;
+    }
+    export function makeIntoPortal(doc: Doc, layoutDoc: Doc, allLinks: Doc[]) {
+        const portalLink = allLinks.find(d => d.link_anchor_1 === doc && d.link_relationship === 'portal to:portal from');
+        if (!portalLink) {
+            DocUtils.MakeLink(
+                doc,
+                Docs.Create.FreeformDocument([], {
+                    _width: NumCast(layoutDoc._width) + 10,
+                    _height: Math.max(NumCast(layoutDoc._height), NumCast(layoutDoc._width) + 10),
+                    _isLightbox: true,
+                    _layout_fitWidth: true,
+                    title: StrCast(doc.title) + ' [Portal]',
+                }),
+                { link_relationship: 'portal to:portal from' }
+            );
+        }
+        doc.followLinkLocation = OpenWhere.lightbox;
+        doc.onClick = FollowLinkScript();
+    }
+
+    export function LeavePushpin(doc: Doc, annotationField: string) {
+        if (doc.followLinkToggle) return undefined;
+        const context = Cast(doc.embedContainer, Doc, null) ?? Cast(doc.annotationOn, Doc, null);
+        const hasContextAnchor = LinkManager.Links(doc).some(l => (l.link_anchor_2 === doc && Cast(l.link_anchor_1, Doc, null)?.annotationOn === context) || (l.link_anchor_1 === doc && Cast(l.link_anchor_2, Doc, null)?.annotationOn === context));
+        if (context && !hasContextAnchor && (context.type === DocumentType.VID || context.type === DocumentType.WEB || context.type === DocumentType.PDF || context.type === DocumentType.IMG)) {
+            const pushpin = Docs.Create.FontIconDocument({
+                title: '',
+                annotationOn: Cast(doc.annotationOn, Doc, null),
+                followLinkToggle: true,
+                icon: 'map-pin',
+                x: Cast(doc.x, 'number', null),
+                y: Cast(doc.y, 'number', null),
+                backgroundColor: '#ACCEF7',
+                layout_hideAllLinks: true,
+                _width: 15,
+                _height: 15,
+                _xPadding: 0,
+                onClick: FollowLinkScript(),
+                _timecodeToShow: Cast(doc._timecodeToShow, 'number', null),
+            });
+            Doc.AddDocToList(context, annotationField, pushpin);
+            DocUtils.MakeLink(pushpin, doc, { link_relationship: 'pushpin' }, '');
+            doc._timecodeToShow = undefined;
+            return pushpin;
+        }
+        return undefined;
+    }
+
+    // /**
+    //  *
+    //  * @param dms Degree Minute Second format exif gps data
+    //  * @param ref ref that determines negativity of decimal coordinates
+    //  * @returns a decimal format of gps latitude / longitude
+    //  */
+    // function getDecimalfromDMS(dms?: number[], ref?: string) {
+    //     if (dms && ref) {
+    //         let degrees = dms[0] / dms[1];
+    //         let minutes = dms[2] / dms[3] / 60.0;
+    //         let seconds = dms[4] / dms[5] / 3600.0;
+
+    //         if (['S', 'W'].includes(ref)) {
+    //             degrees = -degrees; minutes = -minutes; seconds = -seconds
+    //         }
+    //         return (degrees + minutes + seconds).toFixed(5);
+    //     }
+    // }
+
+    function ConvertDMSToDD(degrees: number, minutes: number, seconds: number, direction: string) {
+        let dd = degrees + minutes / 60 + seconds / (60 * 60);
+        if (direction === 'S' || direction === 'W') {
+            dd *= -1;
+        } // Don't do anything for N or E
+        return dd;
+    }
+
+    export function assignImageInfo(result: Upload.FileInformation, protoIn: Doc) {
+        const proto = protoIn;
+        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}`);
+            return;
+        }
+        const full = { ...options, _width: 400, title: name };
+        const pathname = result.accessPaths.agnostic.client;
+        const doc = await DocUtils.DocumentFromType(type, pathname, full, overwriteDoc);
+        if (doc) {
+            const proto = Doc.GetProto(doc);
+            proto.text = result.rawText;
+            !(result instanceof Error) && DocUtils.assignImageInfo(result, proto);
+            if (Upload.isVideoInformation(result)) {
+                proto.data_duration = result.duration;
+            }
+            if (overwriteDoc) {
+                LoadingBox.removeCurrentlyLoading(overwriteDoc);
+            }
+            generatedDocuments.push(doc);
+        }
+    }
+
+    export function GetNewTextDoc(title: string, x: number, y: number, width?: number, height?: number, annotationOn?: Doc, backgroundColor?: string) {
+        const defaultTextTemplate = DocCast(Doc.UserDoc().defaultTextLayout);
+        const tbox = Docs.Create.TextDocument('', {
+            annotationOn,
+            backgroundColor,
+            x,
+            y,
+            title,
+            ...(defaultTextTemplate
+                ? {} // if the new doc will inherit from a template, don't set any layout fields since that would block the inheritance
+                : {
+                      _width: width || 200,
+                      _height: 35,
+                      _layout_centered: BoolCast(Doc.UserDoc()._layout_centered),
+                      _layout_fitWidth: true,
+                      _layout_autoHeight: true,
+                  }),
+        });
+
+        if (defaultTextTemplate) {
+            tbox.layout_fieldKey = 'layout_' + StrCast(defaultTextTemplate.title);
+            Doc.GetProto(tbox)[StrCast(tbox.layout_fieldKey)] = defaultTextTemplate; // set the text doc's layout to render with the text template
+            tbox[DocData].proto = defaultTextTemplate; // and also set the text doc to inherit from the template (this allows the template to specify default field values)
+        }
+        return tbox;
+    }
+
+    export function uploadYoutubeVideoLoading(videoId: string, options: DocumentOptions, overwriteDoc?: Doc) {
+        const generatedDocuments: Doc[] = [];
+        Networking.UploadYoutubeToServer(videoId, overwriteDoc?.[Id]).then(upfiles => {
+            const {
+                source: { newFilename, mimetype },
+                result,
+            } = upfiles.lastElement();
+            if ((result as any).message) {
+                if (overwriteDoc) {
+                    overwriteDoc.isLoading = false;
+                    overwriteDoc.loadingError = (result as any).message;
+                    LoadingBox.removeCurrentlyLoading(overwriteDoc);
+                }
+            } else newFilename && processFileupload(generatedDocuments, newFilename, mimetype ?? '', result, options, overwriteDoc);
+        });
+    }
+
+    /**
+     * uploadFilesToDocs will take in an array of Files, and creates documents for the
+     * new files.
+     *
+     * @param files an array of files that will be uploaded
+     * @param options options to use while uploading
+     * @returns
+     */
+    export async function uploadFilesToDocs(files: File[], options: DocumentOptions) {
+        const generatedDocuments: Doc[] = [];
+
+        // These files do not have overwriteDocs, so we do not set the guid and let the client generate one.
+        const fileNoGuidPairs: Networking.FileGuidPair[] = files.map(file => ({ file }));
+
+        const upfiles = await Networking.UploadFilesToServer(fileNoGuidPairs);
+        upfiles.forEach(({ source: { newFilename, mimetype }, result }) => {
+            newFilename && mimetype && processFileupload(generatedDocuments, newFilename, mimetype, result, options);
+        });
+        return generatedDocuments;
+    }
+
+    export function uploadFileToDoc(file: File, options: DocumentOptions, overwriteDoc: Doc) {
+        const generatedDocuments: Doc[] = [];
+        // Since this file has an overwriteDoc, we can set the client tracking guid to the overwriteDoc's guid.
+        Networking.UploadFilesToServer([{ file, guid: overwriteDoc[Id] }]).then(upfiles => {
+            const {
+                source: { newFilename, mimetype },
+                result,
+            } = upfiles.lastElement() ?? { source: { newFilename: '', mimetype: '' }, result: { message: 'upload failed' } };
+            if ((result as any).message) {
+                if (overwriteDoc) {
+                    overwriteDoc.loadingError = (result as any).message;
+                    LoadingBox.removeCurrentlyLoading(overwriteDoc);
+                }
+            } else newFilename && mimetype && processFileupload(generatedDocuments, newFilename, mimetype, result, options, overwriteDoc);
+        });
+    }
+
+    // copies the specified drag factory document
+    export function copyDragFactory(dragFactory: Doc) {
+        if (!dragFactory) return undefined;
+        const ndoc = dragFactory.isTemplateDoc ? Doc.ApplyTemplate(dragFactory) : Doc.MakeCopy(dragFactory, true);
+        if (ndoc && dragFactory.dragFactory_count !== undefined) {
+            dragFactory.dragFactory_count = NumCast(dragFactory.dragFactory_count) + 1;
+            Doc.SetInPlace(ndoc, 'title', ndoc.title + ' ' + NumCast(dragFactory.dragFactory_count).toString(), true);
+        }
+
+        return ndoc;
+    }
+    export function delegateDragFactory(dragFactory: Doc) {
+        const ndoc = Doc.MakeDelegateWithProto(dragFactory);
+        if (ndoc && dragFactory.dragFactory_count !== undefined) {
+            dragFactory.dragFactory_count = NumCast(dragFactory.dragFactory_count) + 1;
+            Doc.GetProto(ndoc).title = ndoc.title + ' ' + NumCast(dragFactory.dragFactory_count).toString();
+        }
+        return ndoc;
+    }
+
+    export async function Zip(doc: Doc, zipFilename = 'dashExport.zip') {
+        const { clone, map, linkMap } = await Doc.MakeClone(doc);
+        const proms = new Set<string>();
+        function replacer(key: any, value: any) {
+            if (key && ['branchOf', 'cloneOf', 'cursors'].includes(key)) return undefined;
+            if (value?.__type === 'image') {
+                const extension = value.url.replace(/.*\./, '');
+                proms.add(value.url.replace('.' + extension, '_o.' + extension));
+                return SerializationHelper.Serialize(new ImageField(value.url));
+            }
+            if (value?.__type === 'pdf') {
+                proms.add(value.url);
+                return SerializationHelper.Serialize(new PdfField(value.url));
+            }
+            if (value?.__type === 'audio') {
+                proms.add(value.url);
+                return SerializationHelper.Serialize(new AudioField(value.url));
+            }
+            if (value?.__type === 'video') {
+                proms.add(value.url);
+                return SerializationHelper.Serialize(new VideoField(value.url));
+            }
+            if (
+                value instanceof Doc ||
+                value instanceof ScriptField ||
+                value instanceof RichTextField ||
+                value instanceof InkField ||
+                value instanceof CsvField ||
+                value instanceof WebField ||
+                value instanceof DateField ||
+                value instanceof ProxyField ||
+                value instanceof ComputedField
+            ) {
+                return SerializationHelper.Serialize(value);
+            }
+            if (value instanceof Array && key !== ListFieldName && key !== InkDataFieldName) return { fields: value, __type: 'list' };
+            return value;
+        }
+
+        const docs: { [id: string]: any } = {};
+        const links: { [id: string]: any } = {};
+        Array.from(map.entries()).forEach(f => {
+            docs[f[0]] = f[1];
+        });
+        Array.from(linkMap.entries()).forEach(l => {
+            links[l[0]] = l[1];
+        });
+        const jsonDocs = JSON.stringify({ id: clone[Id], docs, links }, decycle(replacer));
+
+        const zip = new JSZip();
+        let count = 0;
+        const promArr = Array.from(proms)
+            .filter(url => url?.startsWith('/files'))
+            .map(url => url.replace('/', '')); // window.location.origin));
+        console.log(promArr.length);
+        if (!promArr.length) {
+            zip.file('docs.json', jsonDocs);
+            zip.generateAsync({ type: 'blob' }).then(content => saveAs(content, zipFilename));
+        } else
+            promArr.forEach((url, i) => {
+                // loading a file and add it in a zip file
+                JSZipUtils.getBinaryContent(window.location.origin + '/' + url, (err: any, data: any) => {
+                    if (err) throw err; // or handle the error
+                    // // Generate a directory within the Zip file structure
+                    // const assets = zip.folder("assets");
+                    // assets.file(filename, data, {binary: true});
+                    const assetPathOnServer = promArr[i].replace(window.location.origin + '/', '').replace(/\//g, '%%%');
+                    zip.file(assetPathOnServer, data, { binary: true });
+                    console.log(' => ' + url);
+                    if (++count === promArr.length) {
+                        zip.file('docs.json', jsonDocs);
+                        zip.generateAsync({ type: 'blob' }).then(content => saveAs(content, zipFilename));
+                        // const a = document.createElement("a");
+                        // const url = Utils.prepend(`/downloadId/${this.props.Document[Id]}`);
+                        // a.href = url;
+                        // a.download = `DocExport-${this.props.Document[Id]}.zip`;
+                        // a.click();
+                    }
+                });
+            });
+    }
+}
+
+ScriptingGlobals.add('Docs', Docs);
+// eslint-disable-next-line prefer-arrow-callback
+ScriptingGlobals.add(function copyDragFactory(dragFactory: Doc, asDelegate?: boolean) {
+    return dragFactory instanceof Doc ? (asDelegate ? DocUtils.delegateDragFactory(dragFactory) : DocUtils.copyDragFactory(dragFactory)) : dragFactory;
+});
+// eslint-disable-next-line prefer-arrow-callback
+ScriptingGlobals.add(function makeDelegate(proto: any) {
+    const d = Docs.Create.DelegateDocument(proto, { title: 'child of ' + proto.title });
+    return d;
+});
+// eslint-disable-next-line prefer-arrow-callback
+ScriptingGlobals.add(function generateLinkTitle(link: Doc) {
+    const linkAnchor1title = link.link_anchor_1 && link.link_anchor_1 !== link ? Cast(link.link_anchor_1, Doc, null)?.title : '<?>';
+    const linkAnchor2title = link.link_anchor_2 && link.link_anchor_2 !== link ? Cast(link.link_anchor_2, Doc, null)?.title : '<?>';
+    const relation = link.link_relationship || 'to';
+    return `${linkAnchor1title} (${relation}) ${linkAnchor2title}`;
+});
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 0e7f911c7..a3fea1ce4 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -1,79 +1,27 @@
 /* eslint-disable prefer-destructuring */
 /* eslint-disable default-param-last */
 /* eslint-disable no-use-before-define */
-import { IconProp } from '@fortawesome/fontawesome-svg-core';
-import { saveAs } from 'file-saver';
-import * as JSZip from 'jszip';
-import { action, reaction, runInAction } from 'mobx';
+import { reaction } from 'mobx';
 import { basename } from 'path';
 import { ClientUtils, OmitKeys } from '../../ClientUtils';
-import * as JSZipUtils from '../../JSZipUtils';
-import { decycle } from '../../decycler/decycler';
 import { DateField } from '../../fields/DateField';
-import { Doc, DocListCast, Field, FieldType, LinkedTo, Opt, StrListCast, updateCachedAcls } from '../../fields/Doc';
-import { DocData, Initializing } from '../../fields/DocSymbols';
-import { Id } from '../../fields/FieldSymbols';
+import { ActiveArrowEnd, ActiveArrowStart, ActiveDash, ActiveFillColor, ActiveInkBezierApprox, ActiveInkColor, ActiveInkWidth, ActiveIsInkMask, CreateLinkToActiveAudio, Doc, FieldType, Opt, updateCachedAcls } from '../../fields/Doc';
+import { Initializing } from '../../fields/DocSymbols';
 import { HtmlField } from '../../fields/HtmlField';
-import { InkDataFieldName, InkField } from '../../fields/InkField';
-import { List, ListFieldName } from '../../fields/List';
-import { ProxyField } from '../../fields/Proxy';
+import { InkField } from '../../fields/InkField';
+import { List } from '../../fields/List';
 import { RichTextField } from '../../fields/RichTextField';
 import { SchemaHeaderField } from '../../fields/SchemaHeaderField';
 import { ComputedField, ScriptField } from '../../fields/ScriptField';
-import { BoolCast, Cast, DocCast, FieldValue, NumCast, ScriptCast, StrCast } from '../../fields/Types';
+import { ScriptCast, StrCast } from '../../fields/Types';
 import { AudioField, CsvField, ImageField, PdfField, VideoField, WebField } from '../../fields/URLField';
-import { SharingPermissions, inheritParentAcls } from '../../fields/util';
+import { SharingPermissions } from '../../fields/util';
 import { PointData } from '../../pen-gestures/GestureTypes';
-import { Upload } from '../../server/SharedMediaTypes';
 import { DocServer } from '../DocServer';
-import { Networking } from '../Network';
-import { DragManager } from '../util/DragManager';
 import { dropActionType } from '../util/DropActionTypes';
-import { FollowLinkScript } from '../util/LinkFollower';
 import { LinkManager } from '../util/LinkManager';
-import { ScriptingGlobals } from '../util/ScriptingGlobals';
-import { SerializationHelper } from '../util/SerializationHelper';
-import { UndoManager, undoable } from '../util/UndoManager';
-import { ContextMenu } from '../views/ContextMenu';
-import { ContextMenuProps } from '../views/ContextMenuItem';
-import { ActiveArrowEnd, ActiveArrowStart, ActiveDash, ActiveFillColor, ActiveInkBezierApprox, ActiveInkColor, ActiveInkWidth, ActiveIsInkMask, InkingStroke } from '../views/InkingStroke';
-import { CollectionDockingView } from '../views/collections/CollectionDockingView';
-import { CollectionView } from '../views/collections/CollectionView';
-import { DimUnit } from '../views/collections/collectionMulticolumn/CollectionMulticolumnView';
-import { AudioBox, mediaState } from '../views/nodes/AudioBox';
-import { ComparisonBox } from '../views/nodes/ComparisonBox';
-import { DataVizBox } from '../views/nodes/DataVizBox/DataVizBox';
-import { OpenWhere } from '../views/nodes/DocumentView';
-import { EquationBox } from '../views/nodes/EquationBox';
-import { FieldViewProps } from '../views/nodes/FieldView';
-import { FontIconBox } from '../views/nodes/FontIconBox/FontIconBox';
-import { FunctionPlotBox } from '../views/nodes/FunctionPlotBox';
-import { ImageBox } from '../views/nodes/ImageBox';
-import { KeyValueBox } from '../views/nodes/KeyValueBox';
-import { LabelBox } from '../views/nodes/LabelBox';
-import { LinkBox } from '../views/nodes/LinkBox';
-import { LinkDescriptionPopup } from '../views/nodes/LinkDescriptionPopup';
-import { LoadingBox } from '../views/nodes/LoadingBox';
-import { MapBox } from '../views/nodes/MapBox/MapBox';
-import { MapPushpinBox } from '../views/nodes/MapBox/MapPushpinBox';
-import { PDFBox } from '../views/nodes/PDFBox';
-import { PhysicsSimulationBox } from '../views/nodes/PhysicsBox/PhysicsSimulationBox';
-import { RecordingBox } from '../views/nodes/RecordingBox/RecordingBox';
-import { ScreenshotBox } from '../views/nodes/ScreenshotBox';
-import { ScriptingBox } from '../views/nodes/ScriptingBox';
-import { TaskCompletionBox } from '../views/nodes/TaskCompletedBox';
-import { VideoBox } from '../views/nodes/VideoBox';
-import { WebBox } from '../views/nodes/WebBox';
-import { CalendarBox } from '../views/nodes/calendarBox/CalendarBox';
-import { FormattedTextBox } from '../views/nodes/formattedText/FormattedTextBox';
-import { PresBox } from '../views/nodes/trails/PresBox';
-import { PresElementBox } from '../views/nodes/trails/PresElementBox';
-import { SearchBox } from '../views/search/SearchBox';
 import { CollectionViewType, DocumentType } from './DocumentTypes';
 
-const { DFLT_IMAGE_NATIVE_DIM } = require('../views/global/globalCssVariables.module.scss'); // prettier-ignore
-
-const defaultNativeImageDim = Number(DFLT_IMAGE_NATIVE_DIM.replace('px', ''));
 class EmptyBox {
     public static LayoutString() {
         return '';
@@ -146,7 +94,7 @@ class DocInfo extends FInfo {
 }
 class DimInfo extends FInfo {
     fieldType? = FInfoFieldType.enumeration;
-    values? = [DimUnit.Pixel, DimUnit.Ratio];
+    values? = []; // DimUnit.Pixel, DimUnit.Ratio];
     readOnly = false;
     filterable = false;
     override searchable = () => false;
@@ -202,7 +150,7 @@ type STRt = StrInfo | string;
 type LISTt = ListInfo | List<any>;
 type DOCt = DocInfo | Doc;
 type RTFt = RtfInfo | RichTextField;
-type DIMt = DimInfo | typeof DimUnit.Pixel | typeof DimUnit.Ratio;
+type DIMt = DimInfo; // | typeof DimUnit.Pixel | typeof DimUnit.Ratio;
 type PEVt = PEInfo | 'none' | 'all';
 type COLLt = CTypeInfo | CollectionViewType;
 type DROPt = DAInfo | dropActionType;
@@ -245,6 +193,7 @@ export class DocumentOptions {
     _nativeWidth?: NUMt = new NumInfo('native width of document contents (e.g., the pixel width of an image)', false);
     _nativeHeight?: NUMt = new NumInfo('native height of document contents (e.g., the pixel height of an image)', false);
 
+    acl?: STRt = new StrInfo('unused except as a display category in KeyValueBox');
     acl_Guest?: STRt = new StrInfo("permissions granted to users logged in as 'guest' (either view, or private)"); // public permissions
     _acl_Guest?: string; // public permissions
     type?: DTYPEt = new DTypeInfo('type of document', true);
@@ -253,6 +202,7 @@ export class DocumentOptions {
     title?: STRt = new StrInfo('title of document', true);
     title_custom?: BOOLt = new BoolInfo('whether title is a default or has been intentionally set');
     caption?: RichTextField;
+    systemIcon?: STRt = new StrInfo("name of icon to use to represent document's type");
     author?: string; // STRt = new StrInfo('creator of document'); // bcz: don't change this. Otherwise, the userDoc's field Infos will have a FieldInfo assigned to its author field which will render it unreadable
     author_date?: DATEt = new DateInfo('date the document was created', true);
     annotationOn?: DOCt = new DocInfo('document annotated by this document', false);
@@ -272,6 +222,12 @@ export class DocumentOptions {
     _lockedPosition?: BOOLt = new BoolInfo("lock the x,y coordinates of the document so that it can't be dragged");
     _lockedTransform?: BOOLt = new BoolInfo('lock the freeform_panx,freeform_pany and scale parameters of the document so that it be panned/zoomed');
 
+    dataViz_title?: string;
+    dataViz_line?: string;
+    dataViz_pie?: string;
+    dataViz_histogram?: string;
+    dataViz?: string;
+
     layout?: string | Doc; // default layout string or template document
     layout_isSvg?: BOOLt = new BoolInfo('whether document decorations and other selections should handle pointerEvents for svg content or use doc bounding box');
     layout_keyValue?: STRt = new StrInfo('layout definition for showing keyValue view of document', false);
@@ -325,6 +281,7 @@ export class DocumentOptions {
     _text_fontSize?: string;
     _text_fontFamily?: string;
     _text_fontWeight?: string;
+    fontSize?: string;
     _pivotField?: string; // field key used to determine headings for sections in stacking, masonry, pivot views
 
     infoWindowOpen?: BOOLt = new BoolInfo('whether info window corresponding to pin is open (on MapDocuments)');
@@ -333,7 +290,7 @@ export class DocumentOptions {
     _label_maxFontSize?: NUMt = new NumInfo('maximum font size for labelBoxes', false);
     stroke_width?: NUMt = new NumInfo('width of an ink stroke', false);
     stroke_showLabel?: BOOLt = new BoolInfo('show label inside of stroke');
-    mediaState?: STRt = new StrInfo(`status of audio/video media document: ${mediaState.PendingRecording}, ${mediaState.Recording}, ${mediaState.Paused}, ${mediaState.Playing}`, false);
+    mediaState?: STRt = new StrInfo(`status of audio/video media document:`); // ${mediaState.PendingRecording}, ${mediaState.Recording}, ${mediaState.Paused}, ${mediaState.Playing}`, false);
     recording?: BOOLt = new BoolInfo('whether WebCam is recording or not');
     slides?: DOCt = new DocInfo('presentation slide associated with video recording (bcz: should be renamed!!)');
     autoPlayAnchors?: BOOLt = new BoolInfo('whether to play audio/video when an anchor is clicked in a stackedTimeline.');
@@ -403,6 +360,7 @@ export class DocumentOptions {
     // STOPPING HERE
 
     // freeform properties
+    freeform?: STRt = new StrInfo('');
     _freeform_backgroundGrid?: BOOLt = new BoolInfo('whether background grid is shown on freeform collections');
     _freeform_scale_min?: NUMt = new NumInfo('how far out a view can zoom (used by image/videoBoxes that are clipped');
     _freeform_scale_max?: NUMt = new NumInfo('how far in a view can zoom (used by sidebar freeform views');
@@ -433,6 +391,7 @@ export class DocumentOptions {
     flexGap?: NUMt = new NumInfo('Linear view flex gap');
     flexDirection?: 'unset' | 'row' | 'column' | 'row-reverse' | 'column-reverse';
 
+    link?: string;
     link_description?: string; // added for links
     link_relationship?: string; // type of relatinoship a link represents
     link_displayLine?: BOOLt = new BoolInfo('whether a link line should be dipslayed between the two link anchors');
@@ -528,120 +487,12 @@ export namespace Docs {
         type PrototypeMap = Map<DocumentType, Doc>;
         const defaultDataKey = 'data';
 
-        const TemplateMap: TemplateMap = new Map([
-            [
-                DocumentType.RTF,
-                {
-                    layout: { view: FormattedTextBox, dataField: 'text' },
-                    options: {
-                        acl: '',
-                        _height: 35,
-                        _xMargin: 10,
-                        _yMargin: 10,
-                        layout_nativeDimEditable: true,
-                        layout_reflowVertical: true,
-                        layout_reflowHorizontal: true,
-                        defaultDoubleClick: 'ignore',
-                        systemIcon: 'BsFileEarmarkTextFill',
-                    },
-                },
-            ],
-            [
-                DocumentType.SEARCH,
-                {
-                    layout: { view: SearchBox, dataField: defaultDataKey },
-                    options: { acl: '', _width: 400 },
-                },
-            ],
-            [
-                DocumentType.IMG,
-                {
-                    layout: { view: ImageBox, dataField: defaultDataKey },
-                    options: { acl: '', freeform: '', systemIcon: 'BsFileEarmarkImageFill' },
-                },
-            ],
-            [
-                DocumentType.WEB,
-                {
-                    layout: { view: WebBox, dataField: defaultDataKey },
-                    options: { acl: '', _height: 300, _layout_fitWidth: true, layout_nativeDimEditable: true, layout_reflowVertical: true, waitForDoubleClickToClick: 'always', systemIcon: 'BsGlobe' },
-                },
-            ],
+        export const TemplateMap: TemplateMap = new Map([
             [
-                DocumentType.COL,
-                {
-                    layout: { view: CollectionView, dataField: defaultDataKey },
-                    options: {
-                        acl: '',
-                        _layout_fitWidth: true,
-                        freeform: '',
-                        _freeform_panX: 0,
-                        _freeform_panY: 0,
-                        _freeform_scale: 1,
-                        layout_nativeDimEditable: true,
-                        layout_reflowHorizontal: true,
-                        layout_reflowVertical: true,
-                        systemIcon: 'BsFillCollectionFill',
-                    },
-                },
-            ],
-            [
-                DocumentType.KVP,
-                {
-                    layout: { view: KeyValueBox, dataField: defaultDataKey },
-                    options: { acl: '', _layout_fitWidth: true, _height: 150 },
-                },
-            ],
-            [
-                DocumentType.VID,
-                {
-                    layout: { view: VideoBox, dataField: defaultDataKey },
-                    options: { acl: '', _layout_currentTimecode: 0, systemIcon: 'BsFileEarmarkPlayFill' },
-                },
-            ],
-            [
-                DocumentType.AUDIO,
-                {
-                    layout: { view: AudioBox, dataField: defaultDataKey },
-                    options: { acl: '', _height: 100, layout_fitWidth: true, layout_reflowHorizontal: true, layout_reflowVertical: true, layout_nativeDimEditable: true, systemIcon: 'BsFillVolumeUpFill' },
-                },
-            ],
-            [
-                DocumentType.REC,
-                {
-                    layout: { view: VideoBox, dataField: defaultDataKey },
-                    options: { acl: '', _height: 100, backgroundColor: 'pink', systemIcon: 'BsFillMicFill' },
-                },
-            ],
-            [
-                DocumentType.PDF,
-                {
-                    layout: { view: PDFBox, dataField: defaultDataKey },
-                    options: { acl: '', _layout_curPage: 1, _layout_fitWidth: true, layout_nativeDimEditable: true, layout_reflowVertical: true, systemIcon: 'BsFileEarmarkPdfFill' },
-                },
-            ],
-            [
-                DocumentType.MAP,
-                {
-                    layout: { view: MapBox, dataField: defaultDataKey },
-                    options: { acl: '', map: '', _height: 600, _width: 800, layout_reflowHorizontal: true, layout_reflowVertical: true, layout_nativeDimEditable: true, systemIcon: 'BsFillPinMapFill' },
-                },
-            ],
-            [
-                DocumentType.LINK,
+                DocumentType.GROUPDB,
                 {
-                    layout: { view: LinkBox, dataField: 'link' },
-                    options: {
-                        acl: '',
-                        childDontRegisterViews: true,
-                        layout_hideLinkAnchors: true,
-                        _height: 1,
-                        _width: 1,
-                        link: '',
-                        link_description: '',
-                        color: 'lightBlue', // lightblue is default color for linking dot and link documents text comment area
-                        _dropPropertiesToRemove: new List(['onClick']),
-                    },
+                    layout: { view: EmptyBox, dataField: defaultDataKey },
+                    options: { acl: '', title: 'Global Group Database' },
                 },
             ],
             [
@@ -652,175 +503,18 @@ export namespace Docs {
                     options: { acl: '', title: 'Global Script Database' },
                 },
             ],
-            [
-                DocumentType.SCRIPTING,
-                {
-                    layout: { view: ScriptingBox, dataField: defaultDataKey },
-                    options: { acl: '', systemIcon: 'BsFileEarmarkCodeFill' },
-                },
-            ],
-            [
-                DocumentType.LABEL,
-                {
-                    layout: { view: LabelBox, dataField: 'title' },
-                    options: { acl: '', _singleLine: true, layout_nativeDimEditable: true, layout_reflowHorizontal: true, layout_reflowVertical: true },
-                },
-            ],
-            [
-                DocumentType.EQUATION,
-                {
-                    layout: { view: EquationBox, dataField: 'text' },
-                    options: { acl: '', fontSize: '14px', layout_reflowHorizontal: true, layout_reflowVertical: true, layout_nativeDimEditable: true, layout_hideDecorationTitle: true, systemIcon: 'BsCalculatorFill' }, // systemIcon: 'BsSuperscript' + BsSubscript
-                },
-            ],
-            [
-                DocumentType.FUNCPLOT,
-                {
-                    layout: { view: FunctionPlotBox, dataField: defaultDataKey },
-                    options: { acl: '', layout_reflowHorizontal: true, layout_reflowVertical: true, layout_nativeDimEditable: true },
-                },
-            ],
-            [
-                DocumentType.BUTTON,
-                {
-                    layout: { view: LabelBox, dataField: 'title' },
-                    options: { acl: '', layout_nativeDimEditable: true, layout_reflowHorizontal: true, layout_reflowVertical: true },
-                },
-            ],
-            [
-                DocumentType.PRES,
-                {
-                    layout: { view: PresBox, dataField: defaultDataKey },
-                    options: { acl: '', defaultDoubleClick: 'ignore', hideClickBehaviors: true, layout_hideLinkAnchors: true },
-                },
-            ],
-            [
-                DocumentType.FONTICON,
-                {
-                    layout: { view: FontIconBox, dataField: 'icon' },
-                    options: { acl: '', defaultDoubleClick: 'ignore', waitForDoubleClickToClick: 'never', layout_hideContextMenu: true, layout_hideLinkButton: true, _width: 40, _height: 40 },
-                },
-            ],
-            [
-                DocumentType.WEBCAM,
-                {
-                    layout: { view: RecordingBox, dataField: defaultDataKey },
-                    options: { acl: '', systemIcon: 'BsFillCameraVideoFill' },
-                },
-            ],
-            [
-                DocumentType.PRESELEMENT,
-                {
-                    layout: { view: PresElementBox, dataField: defaultDataKey },
-                    options: { acl: '', title: 'pres element template', _layout_fitWidth: true, _xMargin: 0, isTemplateDoc: true, isTemplateForField: 'data' },
-                },
-            ],
+
             [
                 DocumentType.CONFIG,
-                {
-                    layout: { view: CollectionView, dataField: defaultDataKey },
-                    options: { acl: '', config: '', layout_hideLinkButton: true, layout_unrendered: true },
-                },
-            ],
-            [
-                DocumentType.INK,
-                {
-                    // NOTE: this is unused!! ink fields are filled in directly within the InkDocument() method
-                    layout: { view: InkingStroke, dataField: 'stroke' },
-                    options: {
-                        acl: '',
-                        systemIcon: 'BsFillPencilFill', //
-                        layout_nativeDimEditable: true,
-                        layout_reflowVertical: true,
-                        layout_reflowHorizontal: true,
-                        layout_hideDecorationTitle: true, // don't show title when selected
-                        fitWidth: false,
-                        layout_isSvg: true,
-                    },
-                },
-            ],
-            [
-                DocumentType.SCREENSHOT,
-                {
-                    layout: { view: ScreenshotBox, dataField: defaultDataKey },
-                    options: { acl: '', layout_nativeDimEditable: true, systemIcon: 'BsCameraFill' },
-                },
-            ],
-            [
-                DocumentType.COMPARISON,
-                {
-                    data: '',
-                    layout: { view: ComparisonBox, dataField: defaultDataKey },
-                    options: {
-                        acl: '',
-                        backgroundColor: 'gray',
-                        dropAction: dropActionType.move,
-                        waitForDoubleClickToClick: 'always',
-                        layout_reflowHorizontal: true,
-                        layout_reflowVertical: true,
-                        layout_nativeDimEditable: true,
-                        systemIcon: 'BsLayoutSplit',
-                    },
-                },
-            ],
-            [
-                DocumentType.GROUPDB,
                 {
                     layout: { view: EmptyBox, dataField: defaultDataKey },
-                    options: { acl: '', title: 'Global Group Database' },
-                },
-            ],
-            [
-                DocumentType.DATAVIZ,
-                {
-                    layout: { view: DataVizBox, dataField: defaultDataKey },
-                    options: {
-                        acl: '',
-                        dataViz_title: '',
-                        dataViz_line: '',
-                        dataViz_pie: '',
-                        dataViz_histogram: '',
-                        dataViz: 'table',
-                        _layout_fitWidth: true,
-                        layout_reflowHorizontal: true,
-                        layout_reflowVertical: true,
-                        layout_nativeDimEditable: true,
-                    },
-                },
-            ],
-            [
-                DocumentType.LOADING,
-                {
-                    layout: { view: LoadingBox, dataField: '' },
-                    options: { acl: '', _layout_fitWidth: true, _fitHeight: true, layout_nativeDimEditable: true },
-                },
-            ],
-            [
-                DocumentType.SIMULATION,
-                {
-                    data: '',
-                    layout: { view: PhysicsSimulationBox, dataField: defaultDataKey, _width: 1000, _height: 800 },
-                    options: { acl: '', _height: 100, mass1: '', mass2: '', layout_nativeDimEditable: true, position: '', acceleration: '', pendulum: '', spring: '', wedge: '', simulation: '', review: '', systemIcon: 'BsShareFill' },
-                },
-            ],
-            [
-                DocumentType.PUSHPIN,
-                {
-                    layout: { view: MapPushpinBox, dataField: defaultDataKey },
-                    options: { acl: '' },
+                    options: { acl: '', config: '', layout_hideLinkButton: true, layout_unrendered: true },
                 },
             ],
             [
                 DocumentType.MAPROUTE,
                 {
-                    layout: { view: CollectionView, dataField: defaultDataKey },
-                    options: { acl: '' },
-                },
-            ],
-            [
-                DocumentType.CALENDAR,
-                {
-                    layout: { view: CalendarBox, dataField: defaultDataKey },
+                    layout: { view: EmptyBox, dataField: defaultDataKey },
                     options: { acl: '' },
                 },
             ],
@@ -1005,7 +699,7 @@ export namespace Docs {
                 }
                 Doc.assign(viewDoc, viewProps, true, true);
                 if (![DocumentType.LINK, DocumentType.CONFIG, DocumentType.LABEL].includes(viewDoc.type as any)) {
-                    DocUtils.MakeLinkToActiveAudio(() => viewDoc);
+                    CreateLinkToActiveAudio(() => viewDoc);
                 }
                 updateCachedAcls(dataDoc);
                 updateCachedAcls(viewDoc);
@@ -1039,7 +733,7 @@ export namespace Docs {
          */
         // eslint-disable-next-line default-param-last
         export function ScriptingDocument(script: Opt<ScriptField> | null, options: DocumentOptions = {}, fieldKey?: string) {
-            return InstanceFromProto(Prototypes.get(DocumentType.SCRIPTING), script || undefined, { ...options, layout: fieldKey ? ScriptingBox.LayoutString(fieldKey) : undefined });
+            return InstanceFromProto(Prototypes.get(DocumentType.SCRIPTING), script || undefined, { ...options, layout: fieldKey ? `<ScriptingBox {...props} fieldKey={'${fieldKey}'}/>` /* ScriptingBox.LayoutString(fieldKey) */ : undefined });
         }
 
         // eslint-disable-next-line default-param-last
@@ -1309,34 +1003,6 @@ export namespace Docs {
             return ret;
         }
 
-        export type DocConfig = {
-            doc: Doc;
-            initialWidth?: number;
-            path?: Doc[];
-        };
-
-        export function StandardCollectionDockingDocument(configs: Array<DocConfig>, options: DocumentOptions, id?: string, type: string = 'row') {
-            const layoutConfig = {
-                content: [
-                    {
-                        type: type,
-                        content: [...configs.map(config => CollectionDockingView.makeDocumentConfig(config.doc, undefined, config.initialWidth))],
-                    },
-                ],
-            };
-            const doc = DockDocument(
-                configs.map(c => c.doc),
-                JSON.stringify(layoutConfig),
-                ClientUtils.CurrentUserEmail() === 'guest' ? options : { acl_Guest: SharingPermissions.View, ...options },
-                id
-            );
-            configs.forEach(c => {
-                Doc.SetContainer(c.doc, doc);
-                inheritParentAcls(doc, c.doc, false);
-            });
-            return doc;
-        }
-
         export function DelegateDocument(proto: Doc, options: DocumentOptions = {}) {
             return InstanceFromProto(proto, undefined, options);
         }
@@ -1346,879 +1012,3 @@ export namespace Docs {
         }
     }
 }
-
-export namespace DocUtils {
-    function matchFieldValue(doc: Doc, key: string, valueIn: any): boolean {
-        let value = valueIn;
-        const hasFunctionFilter = ClientUtils.HasFunctionFilter(value);
-        if (hasFunctionFilter) {
-            return hasFunctionFilter(StrCast(doc[key]));
-        }
-        if (key === LinkedTo) {
-            // links are not a field value, so handled here.  value is an expression of form ([field=]idToDoc("..."))
-            const allLinks = LinkManager.Instance.getAllRelatedLinks(doc);
-            const matchLink = (val: string, anchor: Doc) => {
-                const linkedToExp = (val ?? '').split('=');
-                if (linkedToExp.length === 1) return Field.toScriptString(anchor) === val;
-                return Field.toScriptString(DocCast(anchor[linkedToExp[0]])) === linkedToExp[1];
-            };
-            // prettier-ignore
-            return  (value === Doc.FilterNone && !allLinks.length) || 
-                    (value === Doc.FilterAny && !!allLinks.length) ||
-                    (allLinks.some(link => matchLink(value,DocCast(link.link_anchor_1)) || 
-                                           matchLink(value,DocCast(link.link_anchor_2)) ));
-        }
-        if (typeof value === 'string') {
-            value = value.replace(`,${ClientUtils.noRecursionHack}`, '');
-        }
-        const fieldVal = doc[key];
-        // prettier-ignore
-        if ((value === Doc.FilterAny && fieldVal !== undefined) || 
-            (value === Doc.FilterNone && fieldVal === undefined)) {
-            return true;
-        }
-        const vals = StrListCast(fieldVal); // list typing is very imperfect.  casting to a string list doesn't mean that the entries will actually be strings
-        if (vals.length) {
-            return vals.some(v => typeof v === 'string' && v.includes(value)); // bcz: arghh: Todo: comparison should be parameterized as exact, or substring
-        }
-        return Field.toString(fieldVal as FieldType).includes(value); // bcz: arghh: Todo: comparison should be parameterized as exact, or substring
-    }
-    /**
-     * @param docs
-     * @param childFilters
-     * @param childFiltersByRanges
-     * @param parentCollection
-     * Given a list of docs and childFilters, @returns the list of Docs that match those filters
-     */
-    export function FilterDocs(childDocs: Doc[], childFilters: string[], childFiltersByRanges: string[], parentCollection?: Doc) {
-        if (!childFilters?.length && !childFiltersByRanges?.length) {
-            return childDocs.filter(d => !d.cookies); // remove documents that need a cookie if there are no filters to provide one
-        }
-
-        const filterFacets: { [key: string]: { [value: string]: string } } = {}; // maps each filter key to an object with value=>modifier fields
-        childFilters.forEach(filter => {
-            const fields = filter.split(Doc.FilterSep);
-            const key = fields[0];
-            const value = fields[1];
-            const modifiers = fields[2];
-            if (!filterFacets[key]) {
-                filterFacets[key] = {};
-            }
-            filterFacets[key][value] = modifiers;
-        });
-
-        const filteredDocs = childFilters.length
-            ? childDocs.filter(d => {
-                  if (d.z) return true;
-                  // if the document needs a cookie but no filter provides the cookie, then the document does not pass the filter
-                  if (d.cookies && (!filterFacets.cookies || !Object.keys(filterFacets.cookies).some(key => d.cookies === key))) {
-                      return false;
-                  }
-                  const facetKeys = Object.keys(filterFacets).filter(fkey => fkey !== 'cookies' && fkey !== ClientUtils.noDragDocsFilter.split(Doc.FilterSep)[0]);
-                  // eslint-disable-next-line no-restricted-syntax
-                  for (const facetKey of facetKeys) {
-                      const facet = filterFacets[facetKey];
-
-                      // facets that match some value in the field of the document (e.g. some text field)
-                      const matches = Object.keys(facet).filter(value => value !== 'cookies' && facet[value] === 'match');
-
-                      // facets that have a check next to them
-                      const checks = Object.keys(facet).filter(value => facet[value] === 'check');
-
-                      // metadata facets that exist
-                      const exists = Object.keys(facet).filter(value => facet[value] === 'exists');
-
-                      // facets that unset metadata (a hack for making cookies work)
-                      const unsets = Object.keys(facet).filter(value => facet[value] === 'unset');
-
-                      // facets that specify that a field must not match a specific value
-                      const xs = Object.keys(facet).filter(value => facet[value] === 'x');
-
-                      if (!unsets.length && !exists.length && !xs.length && !checks.length && !matches.length) return true;
-                      const failsNotEqualFacets = !xs.length ? false : xs.some(value => matchFieldValue(d, facetKey, value));
-                      const satisfiesCheckFacets = !checks.length ? true : checks.some(value => matchFieldValue(d, facetKey, value));
-                      const satisfiesExistsFacets = !exists.length ? true : facetKey !== LinkedTo ? d[facetKey] !== undefined : LinkManager.Instance.getAllRelatedLinks(d).length;
-                      const satisfiesUnsetsFacets = !unsets.length ? true : d[facetKey] === undefined;
-                      const satisfiesMatchFacets = !matches.length
-                          ? true
-                          : matches.some(value => {
-                                if (facetKey.startsWith('*')) {
-                                    //  fields starting with a '*' are used to match families of related fields.  ie, *modificationDate will match text_modificationDate, data_modificationDate, etc
-                                    const allKeys = Array.from(Object.keys(d));
-                                    allKeys.push(...Object.keys(Doc.GetProto(d)));
-                                    const keys = allKeys.filter(key => key.includes(facetKey.substring(1)));
-                                    return keys.some(key => Field.toString(d[key] as FieldType).includes(value));
-                                }
-                                return Field.toString(d[facetKey] as FieldType).includes(value);
-                            });
-                      // if we're ORing them together, the default return is false, and we return true for a doc if it satisfies any one set of criteria
-                      if (parentCollection?.childFilters_boolean === 'OR') {
-                          if (satisfiesUnsetsFacets && satisfiesExistsFacets && satisfiesCheckFacets && !failsNotEqualFacets && satisfiesMatchFacets) return true;
-                      }
-                      // if we're ANDing them together, the default return is true, and we return false for a doc if it doesn't satisfy any set of criteria
-                      else if (!satisfiesUnsetsFacets || !satisfiesExistsFacets || !satisfiesCheckFacets || failsNotEqualFacets || (matches.length && !satisfiesMatchFacets)) return false;
-                  }
-                  return parentCollection?.childFilters_boolean !== 'OR';
-              })
-            : childDocs;
-        const rangeFilteredDocs = filteredDocs.filter(d => {
-            for (let i = 0; i < childFiltersByRanges.length; i += 3) {
-                const key = childFiltersByRanges[i];
-                const min = Number(childFiltersByRanges[i + 1]);
-                const max = Number(childFiltersByRanges[i + 2]);
-                const val = typeof d[key] === 'string' ? (Number(StrCast(d[key])).toString() === StrCast(d[key]) ? Number(StrCast(d[key])) : undefined) : Cast(d[key], 'number', null);
-                if (val === undefined) {
-                    // console.log("Should 'undefined' pass range filter or not?")
-                } else if (val < min || val > max) return false;
-            }
-            return true;
-        });
-        return rangeFilteredDocs;
-    }
-
-    export const ActiveRecordings: { props: FieldViewProps; getAnchor: (addAsAnnotation: boolean) => Doc }[] = [];
-
-    export function MakeLinkToActiveAudio(getSourceDoc: () => Doc | undefined, broadcastEvent = true) {
-        broadcastEvent && runInAction(() => { Doc.RecordingEvent += 1; }); // prettier-ignore
-        return DocUtils.ActiveRecordings.map(audio => {
-            const sourceDoc = getSourceDoc();
-            return sourceDoc && DocUtils.MakeLink(sourceDoc, audio.getAnchor(true) || audio.props.Document, { link_displayLine: false, link_relationship: 'recording annotation:linked recording', link_description: 'recording timeline' });
-        });
-    }
-
-    export function MakeLink(source: Doc, target: Doc, linkSettings: { link_relationship?: string; link_description?: string; link_displayLine?: boolean }, id?: string, showPopup?: number[]) {
-        if (!linkSettings.link_relationship) linkSettings.link_relationship = target.type === DocumentType.RTF ? 'Commentary:Comments On' : 'link';
-        if (target.doc === Doc.UserDoc()) return undefined;
-
-        const makeLink = action((linkDoc: Doc, showAt?: number[]) => {
-            if (showAt) {
-                LinkManager.Instance.currentLink = linkDoc;
-
-                TaskCompletionBox.textDisplayed = 'Link Created';
-                TaskCompletionBox.popupX = showAt[0];
-                TaskCompletionBox.popupY = showAt[1] - 33;
-                TaskCompletionBox.taskCompleted = true;
-
-                LinkDescriptionPopup.Instance.popupX = showAt[0];
-                LinkDescriptionPopup.Instance.popupY = showAt[1];
-                LinkDescriptionPopup.Instance.display = true;
-
-                const rect = document.body.getBoundingClientRect();
-                if (LinkDescriptionPopup.Instance.popupX + 200 > rect.width) {
-                    LinkDescriptionPopup.Instance.popupX -= 190;
-                    TaskCompletionBox.popupX -= 40;
-                }
-                if (LinkDescriptionPopup.Instance.popupY + 100 > rect.height) {
-                    LinkDescriptionPopup.Instance.popupY -= 40;
-                    TaskCompletionBox.popupY -= 40;
-                }
-
-                setTimeout(
-                    action(() => {
-                        TaskCompletionBox.taskCompleted = false;
-                    }),
-                    2500
-                );
-            }
-            return linkDoc;
-        });
-
-        const a = source.layout_unrendered ? 'link_anchor_1?.annotationOn' : 'link_anchor_1';
-        const b = target.layout_unrendered ? 'link_anchor_2?.annotationOn' : 'link_anchor_2';
-
-        return makeLink(
-            Docs.Create.LinkDocument(
-                source,
-                target,
-                {
-                    acl_Guest: SharingPermissions.Augment,
-                    _acl_Guest: SharingPermissions.Augment,
-                    title: ComputedField.MakeFunction('generateLinkTitle(this)') as any,
-                    link_anchor_1_useSmallAnchor: source.useSmallAnchor ? true : undefined,
-                    link_anchor_2_useSmallAnchor: target.useSmallAnchor ? true : undefined,
-                    link_displayLine: linkSettings.link_displayLine,
-                    link_relationship: linkSettings.link_relationship,
-                    link_description: linkSettings.link_description,
-                    x: ComputedField.MakeFunction(`((this.${a}?.x||0)+(this.${b}?.x||0))/2`) as any,
-                    y: ComputedField.MakeFunction(`((this.${a}?.y||0)+(this.${b}?.y||0))/2`) as any,
-                    link_autoMoveAnchors: true,
-                    _lockedPosition: true,
-                    _layout_showCaption: '', // removed since they conflict with showing a link with a LinkBox (ie, line, not comparison box)
-                    _layout_showTitle: '',
-                    // _layout_showCaption: 'link_description',
-                    // _layout_showTitle: 'link_relationship',
-                },
-                id
-            ),
-            showPopup
-        );
-    }
-
-    export function AssignScripts(doc: Doc, scripts?: { [key: string]: string | undefined }, funcs?: { [key: string]: string }) {
-        scripts &&
-            Object.keys(scripts).forEach(key => {
-                const script = scripts[key];
-                if (ScriptCast(doc[key])?.script.originalScript !== scripts[key] && script) {
-                    (key.startsWith('_') ? doc : Doc.GetProto(doc))[key] = ScriptField.MakeScript(script, {
-                        self: Doc.name,
-                        this: Doc.name,
-                        dragData: DragManager.DocumentDragData.name,
-                        value: 'any',
-                        _readOnly_: 'boolean',
-                        scriptContext: 'any',
-                        documentView: Doc.name,
-                        heading: Doc.name,
-                        checked: 'boolean',
-                        containingTreeView: Doc.name,
-                        altKey: 'boolean',
-                        ctrlKey: 'boolean',
-                        shiftKey: 'boolean',
-                    });
-                }
-            });
-        funcs &&
-            Object.keys(funcs)
-                .filter(key => !key.endsWith('-setter'))
-                .forEach(key => {
-                    const cfield = ComputedField.WithoutComputed(() => FieldValue(doc[key]));
-                    if (ScriptCast(cfield)?.script.originalScript !== funcs[key]) {
-                        const setFunc = Cast(funcs[key + '-setter'], 'string', null);
-                        (key.startsWith('_') ? doc : Doc.GetProto(doc))[key] = funcs[key] ? ComputedField.MakeFunction(funcs[key], { dragData: DragManager.DocumentDragData.name }, { _readOnly_: true }, setFunc) : undefined;
-                    }
-                });
-        return doc;
-    }
-    export function AssignOpts(doc: Doc | undefined, reqdOpts: DocumentOptions, items?: Doc[]) {
-        if (doc) {
-            const compareValues = (val1: any, val2: any) => {
-                if (val1 instanceof List && val2 instanceof List && val1.length === val2.length) {
-                    return !val1.some(v => !val2.includes(v)) || !val2.some(v => val1.includes(v));
-                }
-                return val1 === val2;
-            };
-            Object.entries(reqdOpts).forEach(pair => {
-                const targetDoc = pair[0].startsWith('_') ? doc : Doc.GetProto(doc as Doc);
-                if (!Object.getOwnPropertyNames(targetDoc).includes(pair[0].replace(/^_/, '')) || !compareValues(pair[1], targetDoc[pair[0]])) {
-                    targetDoc[pair[0]] = pair[1];
-                }
-            });
-            items?.forEach(item => !DocListCast(doc.data).includes(item) && Doc.AddDocToList(Doc.GetProto(doc), 'data', item));
-            items && DocListCast(doc.data).forEach(item => Doc.IsSystem(item) && !items.includes(item) && Doc.RemoveDocFromList(Doc.GetProto(doc), 'data', item));
-        }
-        return doc;
-    }
-    export function AssignDocField(doc: Doc, field: string, creator: (reqdOpts: DocumentOptions, items?: Doc[]) => Doc, reqdOpts: DocumentOptions, items?: Doc[], scripts?: { [key: string]: string }, funcs?: { [key: string]: string }) {
-        // eslint-disable-next-line no-return-assign
-        return DocUtils.AssignScripts(DocUtils.AssignOpts(DocCast(doc[field]), reqdOpts, items) ?? (doc[field] = creator(reqdOpts, items)), scripts, funcs);
-    }
-
-    export function DocumentFromField(target: Doc, fieldKey: string, proto?: Doc, options?: DocumentOptions): Doc | undefined {
-        let created: Doc | undefined;
-        const field = target[fieldKey];
-        const resolved = options ?? {};
-        if (field instanceof ImageField) {
-            created = Docs.Create.ImageDocument(field.url.href, resolved);
-            created.layout = ImageBox.LayoutString(fieldKey);
-        } else if (field instanceof Doc) {
-            created = field;
-        } else if (field instanceof VideoField) {
-            created = Docs.Create.VideoDocument(field.url.href, resolved);
-            created.layout = VideoBox.LayoutString(fieldKey);
-        } else if (field instanceof PdfField) {
-            created = Docs.Create.PdfDocument(field.url.href, resolved);
-            created.layout = PDFBox.LayoutString(fieldKey);
-        } else if (field instanceof AudioField) {
-            created = Docs.Create.AudioDocument(field.url.href, resolved);
-            created.layout = AudioBox.LayoutString(fieldKey);
-        } else if (field instanceof InkField) {
-            created = Docs.Create.InkDocument(field.inkData, resolved);
-            created.layout = InkingStroke.LayoutString(fieldKey);
-        } else if (field instanceof List && field[0] instanceof Doc) {
-            created = Docs.Create.StackingDocument(DocListCast(field), resolved);
-            created.layout = CollectionView.LayoutString(fieldKey);
-        } else {
-            created = Docs.Create.TextDocument('', { ...{ _width: 200, _height: 25, _layout_autoHeight: true }, ...resolved });
-            created.layout = FormattedTextBox.LayoutString(fieldKey);
-        }
-        if (created) {
-            created.title = fieldKey;
-            proto && created.proto && (created.proto = Doc.GetProto(proto));
-        }
-        return created;
-    }
-
-    /**
-     *
-     * @param type the type of file.
-     * @param path the path to the file.
-     * @param options the document options.
-     * @param overwriteDoc the placeholder loading doc.
-     * @returns
-     */
-    export async function DocumentFromType(type: string, path: string, options: DocumentOptions, overwriteDoc?: Doc): Promise<Opt<Doc>> {
-        let ctor: ((path: string, options: DocumentOptions, overwriteDoc?: Doc) => Doc | Promise<Doc | undefined>) | undefined;
-        if (type.indexOf('image') !== -1) {
-            ctor = Docs.Create.ImageDocument;
-            if (!options._width) options._width = 300;
-        }
-        if (type.indexOf('video') !== -1) {
-            ctor = Docs.Create.VideoDocument;
-            if (!options._width) options._width = 600;
-            if (!options._height) options._height = ((options._width as number) * 2) / 3;
-        }
-        if (type.indexOf('audio') !== -1) {
-            ctor = Docs.Create.AudioDocument;
-        }
-        if (type.indexOf('pdf') !== -1) {
-            ctor = Docs.Create.PdfDocument;
-            if (!options._width) options._width = 400;
-            if (!options._height) options._height = ((options._width as number) * 1200) / 927;
-        }
-        if (type.indexOf('csv') !== -1) {
-            ctor = Docs.Create.DataVizDocument;
-            if (!options._width) options._width = 400;
-            if (!options._height) options._height = ((options._width as number) * 1200) / 927;
-        }
-        // TODO:al+glr
-        // if (type.indexOf("map") !== -1) {
-        //     ctor = Docs.Create.MapDocument;
-        //     if (!options._width) options._width = 800;
-        //     if (!options._height) options._height = (options._width as number) * 3 / 4;
-        // }
-        if (type.indexOf('html') !== -1) {
-            if (path.includes(window.location.hostname)) {
-                const s = path.split('/');
-                const id = s[s.length - 1];
-                return DocServer.GetRefField(id).then(field => {
-                    if (field instanceof Doc) {
-                        const embedding = Doc.MakeEmbedding(field);
-                        embedding.x = (options.x as number) || 0;
-                        embedding.y = (options.y as number) || 0;
-                        embedding._width = (options._width as number) || 300;
-                        embedding._height = (options._height as number) || (options._width as number) || 300;
-                        return embedding;
-                    }
-                    return undefined;
-                });
-            }
-            ctor = Docs.Create.WebDocument;
-            // eslint-disable-next-line no-param-reassign
-            options = { ...options, _width: 400, _height: 512, title: path };
-        }
-
-        return ctor ? ctor(path, overwriteDoc ? { ...options, title: StrCast(overwriteDoc.title, path) } : options, overwriteDoc) : undefined;
-    }
-
-    export function addDocumentCreatorMenuItems(docTextAdder: (d: Doc) => void, docAdder: (d: Doc) => void, x: number, y: number, simpleMenu: boolean = false, pivotField?: string, pivotValue?: string): void {
-        const documentList: ContextMenuProps[] = DocListCast(DocListCast(Doc.MyTools?.data)[0]?.data)
-            .filter(btnDoc => !btnDoc.hidden)
-            .map(btnDoc => Cast(btnDoc?.dragFactory, Doc, null))
-            .filter(doc => doc && doc !== Doc.UserDoc().emptyTrail && doc.title)
-            .map(dragDoc => ({
-                description: ':' + StrCast(dragDoc.title).replace('Untitled ', ''),
-                event: undoable(() => {
-                    const newDoc = DocUtils.copyDragFactory(dragDoc);
-                    if (newDoc) {
-                        newDoc.author = ClientUtils.CurrentUserEmail();
-                        newDoc.x = x;
-                        newDoc.y = y;
-                        EquationBox.SelectOnLoad = newDoc[Id];
-                        if (newDoc.type === DocumentType.RTF) FormattedTextBox.SetSelectOnLoad(newDoc);
-                        if (pivotField) {
-                            newDoc[pivotField] = pivotValue;
-                        }
-                        docAdder?.(newDoc);
-                    }
-                }, StrCast(dragDoc.title)),
-                icon: Doc.toIcon(dragDoc),
-            })) as ContextMenuProps[];
-        ContextMenu.Instance.addItem({
-            description: 'Create document',
-            subitems: documentList,
-            icon: 'file',
-        });
-        !simpleMenu &&
-            ContextMenu.Instance.addItem({
-                description: 'Styled Notes',
-                subitems: DocListCast((Doc.UserDoc().template_notes as Doc).data).map(note => ({
-                    description: ':' + StrCast(note.title),
-                    event: undoable(() => {
-                        const textDoc = Docs.Create.TextDocument('', {
-                            _width: 200,
-                            x,
-                            y,
-                            _layout_autoHeight: note._layout_autoHeight !== false,
-                            title: StrCast(note.title) + '#' + (note.embeddingCount = NumCast(note.embeddingCount) + 1),
-                        });
-                        textDoc.layout_fieldKey = 'layout_' + note.title;
-                        textDoc[textDoc.layout_fieldKey] = note;
-                        if (pivotField) {
-                            textDoc[pivotField] = pivotValue;
-                        }
-                        docTextAdder(textDoc);
-                    }, 'create quick note'),
-                    icon: StrCast(note.icon) as IconProp,
-                })) as ContextMenuProps[],
-                icon: 'sticky-note',
-            });
-        const userDocList: ContextMenuProps[] = DocListCast(DocListCast(Doc.MyTools?.data)[1]?.data)
-            .filter(btnDoc => !btnDoc.hidden)
-            .map(btnDoc => Cast(btnDoc?.dragFactory, Doc, null))
-            .filter(doc => doc && doc !== Doc.UserDoc().emptyTrail && doc !== Doc.UserDoc().emptyNote && doc.title)
-            .map(dragDoc => ({
-                description: ':' + StrCast(dragDoc.title).replace('Untitled ', ''),
-                event: undoable(() => {
-                    const newDoc = DocUtils.delegateDragFactory(dragDoc);
-                    if (newDoc) {
-                        newDoc.author = ClientUtils.CurrentUserEmail();
-                        newDoc.x = x;
-                        newDoc.y = y;
-                        EquationBox.SelectOnLoad = newDoc[Id];
-                        if (newDoc.type === DocumentType.RTF) FormattedTextBox.SetSelectOnLoad(newDoc);
-                        if (pivotField) {
-                            newDoc[pivotField] = pivotValue;
-                        }
-                        docAdder?.(newDoc);
-                    }
-                }, StrCast(dragDoc.title)),
-                icon: Doc.toIcon(dragDoc),
-            })) as ContextMenuProps[];
-        ContextMenu.Instance.addItem({
-            description: 'User Templates',
-            subitems: userDocList,
-            icon: 'file',
-        });
-    }
-
-    // applies a custom template to a document.  the template is identified by it's short name (e.g, slideView not layout_slideView)
-
-    /**
-     * Applies a template to a Doc and logs the action with the UndoManager
-     * If the template already exists and has been registered, it can be specified by it's signature name (e.g., 'icon' not 'layout_icon').
-     * Alternatively, the signature can be omitted and the template can be provided.
-     * @param doc the Doc to apply the template to.
-     * @param creator a function that will create the template if it doesn't exist
-     * @param templateSignature the signature name for a template that has already been created and registered on the userDoc.  (can be "" if template is provide)
-     * @param template the template to use (optional if templateSignature is provided)
-     * @returns doc
-     */
-    export function makeCustomViewClicked(doc: Doc, creator: Opt<(documents: Array<Doc>, options: DocumentOptions, id?: string) => Doc>, templateSignature: string = 'custom', template?: Doc) {
-        const batch = UndoManager.StartBatch('makeCustomViewClicked');
-        createCustomView(doc, creator, templateSignature || StrCast(template?.title), template);
-        batch.end();
-        return doc;
-    }
-    export function findTemplate(templateName: string, type: string) {
-        let docLayoutTemplate: Opt<Doc>;
-        const iconViews = DocListCast(Cast(Doc.UserDoc().template_icons, Doc, null)?.data);
-        const templBtns = DocListCast(Cast(Doc.UserDoc().template_buttons, Doc, null)?.data);
-        const noteTypes = DocListCast(Cast(Doc.UserDoc().template_notes, Doc, null)?.data);
-        const userTypes = DocListCast(Cast(Doc.UserDoc().template_user, Doc, null)?.data);
-        const clickFuncs = DocListCast(Cast(Doc.UserDoc().template_clickFuncs, Doc, null)?.data);
-        const allTemplates = iconViews
-            .concat(templBtns)
-            .concat(noteTypes)
-            .concat(userTypes)
-            .concat(clickFuncs)
-            .map(btnDoc => (btnDoc.dragFactory as Doc) || btnDoc)
-            .filter(doc => doc.isTemplateDoc);
-        // bcz: this is hacky -- want to have different templates be applied depending on the "type" of a document.  but type is not reliable and there could be other types of template searches so this should be generalized
-        // first try to find a template that matches the specific document type (<typeName>_<templateName>).  otherwise, fallback to a general match on <templateName>
-        !docLayoutTemplate &&
-            allTemplates.forEach(tempDoc => {
-                StrCast(tempDoc.title) === templateName + '_' + type && (docLayoutTemplate = tempDoc);
-            });
-        !docLayoutTemplate &&
-            allTemplates.forEach(tempDoc => {
-                StrCast(tempDoc.title) === templateName && (docLayoutTemplate = tempDoc);
-            });
-        return docLayoutTemplate;
-    }
-    export function createCustomView(doc: Doc, creator: Opt<(documents: Array<Doc>, options: DocumentOptions, id?: string) => Doc>, templateSignature: string = 'custom', docLayoutTemplate?: Doc) {
-        const templateName = templateSignature.replace(/\(.*\)/, '');
-        doc.layout_fieldKey = 'layout_' + (templateSignature || (docLayoutTemplate?.title ?? ''));
-        // eslint-disable-next-line no-param-reassign
-        docLayoutTemplate = docLayoutTemplate || findTemplate(templateName, StrCast(doc.isGroup && doc.transcription ? 'transcription' : doc.type));
-
-        const customName = 'layout_' + templateSignature;
-        const _width = NumCast(doc._width);
-        const _height = NumCast(doc._height);
-        const options = { title: 'data', backgroundColor: StrCast(doc.backgroundColor), _layout_autoHeight: true, _width, x: -_width / 2, y: -_height / 2, _layout_showSidebar: false };
-
-        if (docLayoutTemplate) {
-            if (docLayoutTemplate !== doc[customName]) {
-                Doc.ApplyTemplateTo(docLayoutTemplate, doc, customName, undefined);
-            }
-        } else {
-            let fieldTemplate: Opt<Doc>;
-            if (doc.data instanceof RichTextField || typeof doc.data === 'string') {
-                fieldTemplate = Docs.Create.TextDocument('', options);
-            } else if (doc.data instanceof PdfField) {
-                fieldTemplate = Docs.Create.PdfDocument('http://www.msn.com', options);
-            } else if (doc.data instanceof VideoField) {
-                fieldTemplate = Docs.Create.VideoDocument('http://www.cs.brown.edu', options);
-            } else if (doc.data instanceof AudioField) {
-                fieldTemplate = Docs.Create.AudioDocument('http://www.cs.brown.edu', options);
-            } else if (doc.data instanceof ImageField) {
-                fieldTemplate = Docs.Create.ImageDocument('http://www.cs.brown.edu', options);
-            }
-            const docTemplate = creator?.(fieldTemplate ? [fieldTemplate] : [], { title: customName + '(' + doc.title + ')', isTemplateDoc: true, _width: _width + 20, _height: Math.max(100, _height + 45) });
-            fieldTemplate && Doc.MakeMetadataFieldTemplate(fieldTemplate, docTemplate ? Doc.GetProto(docTemplate) : docTemplate);
-            docTemplate && Doc.ApplyTemplateTo(docTemplate, doc, customName, undefined);
-        }
-    }
-    export function makeCustomView(doc: Doc, custom: boolean, layout: string) {
-        Doc.setNativeView(doc);
-        if (custom) {
-            makeCustomViewClicked(doc, Docs.Create.StackingDocument, layout, undefined);
-        }
-    }
-    export function iconify(doc: Doc) {
-        const layoutFieldKey = Cast(doc.layout_fieldKey, 'string', null);
-        DocUtils.makeCustomViewClicked(doc, Docs.Create.StackingDocument, 'icon', undefined);
-        if (layoutFieldKey && layoutFieldKey !== 'layout' && layoutFieldKey !== 'layout_icon') doc.deiconifyLayout = layoutFieldKey.replace('layout_', '');
-    }
-
-    export function pileup(docList: Doc[], x?: number, y?: number, size: number = 55, create: boolean = true) {
-        runInAction(() => {
-            docList.forEach((doc, i) => {
-                const d = doc;
-                DocUtils.iconify(d);
-                d.x = Math.cos((Math.PI * 2 * i) / docList.length) * size - size;
-                d.y = Math.sin((Math.PI * 2 * i) / docList.length) * size - size;
-                d._timecodeToShow = undefined; // bcz: this should be automatic somehow.. along with any other properties that were logically associated with the original collection
-            });
-        });
-        if (create) {
-            const newCollection = Docs.Create.PileDocument(docList, { title: 'pileup', _freeform_noZoom: true, x: (x || 0) - size, y: (y || 0) - size, _width: size * 2, _height: size * 2, dragWhenActive: true, _layout_fitWidth: false });
-            newCollection.x = NumCast(newCollection.x) + NumCast(newCollection._width) / 2 - size;
-            newCollection.y = NumCast(newCollection.y) + NumCast(newCollection._height) / 2 - size;
-            newCollection._width = newCollection._height = size * 2;
-            return newCollection;
-        }
-        return undefined;
-    }
-    export function makeIntoPortal(doc: Doc, layoutDoc: Doc, allLinks: Doc[]) {
-        const portalLink = allLinks.find(d => d.link_anchor_1 === doc && d.link_relationship === 'portal to:portal from');
-        if (!portalLink) {
-            DocUtils.MakeLink(
-                doc,
-                Docs.Create.FreeformDocument([], {
-                    _width: NumCast(layoutDoc._width) + 10,
-                    _height: Math.max(NumCast(layoutDoc._height), NumCast(layoutDoc._width) + 10),
-                    _isLightbox: true,
-                    _layout_fitWidth: true,
-                    title: StrCast(doc.title) + ' [Portal]',
-                }),
-                { link_relationship: 'portal to:portal from' }
-            );
-        }
-        doc.followLinkLocation = OpenWhere.lightbox;
-        doc.onClick = FollowLinkScript();
-    }
-
-    export function LeavePushpin(doc: Doc, annotationField: string) {
-        if (doc.followLinkToggle) return undefined;
-        const context = Cast(doc.embedContainer, Doc, null) ?? Cast(doc.annotationOn, Doc, null);
-        const hasContextAnchor = LinkManager.Links(doc).some(l => (l.link_anchor_2 === doc && Cast(l.link_anchor_1, Doc, null)?.annotationOn === context) || (l.link_anchor_1 === doc && Cast(l.link_anchor_2, Doc, null)?.annotationOn === context));
-        if (context && !hasContextAnchor && (context.type === DocumentType.VID || context.type === DocumentType.WEB || context.type === DocumentType.PDF || context.type === DocumentType.IMG)) {
-            const pushpin = Docs.Create.FontIconDocument({
-                title: '',
-                annotationOn: Cast(doc.annotationOn, Doc, null),
-                followLinkToggle: true,
-                icon: 'map-pin',
-                x: Cast(doc.x, 'number', null),
-                y: Cast(doc.y, 'number', null),
-                backgroundColor: '#ACCEF7',
-                layout_hideAllLinks: true,
-                _width: 15,
-                _height: 15,
-                _xPadding: 0,
-                onClick: FollowLinkScript(),
-                _timecodeToShow: Cast(doc._timecodeToShow, 'number', null),
-            });
-            Doc.AddDocToList(context, annotationField, pushpin);
-            DocUtils.MakeLink(pushpin, doc, { link_relationship: 'pushpin' }, '');
-            doc._timecodeToShow = undefined;
-            return pushpin;
-        }
-        return undefined;
-    }
-
-    // /**
-    //  *
-    //  * @param dms Degree Minute Second format exif gps data
-    //  * @param ref ref that determines negativity of decimal coordinates
-    //  * @returns a decimal format of gps latitude / longitude
-    //  */
-    // function getDecimalfromDMS(dms?: number[], ref?: string) {
-    //     if (dms && ref) {
-    //         let degrees = dms[0] / dms[1];
-    //         let minutes = dms[2] / dms[3] / 60.0;
-    //         let seconds = dms[4] / dms[5] / 3600.0;
-
-    //         if (['S', 'W'].includes(ref)) {
-    //             degrees = -degrees; minutes = -minutes; seconds = -seconds
-    //         }
-    //         return (degrees + minutes + seconds).toFixed(5);
-    //     }
-    // }
-
-    function ConvertDMSToDD(degrees: number, minutes: number, seconds: number, direction: string) {
-        let dd = degrees + minutes / 60 + seconds / (60 * 60);
-        if (direction === 'S' || direction === 'W') {
-            dd *= -1;
-        } // Don't do anything for N or E
-        return dd;
-    }
-
-    export function assignImageInfo(result: Upload.FileInformation, protoIn: Doc) {
-        const proto = protoIn;
-        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}`);
-            return;
-        }
-        const full = { ...options, _width: 400, title: name };
-        const pathname = result.accessPaths.agnostic.client;
-        const doc = await DocUtils.DocumentFromType(type, pathname, full, overwriteDoc);
-        if (doc) {
-            const proto = Doc.GetProto(doc);
-            proto.text = result.rawText;
-            !(result instanceof Error) && DocUtils.assignImageInfo(result, proto);
-            if (Upload.isVideoInformation(result)) {
-                proto.data_duration = result.duration;
-            }
-            if (overwriteDoc) {
-                LoadingBox.removeCurrentlyLoading(overwriteDoc);
-            }
-            generatedDocuments.push(doc);
-        }
-    }
-
-    export function GetNewTextDoc(title: string, x: number, y: number, width?: number, height?: number, annotationOn?: Doc, backgroundColor?: string) {
-        const defaultTextTemplate = DocCast(Doc.UserDoc().defaultTextLayout);
-        const tbox = Docs.Create.TextDocument('', {
-            annotationOn,
-            backgroundColor,
-            x,
-            y,
-            title,
-            ...(defaultTextTemplate
-                ? {} // if the new doc will inherit from a template, don't set any layout fields since that would block the inheritance
-                : {
-                      _width: width || 200,
-                      _height: 35,
-                      _layout_centered: BoolCast(Doc.UserDoc()._layout_centered),
-                      _layout_fitWidth: true,
-                      _layout_autoHeight: true,
-                  }),
-        });
-
-        if (defaultTextTemplate) {
-            tbox.layout_fieldKey = 'layout_' + StrCast(defaultTextTemplate.title);
-            Doc.GetProto(tbox)[StrCast(tbox.layout_fieldKey)] = defaultTextTemplate; // set the text doc's layout to render with the text template
-            tbox[DocData].proto = defaultTextTemplate; // and also set the text doc to inherit from the template (this allows the template to specify default field values)
-        }
-        return tbox;
-    }
-
-    export function uploadYoutubeVideoLoading(videoId: string, options: DocumentOptions, overwriteDoc?: Doc) {
-        const generatedDocuments: Doc[] = [];
-        Networking.UploadYoutubeToServer(videoId, overwriteDoc?.[Id]).then(upfiles => {
-            const {
-                source: { newFilename, mimetype },
-                result,
-            } = upfiles.lastElement();
-            if ((result as any).message) {
-                if (overwriteDoc) {
-                    overwriteDoc.isLoading = false;
-                    overwriteDoc.loadingError = (result as any).message;
-                    LoadingBox.removeCurrentlyLoading(overwriteDoc);
-                }
-            } else newFilename && processFileupload(generatedDocuments, newFilename, mimetype ?? '', result, options, overwriteDoc);
-        });
-    }
-
-    /**
-     * uploadFilesToDocs will take in an array of Files, and creates documents for the
-     * new files.
-     *
-     * @param files an array of files that will be uploaded
-     * @param options options to use while uploading
-     * @returns
-     */
-    export async function uploadFilesToDocs(files: File[], options: DocumentOptions) {
-        const generatedDocuments: Doc[] = [];
-
-        // These files do not have overwriteDocs, so we do not set the guid and let the client generate one.
-        const fileNoGuidPairs: Networking.FileGuidPair[] = files.map(file => ({ file }));
-
-        const upfiles = await Networking.UploadFilesToServer(fileNoGuidPairs);
-        upfiles.forEach(({ source: { newFilename, mimetype }, result }) => {
-            newFilename && mimetype && processFileupload(generatedDocuments, newFilename, mimetype, result, options);
-        });
-        return generatedDocuments;
-    }
-
-    export function uploadFileToDoc(file: File, options: DocumentOptions, overwriteDoc: Doc) {
-        const generatedDocuments: Doc[] = [];
-        // Since this file has an overwriteDoc, we can set the client tracking guid to the overwriteDoc's guid.
-        Networking.UploadFilesToServer([{ file, guid: overwriteDoc[Id] }]).then(upfiles => {
-            const {
-                source: { newFilename, mimetype },
-                result,
-            } = upfiles.lastElement() ?? { source: { newFilename: '', mimetype: '' }, result: { message: 'upload failed' } };
-            if ((result as any).message) {
-                if (overwriteDoc) {
-                    overwriteDoc.loadingError = (result as any).message;
-                    LoadingBox.removeCurrentlyLoading(overwriteDoc);
-                }
-            } else newFilename && mimetype && processFileupload(generatedDocuments, newFilename, mimetype, result, options, overwriteDoc);
-        });
-    }
-
-    // copies the specified drag factory document
-    export function copyDragFactory(dragFactory: Doc) {
-        if (!dragFactory) return undefined;
-        const ndoc = dragFactory.isTemplateDoc ? Doc.ApplyTemplate(dragFactory) : Doc.MakeCopy(dragFactory, true);
-        if (ndoc && dragFactory.dragFactory_count !== undefined) {
-            dragFactory.dragFactory_count = NumCast(dragFactory.dragFactory_count) + 1;
-            Doc.SetInPlace(ndoc, 'title', ndoc.title + ' ' + NumCast(dragFactory.dragFactory_count).toString(), true);
-        }
-
-        return ndoc;
-    }
-    export function delegateDragFactory(dragFactory: Doc) {
-        const ndoc = Doc.MakeDelegateWithProto(dragFactory);
-        if (ndoc && dragFactory.dragFactory_count !== undefined) {
-            dragFactory.dragFactory_count = NumCast(dragFactory.dragFactory_count) + 1;
-            Doc.GetProto(ndoc).title = ndoc.title + ' ' + NumCast(dragFactory.dragFactory_count).toString();
-        }
-        return ndoc;
-    }
-
-    export async function Zip(doc: Doc, zipFilename = 'dashExport.zip') {
-        const { clone, map, linkMap } = await Doc.MakeClone(doc);
-        const proms = new Set<string>();
-        function replacer(key: any, value: any) {
-            if (key && ['branchOf', 'cloneOf', 'cursors'].includes(key)) return undefined;
-            if (value?.__type === 'image') {
-                const extension = value.url.replace(/.*\./, '');
-                proms.add(value.url.replace('.' + extension, '_o.' + extension));
-                return SerializationHelper.Serialize(new ImageField(value.url));
-            }
-            if (value?.__type === 'pdf') {
-                proms.add(value.url);
-                return SerializationHelper.Serialize(new PdfField(value.url));
-            }
-            if (value?.__type === 'audio') {
-                proms.add(value.url);
-                return SerializationHelper.Serialize(new AudioField(value.url));
-            }
-            if (value?.__type === 'video') {
-                proms.add(value.url);
-                return SerializationHelper.Serialize(new VideoField(value.url));
-            }
-            if (
-                value instanceof Doc ||
-                value instanceof ScriptField ||
-                value instanceof RichTextField ||
-                value instanceof InkField ||
-                value instanceof CsvField ||
-                value instanceof WebField ||
-                value instanceof DateField ||
-                value instanceof ProxyField ||
-                value instanceof ComputedField
-            ) {
-                return SerializationHelper.Serialize(value);
-            }
-            if (value instanceof Array && key !== ListFieldName && key !== InkDataFieldName) return { fields: value, __type: 'list' };
-            return value;
-        }
-
-        const docs: { [id: string]: any } = {};
-        const links: { [id: string]: any } = {};
-        Array.from(map.entries()).forEach(f => {
-            docs[f[0]] = f[1];
-        });
-        Array.from(linkMap.entries()).forEach(l => {
-            links[l[0]] = l[1];
-        });
-        const jsonDocs = JSON.stringify({ id: clone[Id], docs, links }, decycle(replacer));
-
-        const zip = new JSZip();
-        let count = 0;
-        const promArr = Array.from(proms)
-            .filter(url => url?.startsWith('/files'))
-            .map(url => url.replace('/', '')); // window.location.origin));
-        console.log(promArr.length);
-        if (!promArr.length) {
-            zip.file('docs.json', jsonDocs);
-            zip.generateAsync({ type: 'blob' }).then(content => saveAs(content, zipFilename));
-        } else
-            promArr.forEach((url, i) => {
-                // loading a file and add it in a zip file
-                JSZipUtils.getBinaryContent(window.location.origin + '/' + url, (err: any, data: any) => {
-                    if (err) throw err; // or handle the error
-                    // // Generate a directory within the Zip file structure
-                    // const assets = zip.folder("assets");
-                    // assets.file(filename, data, {binary: true});
-                    const assetPathOnServer = promArr[i].replace(window.location.origin + '/', '').replace(/\//g, '%%%');
-                    zip.file(assetPathOnServer, data, { binary: true });
-                    console.log(' => ' + url);
-                    if (++count === promArr.length) {
-                        zip.file('docs.json', jsonDocs);
-                        zip.generateAsync({ type: 'blob' }).then(content => saveAs(content, zipFilename));
-                        // const a = document.createElement("a");
-                        // const url = Utils.prepend(`/downloadId/${this.props.Document[Id]}`);
-                        // a.href = url;
-                        // a.download = `DocExport-${this.props.Document[Id]}.zip`;
-                        // a.click();
-                    }
-                });
-            });
-    }
-}
-
-ScriptingGlobals.add('Docs', Docs);
-// eslint-disable-next-line prefer-arrow-callback
-ScriptingGlobals.add(function copyDragFactory(dragFactory: Doc, asDelegate?: boolean) {
-    return dragFactory instanceof Doc ? (asDelegate ? DocUtils.delegateDragFactory(dragFactory) : DocUtils.copyDragFactory(dragFactory)) : dragFactory;
-});
-// eslint-disable-next-line prefer-arrow-callback
-ScriptingGlobals.add(function makeDelegate(proto: any) {
-    const d = Docs.Create.DelegateDocument(proto, { title: 'child of ' + proto.title });
-    return d;
-});
-// eslint-disable-next-line prefer-arrow-callback
-ScriptingGlobals.add(function generateLinkTitle(link: Doc) {
-    const linkAnchor1title = link.link_anchor_1 && link.link_anchor_1 !== link ? Cast(link.link_anchor_1, Doc, null)?.title : '<?>';
-    const linkAnchor2title = link.link_anchor_2 && link.link_anchor_2 !== link ? Cast(link.link_anchor_2, Doc, null)?.title : '<?>';
-    const relation = link.link_relationship || 'to';
-    return `${linkAnchor1title} (${relation}) ${linkAnchor2title}`;
-});
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index 00279e7e1..96f5172bc 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -8,28 +8,28 @@ import { List } from "../../fields/List";
 import { PrefetchProxy } from "../../fields/Proxy";
 import { RichTextField } from "../../fields/RichTextField";
 import { listSpec } from "../../fields/Schema";
-import { ScriptField } from "../../fields/ScriptField";
+import { FollowLinkScript, ScriptField } from "../../fields/ScriptField";
 import { Cast, DateCast, DocCast, StrCast } from "../../fields/Types";
 import { WebField, nullAudio } from "../../fields/URLField";
 import { SetCachedGroups, SharingPermissions } from "../../fields/util";
 import { Gestures } from "../../pen-gestures/GestureTypes";
 import { DocServer } from "../DocServer";
+import { DocUtils } from '../documents/DocUtils';
 import { CollectionViewType, DocumentType } from "../documents/DocumentTypes";
-import { DocUtils, Docs, DocumentOptions, FInfo, FInfoFieldType } from "../documents/Documents";
+import { Docs, DocumentOptions, FInfo, FInfoFieldType } from "../documents/Documents";
 import { DashboardView } from "../views/DashboardView";
 import { OverlayView } from "../views/OverlayView";
 import { CollectionTreeView, TreeViewType } from "../views/collections/CollectionTreeView";
 import { Colors } from "../views/global/globalEnums";
 import { mediaState } from "../views/nodes/AudioBox";
-import { OpenWhere } from "../views/nodes/DocumentView";
 import { ButtonType, FontIconBox } from "../views/nodes/FontIconBox/FontIconBox";
 import { ImageBox } from "../views/nodes/ImageBox";
 import { LabelBox } from "../views/nodes/LabelBox";
+import { OpenWhere } from "../views/nodes/OpenWhere";
 import { ImportElementBox } from "../views/nodes/importBox/ImportElementBox";
 import { DragManager } from "./DragManager";
 import { dropActionType } from "./DropActionTypes";
 import { MakeTemplate } from "./DropConverter";
-import { FollowLinkScript } from "./LinkFollower";
 import { LinkManager, UPDATE_SERVER_CACHE } from "./LinkManager";
 import { ScriptingGlobals } from "./ScriptingGlobals";
 import { ColorScheme } from "./SettingsManager";
diff --git a/src/client/util/DictationManager.ts b/src/client/util/DictationManager.ts
index 9026b368f..97ee628e2 100644
--- a/src/client/util/DictationManager.ts
+++ b/src/client/util/DictationManager.ts
@@ -4,6 +4,7 @@ import * as interpreter from 'words-to-numbers';
 import type {} from '@types/dom-speech-recognition';
 import { ClientUtils } from '../../ClientUtils';
 import { Doc, Opt } from '../../fields/Doc';
+import { DocData } from '../../fields/DocSymbols';
 import { List } from '../../fields/List';
 import { RichTextField } from '../../fields/RichTextField';
 import { listSpec } from '../../fields/Schema';
@@ -12,10 +13,10 @@ import { AudioField, ImageField } from '../../fields/URLField';
 import { DocumentType } from '../documents/DocumentTypes';
 import { Docs } from '../documents/Documents';
 import { DictationOverlay } from '../views/DictationOverlay';
-import { DocumentView, OpenWhere } from '../views/nodes/DocumentView';
+import { DocumentView } from '../views/nodes/DocumentView';
+import { OpenWhere } from '../views/nodes/OpenWhere';
 import { SelectionManager } from './SelectionManager';
 import { UndoManager } from './UndoManager';
-import { DocData } from '../../fields/DocSymbols';
 
 /**
  * This namespace provides a singleton instance of a manager that
diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts
index a893fe627..8f0c2b0bf 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -10,10 +10,11 @@ import { GetEffectiveAcl } from '../../fields/util';
 import { CollectionViewType } from '../documents/DocumentTypes';
 import { TabDocView } from '../views/collections/TabDocView';
 import { LightboxView } from '../views/LightboxView';
-import { DocumentView, DocumentViewInternal, OpenWhere } from '../views/nodes/DocumentView';
-import { FocusViewOptions } from '../views/nodes/FieldView';
+import { DocumentView, DocumentViewInternal } from '../views/nodes/DocumentView';
+import { FocusViewOptions } from '../views/nodes/FocusViewOptions';
 import { KeyValueBox } from '../views/nodes/KeyValueBox';
 import { LinkAnchorBox } from '../views/nodes/LinkAnchorBox';
+import { OpenWhere } from '../views/nodes/OpenWhere';
 import { PresBox } from '../views/nodes/trails';
 import { ScriptingGlobals } from './ScriptingGlobals';
 import { SelectionManager } from './SelectionManager';
diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts
index 3890b7845..34f54be2e 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -18,14 +18,13 @@ import { action, observable, runInAction } from 'mobx';
 import { ClientUtils } from '../../ClientUtils';
 import { emptyFunction } from '../../Utils';
 import { DateField } from '../../fields/DateField';
-import { Doc, FieldType, Opt, StrListCast } from '../../fields/Doc';
+import { CreateLinkToActiveAudio, Doc, FieldType, Opt, StrListCast } from '../../fields/Doc';
 import { DocData } from '../../fields/DocSymbols';
 import { List } from '../../fields/List';
 import { PrefetchProxy } from '../../fields/Proxy';
 import { ScriptField } from '../../fields/ScriptField';
 import { ScriptCast } from '../../fields/Types';
-import { DocUtils, Docs } from '../documents/Documents';
-import { CollectionFreeFormDocumentView } from '../views/nodes/CollectionFreeFormDocumentView';
+import { Docs } from '../documents/Documents';
 import { DocumentView } from '../views/nodes/DocumentView';
 import { dropActionType } from './DropActionTypes';
 import { ScriptingGlobals } from './ScriptingGlobals';
@@ -74,6 +73,7 @@ export function SetupDrag(_reference: React.RefObject<HTMLElement>, docFunc: ()
 }
 
 export namespace DragManager {
+    export const dragClassName = 'collectionFreeFormDocumentView-container';
     let dragDiv: HTMLDivElement;
     let dragLabel: HTMLDivElement;
     export let StartWindowDrag: Opt<(e: { pageX: number; pageY: number }, dragDocs: Doc[], finishDrag?: (aborted: boolean) => void) => boolean>;
@@ -139,11 +139,11 @@ export namespace DragManager {
         constructor(dragDoc: Doc[], dropAction?: dropActionType) {
             this.draggedDocuments = dragDoc;
             this.droppedDocuments = [];
-            this.draggedViews = [];
             this.offset = [0, 0];
             this.dropAction = dropAction;
         }
-        draggedViews: DocumentView[];
+        dragEnding?: () => void;
+        dragStarting?: () => void;
         draggedDocuments: Doc[];
         droppedDocuments: Doc[];
         treeViewDoc?: Doc;
@@ -157,6 +157,7 @@ export namespace DragManager {
         removeDocument?: RemoveFunction;
         isDocDecorationMove?: boolean; // Flags that Document decorations are used to drag document which allows suppression of onDragStart scripts
     }
+    Doc.SetDocDragDataName(DocumentDragData.name);
     export class LinkDragData {
         constructor(dragView: DocumentView, linkSourceGetAnchor: () => Doc) {
             this.linkDragView = dragView;
@@ -224,12 +225,12 @@ export namespace DragManager {
     export function StartDocumentDrag(eles: HTMLElement[], dragData: DocumentDragData, downX: number, downY: number, options?: DragOptions, onDropCompleted?: (e?: DragCompleteEvent) => any) {
         const addAudioTag = (dropDoc: any) => {
             dropDoc && !dropDoc.author_date && (dropDoc.author_date = new DateField());
-            dropDoc instanceof Doc && DocUtils.MakeLinkToActiveAudio(() => dropDoc);
+            dropDoc instanceof Doc && CreateLinkToActiveAudio(() => dropDoc);
             return dropDoc;
         };
         const finishDrag = async (e: DragCompleteEvent) => {
             const { docDragData } = e;
-            setTimeout(() => dragData.draggedViews.forEach(view => view.props.dragEnding?.()));
+            setTimeout(() => dragData.dragEnding?.());
             onDropCompleted?.(e); // glr: optional additional function to be called - in this case with presentation trails
             if (docDragData && !docDragData.droppedDocuments.length) {
                 docDragData.dropAction = dragData.userDropAction || dragData.dropAction;
@@ -265,7 +266,7 @@ export namespace DragManager {
         };
         dragData.draggedDocuments.map(d => d.dragFactory); // does this help?  trying to make sure the dragFactory Doc is loaded
         StartDrag(eles, dragData, downX, downY, options, finishDrag);
-        dragData.draggedViews.forEach(view => view.props.dragStarting?.());
+        dragData.dragStarting?.();
         return true;
     }
 
@@ -418,7 +419,7 @@ export namespace DragManager {
             // if the parent isn't a freeform view, then the element's width and height are presumed to match the acutal doc's dimensions (eg, dragging from import sidebar menu)
             let rotation: number | undefined;
             for (let parEle: HTMLElement | null | undefined = ele.parentElement; parEle; parEle = parEle?.parentElement) {
-                if (parEle.className === CollectionFreeFormDocumentView.CollectionFreeFormDocViewClassName) {
+                if (parEle.className === DragManager.dragClassName) {
                     rotation = (rotation ?? 0) + Number(parEle.style.transform.replace(/.*rotate\(([-0-9.e]*)deg\).*/, '$1') || 0);
                 }
                 parEle = parEle.parentElement;
@@ -630,7 +631,7 @@ export namespace DragManager {
             dispatchDrag(document.elementFromPoint(e.x, e.y) || document.body, e, new DragCompleteEvent(false, dragData), snapDrag(e, xFromLeft, yFromTop, xFromRight, yFromBottom), finishDrag, options, () => cleanupDrag(false));
         };
         const cleanupDrag = action((undo: boolean) => {
-            (dragData as DocumentDragData).draggedViews?.forEach(view => view.props.dragEnding?.());
+            (dragData as DocumentDragData).dragEnding?.();
             hideDragShowOriginalElements(false);
             document.removeEventListener('pointermove', moveHandler, true);
             document.removeEventListener('pointerup', upHandler, true);
diff --git a/src/client/util/GroupManager.tsx b/src/client/util/GroupManager.tsx
index 8d84dbad8..e1a503ac9 100644
--- a/src/client/util/GroupManager.tsx
+++ b/src/client/util/GroupManager.tsx
@@ -19,8 +19,8 @@ import { ObservableReactComponent } from '../views/ObservableReactComponent';
 import { TaskCompletionBox } from '../views/nodes/TaskCompletedBox';
 import './GroupManager.scss';
 import { GroupMemberView } from './GroupMemberView';
-import { SettingsManager } from './SettingsManager';
 import { SharingManager, User } from './SharingManager';
+import { SnappingManager } from './SnappingManager';
 
 /**
  * Interface for options for the react-select component
@@ -289,7 +289,7 @@ export class GroupManager extends ObservableReactComponent<{}> {
      */
     private get groupCreationModal() {
         const contents = (
-            <div className="group-create" style={{ background: SettingsManager.userBackgroundColor, color: SettingsManager.userColor }}>
+            <div className="group-create" style={{ background: SnappingManager.userBackgroundColor, color: SnappingManager.userColor }}>
                 <div className="group-heading" style={{ marginBottom: 0 }}>
                     <p>
                         <b>New Group</b>
@@ -384,7 +384,7 @@ export class GroupManager extends ObservableReactComponent<{}> {
         const groups = this.groupSort === 'ascending' ? this.allGroups.sort(sortGroups) : this.groupSort === 'descending' ? this.allGroups.sort(sortGroups).reverse() : this.allGroups;
 
         return (
-            <div className="group-interface" style={{ background: SettingsManager.userBackgroundColor, color: SettingsManager.userColor }}>
+            <div className="group-interface" style={{ background: SnappingManager.userBackgroundColor, color: SnappingManager.userColor }}>
                 {this.groupCreationModal}
                 {this.currentGroup ? (
                     <GroupMemberView
diff --git a/src/client/util/GroupMemberView.tsx b/src/client/util/GroupMemberView.tsx
index c703c3f98..da9e1aa28 100644
--- a/src/client/util/GroupMemberView.tsx
+++ b/src/client/util/GroupMemberView.tsx
@@ -11,7 +11,7 @@ import { StrCast } from '../../fields/Types';
 import { MainViewModal } from '../views/MainViewModal';
 import { GroupManager, UserOptions } from './GroupManager';
 import './GroupMemberView.scss';
-import { SettingsManager } from './SettingsManager';
+import { SnappingManager } from './SnappingManager';
 
 interface GroupMemberViewProps {
     group: Doc;
@@ -34,7 +34,7 @@ export class GroupMemberView extends React.Component<GroupMemberViewProps> {
         const hasEditAccess = GroupManager.Instance.hasEditAccess(this.group);
 
         return !this.group ? null : (
-            <div className="editing-interface" style={{ background: SettingsManager.userBackgroundColor, color: SettingsManager.userColor }}>
+            <div className="editing-interface" style={{ background: SnappingManager.userBackgroundColor, color: SnappingManager.userColor }}>
                 <div className="editing-header">
                     <input
                         className="group-title"
diff --git a/src/client/util/LinkFollower.ts b/src/client/util/LinkFollower.ts
index 09262f5ac..7ab476591 100644
--- a/src/client/util/LinkFollower.ts
+++ b/src/client/util/LinkFollower.ts
@@ -1,11 +1,9 @@
 import { action, runInAction } from 'mobx';
-import { Doc, DocListCast, FieldType, FieldResult, Opt } from '../../fields/Doc';
-import { ScriptField } from '../../fields/ScriptField';
+import { Doc, DocListCast, FieldResult, FieldType, Opt } from '../../fields/Doc';
 import { BoolCast, Cast, DocCast, NumCast, ScriptCast, StrCast } from '../../fields/Types';
 import { DocumentType } from '../documents/DocumentTypes';
-import { OpenWhere } from '../views/nodes/DocumentView';
-import { FocusViewOptions } from '../views/nodes/FieldView';
-import { PresBox } from '../views/nodes/trails';
+import { FocusViewOptions } from '../views/nodes/FocusViewOptions';
+import { OpenWhere } from '../views/nodes/OpenWhere';
 import { DocumentManager } from './DocumentManager';
 import { LinkManager } from './LinkManager';
 import { ScriptingGlobals } from './ScriptingGlobals';
@@ -90,8 +88,9 @@ export class LinkFollower {
                     if (target.type === DocumentType.PRES) {
                         const containerDocContext = DocumentManager.GetContextPath(sourceDoc, true); // gather all views that affect layout of sourceDoc so we can revert them after playing the rail
                         SelectionManager.DeselectAll();
-                        if (!DocumentManager.Instance.AddViewRenderedCb(target, dv => containerDocContext.length && (dv.ComponentView as PresBox).PlayTrail(containerDocContext))) {
-                            PresBox.OpenPresMinimized(target, [0, 0]);
+                        if (!DocumentManager.Instance.AddViewRenderedCb(target, dv => containerDocContext.length && dv.ComponentView?.playTrail?.(containerDocContext))) {
+                            alert('OPEN Pres minimized');
+                            // PresBox.OpenPresMinimized(target, [0, 0]);
                         }
                         finished?.();
                     } else {
@@ -138,9 +137,6 @@ ScriptingGlobals.add(function followLink(doc: Doc, altKey: boolean) {
     return LinkFollower.FollowLink(undefined, doc, altKey) ? undefined : { select: true };
 });
 
-export function FollowLinkScript() {
-    return ScriptField.MakeScript('return followLink(this,altKey)', { altKey: 'boolean' });
-}
 export function IsFollowLinkScript(field: FieldResult<FieldType>) {
     return ScriptCast(field)?.script.originalScript.includes('return followLink(');
 }
diff --git a/src/client/util/RTFMarkup.tsx b/src/client/util/RTFMarkup.tsx
index 05fb849fd..248fda7e3 100644
--- a/src/client/util/RTFMarkup.tsx
+++ b/src/client/util/RTFMarkup.tsx
@@ -2,7 +2,7 @@ import { action, computed, makeObservable, observable } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
 import { MainViewModal } from '../views/MainViewModal';
-import { SettingsManager } from './SettingsManager';
+import { SnappingManager } from './SnappingManager';
 
 @observer
 export class RTFMarkup extends React.Component<{}> {
@@ -10,6 +10,10 @@ export class RTFMarkup extends React.Component<{}> {
     static Instance: RTFMarkup;
     @observable private isOpen = false; // whether the SharingManager modal is open or not
 
+    public setOpen = action((status: boolean) => {
+        this.isOpen = status;
+    });
+
     constructor(props: {}) {
         super(props);
         makeObservable(this);
@@ -21,7 +25,7 @@ export class RTFMarkup extends React.Component<{}> {
      */
     @computed get cheatSheet() {
         return (
-            <div style={{ background: SettingsManager.userBackgroundColor, color: SettingsManager.userColor, textAlign: 'initial', height: '100%' }}>
+            <div style={{ background: SnappingManager.userBackgroundColor, color: SnappingManager.userColor, textAlign: 'initial', height: '100%' }}>
                 <p>
                     <b style={{ fontSize: 'larger' }}>(@wiki:phrase)</b>
                     {` display wikipedia page for entered text (terminate with carriage return)`}
@@ -122,24 +126,14 @@ export class RTFMarkup extends React.Component<{}> {
         );
     }
 
-    @action
-    public open = () => {
-        this.isOpen = true;
-    };
-
-    @action
-    public close = () => {
-        this.isOpen = false;
-    };
-
     render() {
         return (
             <MainViewModal
-                dialogueBoxStyle={{ backgroundColor: SettingsManager.userBackgroundColor, alignContent: 'normal', color: SettingsManager.userColor, padding: '16px' }}
+                dialogueBoxStyle={{ backgroundColor: SnappingManager.userBackgroundColor, alignContent: 'normal', color: SnappingManager.userColor, padding: '16px' }}
                 contents={this.cheatSheet}
                 isDisplayed={this.isOpen}
                 interactive
-                closeOnExternalClick={this.close}
+                closeOnExternalClick={() => this.setOpen(false)}
             />
         );
     }
diff --git a/src/client/util/ReplayMovements.ts b/src/client/util/ReplayMovements.ts
index 2c8fdf483..dcc6aabab 100644
--- a/src/client/util/ReplayMovements.ts
+++ b/src/client/util/ReplayMovements.ts
@@ -2,7 +2,7 @@ import { IReactionDisposer, makeObservable, observable, reaction } from 'mobx';
 import { Doc, IdToDoc } from '../../fields/Doc';
 import { CollectionDockingView } from '../views/collections/CollectionDockingView';
 import { CollectionFreeFormView } from '../views/collections/collectionFreeForm';
-import { OpenWhereMod } from '../views/nodes/DocumentView';
+import { OpenWhereMod } from '../views/nodes/OpenWhere';
 import { VideoBox } from '../views/nodes/VideoBox';
 import { DocumentManager } from './DocumentManager';
 import { Movement, Presentation } from './TrackMovements';
diff --git a/src/client/util/SettingsManager.tsx b/src/client/util/SettingsManager.tsx
index 8347844f7..8cb97fe50 100644
--- a/src/client/util/SettingsManager.tsx
+++ b/src/client/util/SettingsManager.tsx
@@ -14,9 +14,7 @@ import { BoolCast, Cast, NumCast, StrCast } from '../../fields/Types';
 import { DocServer } from '../DocServer';
 import { Networking } from '../Network';
 import { GoogleAuthenticationManager } from '../apis/GoogleAuthenticationManager';
-import { GestureOverlay } from '../views/GestureOverlay';
 import { MainViewModal } from '../views/MainViewModal';
-import { FontIconBox } from '../views/nodes/FontIconBox/FontIconBox';
 import { GroupManager } from './GroupManager';
 import './SettingsManager.scss';
 import { SnappingManager } from './SnappingManager';
@@ -256,25 +254,14 @@ export class SettingsManager extends React.Component<{}> {
                     size={Size.XSMALL}
                     color={SettingsManager.userColor}
                 />
-                <Toggle
-                    formLabel="Show Button Labels"
-                    formLabelPlacement="right"
-                    toggleType={ToggleType.SWITCH}
-                    onClick={() => {
-                        FontIconBox.ShowIconLabels = !FontIconBox.ShowIconLabels;
-                    }}
-                    toggleStatus={FontIconBox.ShowIconLabels}
-                    size={Size.XSMALL}
-                    color={SettingsManager.userColor}
-                />
                 <Toggle
                     formLabel="Recognize Ink Gestures"
                     formLabelPlacement="right"
                     toggleType={ToggleType.SWITCH}
                     onClick={() => {
-                        GestureOverlay.RecognizeGestures = !GestureOverlay.RecognizeGestures;
+                        Doc.UserDoc().recognizeGestures = !Doc.UserDoc().recognizeGestures;
                     }}
-                    toggleStatus={GestureOverlay.RecognizeGestures}
+                    toggleStatus={BoolCast(Doc.UserDoc().recognizeGestures)}
                     size={Size.XSMALL}
                     color={SettingsManager.userColor}
                 />
diff --git a/src/client/util/SharingManager.tsx b/src/client/util/SharingManager.tsx
index 19a7948e6..b0140b73c 100644
--- a/src/client/util/SharingManager.tsx
+++ b/src/client/util/SharingManager.tsx
@@ -18,7 +18,6 @@ import { Id } from '../../fields/FieldSymbols';
 import { StrCast } from '../../fields/Types';
 import { GetEffectiveAcl, SharingPermissions, TraceMobx, distributeAcls, normalizeEmail } from '../../fields/util';
 import { DocServer } from '../DocServer';
-import { DictationOverlay } from '../views/DictationOverlay';
 import { MainViewModal } from '../views/MainViewModal';
 import { DocumentView } from '../views/nodes/DocumentView';
 import { TaskCompletionBox } from '../views/nodes/TaskCompletedBox';
@@ -27,8 +26,8 @@ import { GroupManager, UserOptions } from './GroupManager';
 import { GroupMemberView } from './GroupMemberView';
 import { SearchUtil } from './SearchUtil';
 import { SelectionManager } from './SelectionManager';
-import { SettingsManager } from './SettingsManager';
 import './SharingManager.scss';
+import { SnappingManager } from './SnappingManager';
 import { undoable } from './UndoManager';
 
 export interface User {
@@ -238,14 +237,14 @@ export class SharingManager extends React.Component<{}> {
             const permissions = uniform ? StrCast(targetDoc?.[groupKey]) : '-multiple-';
 
             return !permissions ? null : (
-                <div key={groupKey} className="container" style={{ background: SettingsManager.userBackgroundColor, color: SettingsManager.userColor }}>
+                <div key={groupKey} className="container" style={{ background: SnappingManager.userBackgroundColor, color: SnappingManager.userColor }}>
                     <div className="padding">{StrCast(group.title)}</div>
                     &nbsp;
                     {group instanceof Doc ? (
                         <IconButton
                             icon={<FontAwesomeIcon icon="info-circle" />}
                             size={Size.XSMALL}
-                            color={SettingsManager.userColor}
+                            color={SnappingManager.userColor}
                             onClick={action(() => {
                                 GroupManager.Instance.currentGroup = group;
                             })}
@@ -279,10 +278,10 @@ export class SharingManager extends React.Component<{}> {
                 <div
                     className="sharing-contents"
                     style={{
-                        background: SettingsManager.userBackgroundColor,
+                        background: SnappingManager.userBackgroundColor,
                         color: StrCast(Doc.UserDoc().userColor),
                     }}>
-                    <p className="share-title" style={{ color: SettingsManager.userColor }}>
+                    <p className="share-title" style={{ color: SnappingManager.userColor }}>
                         <div className="share-info" onClick={() => window.open('https://brown-dash.github.io/Dash-Documentation/features/collaboration/', '_blank')}>
                             <FontAwesomeIcon icon="question-circle" size="sm" onClick={() => window.open('https://brown-dash.github.io/Dash-Documentation/features/collaboration/', '_blank')} />
                         </div>
@@ -290,10 +289,10 @@ export class SharingManager extends React.Component<{}> {
                         {this.focusOn(docs.length < 2 ? StrCast(targetDoc?.title, 'this document') : '-multiple-')}
                     </p>
                     <div className="share-copy-link">
-                        <Button type={Type.TERT} color={SettingsManager.userColor} icon={<FontAwesomeIcon icon="copy" size="sm" />} iconPlacement="left" text="Copy Guest URL" onClick={this.copyURL} />
+                        <Button type={Type.TERT} color={SnappingManager.userColor} icon={<FontAwesomeIcon icon="copy" size="sm" />} iconPlacement="left" text="Copy Guest URL" onClick={this.copyURL} />
                     </div>
                     <div className="close-button">
-                        <Button icon={<FontAwesomeIcon icon="times" size="lg" />} onClick={this.close} color={SettingsManager.userColor} />
+                        <Button icon={<FontAwesomeIcon icon="times" size="lg" />} onClick={this.close} color={SnappingManager.userColor} />
                     </div>
                     {admin ? (
                         <div className="share-container">
@@ -335,7 +334,7 @@ export class SharingManager extends React.Component<{}> {
                                     </select>
                                 </div>
                                 <div className="share-button">
-                                    <Button text="SHARE" type={Type.TERT} color={SettingsManager.userColor} onClick={this.share} />
+                                    <Button text="SHARE" type={Type.TERT} color={SnappingManager.userColor} onClick={this.share} />
                                 </div>
                             </div>
                             <div className="sort-checkboxes">
@@ -514,7 +513,6 @@ export class SharingManager extends React.Component<{}> {
         setTimeout(
             action(() => {
                 // this.copied = false;
-                DictationOverlay.Instance.hasActiveModal = false;
                 this.targetDoc = undefined;
             }),
             500
@@ -528,7 +526,6 @@ export class SharingManager extends React.Component<{}> {
         runInAction(() => {
             this.targetDocView = target;
             this.targetDoc = targetDoc || target?.Document;
-            DictationOverlay.Instance.hasActiveModal = true;
             this.isOpen = this.targetDoc !== undefined;
             this.permissions = SharingPermissions.Augment;
             this.upgradeNested = true;
diff --git a/src/client/views/DashboardView.tsx b/src/client/views/DashboardView.tsx
index c4855d2e7..d92a73b63 100644
--- a/src/client/views/DashboardView.tsx
+++ b/src/client/views/DashboardView.tsx
@@ -15,9 +15,10 @@ import { PrefetchProxy } from '../../fields/Proxy';
 import { listSpec } from '../../fields/Schema';
 import { ScriptField } from '../../fields/ScriptField';
 import { Cast, ImageCast, StrCast } from '../../fields/Types';
-import { normalizeEmail } from '../../fields/util';
+import { SharingPermissions, inheritParentAcls, normalizeEmail } from '../../fields/util';
 import { DocServer } from '../DocServer';
-import { DocUtils, Docs, DocumentOptions } from '../documents/Documents';
+import { DocUtils } from '../documents/DocUtils';
+import { Docs, DocumentOptions } from '../documents/Documents';
 import { dropActionType } from '../util/DropActionTypes';
 import { HistoryUtil } from '../util/History';
 import { ScriptingGlobals } from '../util/ScriptingGlobals';
@@ -36,11 +37,51 @@ enum DashboardGroup {
     SharedDashboards,
 }
 
+export type DocConfig = {
+    doc: Doc;
+    initialWidth?: number;
+    path?: Doc[];
+};
+
 // DashboardView is the view with the dashboard previews, rendered when the app first loads
 
 @observer
 export class DashboardView extends ObservableReactComponent<{}> {
     public static _urlState: HistoryUtil.DocUrl;
+    public static makeDocumentConfig(document: Doc, panelName?: string, width?: number, keyValue?: boolean) {
+        return {
+            type: 'react-component',
+            component: 'DocumentFrameRenderer',
+            title: document.title,
+            width: width,
+            props: {
+                documentId: document[Id],
+                keyValue,
+                panelName, // name of tab that can be used to close or replace its contents
+            },
+        };
+    }
+    static StandardCollectionDockingDocument(configs: Array<DocConfig>, options: DocumentOptions, id?: string, type: string = 'row') {
+        const layoutConfig = {
+            content: [
+                {
+                    type: type,
+                    content: [...configs.map(config => DashboardView.makeDocumentConfig(config.doc, undefined, config.initialWidth))],
+                },
+            ],
+        };
+        const doc = Docs.Create.DockDocument(
+            configs.map(c => c.doc),
+            JSON.stringify(layoutConfig),
+            ClientUtils.CurrentUserEmail() === 'guest' ? options : { acl_Guest: SharingPermissions.View, ...options },
+            id
+        );
+        configs.forEach(c => {
+            Doc.SetContainer(c.doc, doc);
+            inheritParentAcls(doc, c.doc, false);
+        });
+        return doc;
+    }
     constructor(props: any) {
         super(props);
         makeObservable(this);
@@ -389,7 +430,7 @@ export class DashboardView extends ObservableReactComponent<{}> {
 
         const title = name || `Dashboard ${dashboardCount}`;
         const freeformDoc = Doc.GuestTarget || Docs.Create.FreeformDocument([], freeformOptions);
-        const dashboardDoc = Docs.Create.StandardCollectionDockingDocument([{ doc: freeformDoc, initialWidth: 600 }], { title: title }, id, 'row');
+        const dashboardDoc = DashboardView.StandardCollectionDockingDocument([{ doc: freeformDoc, initialWidth: 600 }], { title: title }, id, 'row');
 
         Doc.AddDocToList(Doc.MyHeaderBar, 'data', freeformDoc, undefined, undefined, true);
         dashboardDoc.pane_count = 1;
diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx
index 391c2f694..a0e3d2ddd 100644
--- a/src/client/views/DocComponent.tsx
+++ b/src/client/views/DocComponent.tsx
@@ -12,8 +12,10 @@ import { DocumentType } from '../documents/DocumentTypes';
 import { DragManager } from '../util/DragManager';
 import { ObservableReactComponent } from './ObservableReactComponent';
 import { PinProps } from './PinFuncs';
-import { DocumentView, OpenWhere } from './nodes/DocumentView';
-import { FieldViewProps, FocusViewOptions } from './nodes/FieldView';
+import { DocumentView } from './nodes/DocumentView';
+import { FocusViewOptions } from './nodes/FocusViewOptions';
+import { FieldViewProps } from './nodes/FieldView';
+import { OpenWhere } from './nodes/OpenWhere';
 // import { DocUtils } from '../documents/Documents';
 
 /**
@@ -41,6 +43,7 @@ export interface ViewBoxInterface {
     onClickScriptDisable?: () => 'never' | 'always'; // disable click scripts : never, always, or undefined = only when selected
     getKeyFrameEditing?: () => boolean; // whether the document is in keyframe editing mode (if it is, then all hidden documents that are not active at the keyframe time will still be shown)
     setKeyFrameEditing?: (set: boolean) => void; // whether the document is in keyframe editing mode (if it is, then all hidden documents that are not active at the keyframe time will still be shown)
+    playTrail?: (docs: Doc[]) => void;
     playFrom?: (time: number, endTime?: number) => void;
     Pause?: () => void; // pause a media document (eg, audio/video)
     IsPlaying?: () => boolean; // is a media document playing
diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx
index c5358dffe..dd744f272 100644
--- a/src/client/views/DocumentButtonBar.tsx
+++ b/src/client/views/DocumentButtonBar.tsx
@@ -14,7 +14,7 @@ import { returnFalse, returnTrue, setupMoveUpEvents, simulateMouseClick } from '
 import { emptyFunction } from '../../Utils';
 import { Doc } from '../../fields/Doc';
 import { Cast, DocCast } from '../../fields/Types';
-import { DocUtils } from '../documents/Documents';
+import { DocUtils } from '../documents/DocUtils';
 import { CalendarManager } from '../util/CalendarManager';
 import { DragManager } from '../util/DragManager';
 import { dropActionType } from '../util/DropActionTypes';
@@ -22,15 +22,16 @@ import { IsFollowLinkScript } from '../util/LinkFollower';
 import { SelectionManager } from '../util/SelectionManager';
 import { SharingManager } from '../util/SharingManager';
 import { UndoManager, undoBatch } from '../util/UndoManager';
-import { PinProps } from './DocComponent';
 import './DocumentButtonBar.scss';
 import { ObservableReactComponent } from './ObservableReactComponent';
+import { PinProps } from './PinFuncs';
 import { TemplateMenu } from './TemplateMenu';
 import { TabDocView } from './collections/TabDocView';
 import { Colors } from './global/globalEnums';
 import { LinkPopup } from './linking/LinkPopup';
 import { DocumentLinksButton } from './nodes/DocumentLinksButton';
-import { DocumentView, DocumentViewInternal, OpenWhere } from './nodes/DocumentView';
+import { DocumentView, DocumentViewInternal } from './nodes/DocumentView';
+import { OpenWhere } from './nodes/OpenWhere';
 import { DashFieldView } from './nodes/formattedText/DashFieldView';
 
 @observer
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 1e6eb1aeb..ef493fb69 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -33,9 +33,10 @@ import { CollectionDockingView } from './collections/CollectionDockingView';
 import { CollectionFreeFormView } from './collections/collectionFreeForm';
 import { Colors } from './global/globalEnums';
 import { CollectionFreeFormDocumentView } from './nodes/CollectionFreeFormDocumentView';
-import { DocumentView, OpenWhereMod } from './nodes/DocumentView';
+import { DocumentView } from './nodes/DocumentView';
 import { ImageBox } from './nodes/ImageBox';
 import { KeyValueBox } from './nodes/KeyValueBox';
+import { OpenWhereMod } from './nodes/OpenWhere';
 import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox';
 
 interface DocumentDecorationsProps {
@@ -448,7 +449,7 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
         };
         this._resizeUndo = UndoManager.StartBatch('drag resizing');
         this._snapPt = { x: e.pageX, y: e.pageY };
-        SelectionManager.Views.forEach(docView => CollectionFreeFormDocumentView.from(docView)?.CollectionFreeFormView?.dragStarting(false, false));
+        SelectionManager.Views.forEach(docView => CollectionFreeFormView.from(docView)?.dragStarting(false, false));
     };
 
     projectDragToAspect = (e: PointerEvent, docView: DocumentView, fixedAspect: number) => {
diff --git a/src/client/views/GestureOverlay.tsx b/src/client/views/GestureOverlay.tsx
index bf5b51a2d..0d1573ea3 100644
--- a/src/client/views/GestureOverlay.tsx
+++ b/src/client/views/GestureOverlay.tsx
@@ -4,17 +4,6 @@ import { observer } from 'mobx-react';
 import * as React from 'react';
 import { returnEmptyDoclist, returnEmptyFilter, returnEmptyString, returnFalse, setupMoveUpEvents } from '../../ClientUtils';
 import { emptyFunction } from '../../Utils';
-import { Doc, Opt } from '../../fields/Doc';
-import { InkData, InkTool } from '../../fields/InkField';
-import { BoolCast, NumCast } from '../../fields/Types';
-import MobileInkOverlay from '../../mobile/MobileInkOverlay';
-import { Gestures } from '../../pen-gestures/GestureTypes';
-import { GestureUtils } from '../../pen-gestures/GestureUtils';
-import { MobileInkOverlayContent } from '../../server/Message';
-import { InteractionUtils } from '../util/InteractionUtils';
-import { ScriptingGlobals } from '../util/ScriptingGlobals';
-import { Transform } from '../util/Transform';
-import './GestureOverlay.scss';
 import {
     ActiveArrowEnd,
     ActiveArrowScale,
@@ -24,12 +13,24 @@ import {
     ActiveInkBezierApprox,
     ActiveInkColor,
     ActiveInkWidth,
+    Doc,
+    Opt,
     SetActiveArrowStart,
     SetActiveDash,
     SetActiveFillColor,
     SetActiveInkColor,
     SetActiveInkWidth,
-} from './InkingStroke';
+} from '../../fields/Doc';
+import { InkData, InkTool } from '../../fields/InkField';
+import { NumCast } from '../../fields/Types';
+// import MobileInkOverlay from '../../mobile/MobileInkOverlay';
+import { Gestures } from '../../pen-gestures/GestureTypes';
+import { GestureUtils } from '../../pen-gestures/GestureUtils';
+// import { MobileInkOverlayContent } from '../../server/Message';
+import { InteractionUtils } from '../util/InteractionUtils';
+import { ScriptingGlobals } from '../util/ScriptingGlobals';
+import { Transform } from '../util/Transform';
+import './GestureOverlay.scss';
 import { ObservableReactComponent } from './ObservableReactComponent';
 import { DocumentView } from './nodes/DocumentView';
 
@@ -49,13 +50,6 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
     // eslint-disable-next-line no-use-before-define
     static Instances: GestureOverlay[] = [];
 
-    public static set RecognizeGestures(active) {
-        Doc.UserDoc().recognizeGestures = active;
-    }
-    public static get RecognizeGestures() {
-        return BoolCast(Doc.UserDoc().recognizeGestures);
-    }
-
     @observable public InkShape: Opt<Gestures> = undefined;
     @observable public SavedColor?: string = undefined;
     @observable public SavedWidth?: number = undefined;
@@ -80,7 +74,7 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
         return this.Tool !== ToolglassTools.None;
     }
 
-    @observable private showMobileInkOverlay: boolean = false;
+    // @observable private showMobileInkOverlay: boolean = false;
 
     private _overlayRef = React.createRef<HTMLDivElement>();
     private _d1: Doc | undefined;
@@ -158,7 +152,7 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
                 // need to decide when to turn gestures back on
                 const result = points.length > 2 && GestureUtils.GestureRecognizer.Recognize([points]);
                 let actionPerformed = false;
-                if (GestureOverlay.RecognizeGestures && result && result.Score > 0.7) {
+                if (Doc.UserDoc().recognizeGestures && result && result.Score > 0.7) {
                     switch (result.Name) {
                         case Gestures.Line:
                         case Gestures.Triangle:
@@ -502,15 +496,10 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
         this._clipboardDoc = undefined;
     };
 
-    @action
-    enableMobileInkOverlay = (content: MobileInkOverlayContent) => {
-        this.showMobileInkOverlay = content.enableOverlay;
-    };
-
     render() {
         return (
             <div className="gestureOverlay-cont" style={{ pointerEvents: this._props.isActive ? 'all' : 'none' }} ref={this._overlayRef} onPointerDown={this.onPointerDown}>
-                {this.showMobileInkOverlay ? <MobileInkOverlay /> : null}
+                {/* {this.showMobileInkOverlay ? <MobileInkOverlay /> : null} */}
                 {this.elements}
 
                 <div
diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts
index ebd61db7d..0f444094a 100644
--- a/src/client/views/GlobalKeyHandler.ts
+++ b/src/client/views/GlobalKeyHandler.ts
@@ -21,8 +21,9 @@ import { MainView } from './MainView';
 import { CollectionDockingView } from './collections/CollectionDockingView';
 import { CollectionStackedTimeline } from './collections/CollectionStackedTimeline';
 import { CollectionFreeFormView } from './collections/collectionFreeForm';
+import { CollectionFreeFormDocumentView } from './nodes/CollectionFreeFormDocumentView';
 import { DocumentLinksButton } from './nodes/DocumentLinksButton';
-import { OpenWhereMod } from './nodes/DocumentView';
+import { OpenWhereMod } from './nodes/OpenWhere';
 import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox';
 import { AnchorMenu } from './pdf/AnchorMenu';
 
@@ -88,8 +89,8 @@ export class KeyManager {
     private handleGreedy = action((/* keyname: string */) => {});
 
     nudge = (x: number, y: number, label: string) => {
-        const nudgeable = SelectionManager.Views.some(dv => dv.CollectionFreeFormDocumentView?.nudge);
-        nudgeable && UndoManager.RunInBatch(() => SelectionManager.Views.map(dv => dv.CollectionFreeFormDocumentView?.nudge(x, y)), label);
+        const nudgeable = SelectionManager.Views.some(dv => CollectionFreeFormDocumentView.from(dv)?.nudge);
+        nudgeable && UndoManager.RunInBatch(() => SelectionManager.Views.map(dv => CollectionFreeFormDocumentView.from(dv)?.nudge(x, y)), label);
         return { stopPropagation: nudgeable, preventDefault: nudgeable };
     };
 
@@ -105,7 +106,7 @@ export class KeyManager {
             case 'g':
                 if (document.activeElement?.tagName !== 'INPUT' && document.activeElement?.tagName !== 'TEXTAREA') {
                     const selected = SelectionManager.Views;
-                    const cv = selected.reduce((col, dv) => (!col || dv.CollectionFreeFormView === col ? dv.CollectionFreeFormView : undefined), undefined as undefined | CollectionFreeFormView);
+                    const cv = selected.reduce((col, dv) => (!col || CollectionFreeFormView.from(dv) === col ? CollectionFreeFormView.from(dv) : undefined), undefined as undefined | CollectionFreeFormView);
                     cv && undoable(() => cv._marqueeViewRef.current?.collection(e, true, SelectionManager.Docs), 'grouping');
                 }
                 break;
@@ -205,7 +206,7 @@ export class KeyManager {
         switch (keyname) {
             case 'ƒ':
             case 'f':
-                UndoManager.RunInBatch(() => SelectionManager.Views?.[0]?.CollectionFreeFormDocumentView?.float(), 'float');
+                UndoManager.RunInBatch(() => CollectionFreeFormDocumentView.from(SelectionManager.Views?.[0])?.float(), 'float');
                 break;
             default:
         }
diff --git a/src/client/views/InkStrokeProperties.ts b/src/client/views/InkStrokeProperties.ts
index 98cf33c41..109a9cad4 100644
--- a/src/client/views/InkStrokeProperties.ts
+++ b/src/client/views/InkStrokeProperties.ts
@@ -13,6 +13,7 @@ import { DocumentManager } from '../util/DocumentManager';
 import { undoBatch } from '../util/UndoManager';
 import { FitOneCurve } from '../util/bezierFit';
 import { InkingStroke } from './InkingStroke';
+import { CollectionFreeFormView } from './collections/collectionFreeForm';
 import { DocumentView } from './nodes/DocumentView';
 
 export class InkStrokeProperties {
@@ -380,7 +381,7 @@ export class InkStrokeProperties {
     };
 
     snapToAllCurves = (screenDragPt: { X: number; Y: number }, inkView: DocumentView, snapData: { nearestPt: { X: number; Y: number }; distance: number }, ink: InkData, controlIndex: number) => {
-        const containingCollection = inkView.CollectionFreeFormView;
+        const containingCollection = CollectionFreeFormView.from(inkView);
         const containingDocView = containingCollection?.DocumentView?.();
         containingCollection?.childDocs
             .filter(doc => doc.type === DocumentType.INK)
diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx
index cf609d8f9..884512539 100644
--- a/src/client/views/InkingStroke.tsx
+++ b/src/client/views/InkingStroke.tsx
@@ -30,6 +30,7 @@ import { BoolCast, Cast, NumCast, RTFCast, StrCast } from '../../fields/Types';
 import { TraceMobx } from '../../fields/util';
 import { CognitiveServices } from '../cognitive_services/CognitiveServices';
 import { Docs } from '../documents/Documents';
+import { DocumentType } from '../documents/DocumentTypes';
 import { InteractionUtils } from '../util/InteractionUtils';
 import { SnappingManager } from '../util/SnappingManager';
 import { UndoManager } from '../util/UndoManager';
@@ -491,67 +492,17 @@ export class InkingStroke extends ViewBoxAnnotatableComponent<FieldViewProps>()
         );
     }
 }
-export function ActiveInkPen(): Doc {
-    return Doc.UserDoc();
-}
-export function ActiveInkColor(): string {
-    return StrCast(ActiveInkPen()?.activeInkColor, 'black');
-}
-export function ActiveFillColor(): string {
-    return StrCast(ActiveInkPen()?.activeFillColor, '');
-}
-export function ActiveIsInkMask(): boolean {
-    return BoolCast(ActiveInkPen()?.activeIsInkMask, false);
-}
-export function ActiveInkHideTextLabels(): boolean {
-    return BoolCast(ActiveInkPen().activeInkHideTextLabels, false);
-}
-export function ActiveArrowStart(): string {
-    return StrCast(ActiveInkPen()?.activeArrowStart, '');
-}
-export function ActiveArrowEnd(): string {
-    return StrCast(ActiveInkPen()?.activeArrowEnd, '');
-}
-export function ActiveArrowScale(): number {
-    return NumCast(ActiveInkPen()?.activeArrowScale, 1);
-}
-export function ActiveDash(): string {
-    return StrCast(ActiveInkPen()?.activeDash, '0');
-}
-export function ActiveInkWidth(): number {
-    return Number(ActiveInkPen()?.activeInkWidth);
-}
-export function ActiveInkBezierApprox(): string {
-    return StrCast(ActiveInkPen()?.activeInkBezier);
-}
-
-export function SetActiveInkWidth(width: string): void {
-    !isNaN(parseInt(width)) && ActiveInkPen() && (ActiveInkPen().activeInkWidth = width);
-}
-export function SetActiveBezierApprox(bezier: string): void {
-    ActiveInkPen() && (ActiveInkPen().activeInkBezier = isNaN(parseInt(bezier)) ? '' : bezier);
-}
-export function SetActiveInkColor(value: string) {
-    ActiveInkPen() && (ActiveInkPen().activeInkColor = value);
-}
-export function SetActiveIsInkMask(value: boolean) {
-    ActiveInkPen() && (ActiveInkPen().activeIsInkMask = value);
-}
-export function SetActiveInkHideTextLabels(value: boolean) {
-    ActiveInkPen() && (ActiveInkPen().activeInkHideTextLabels = value);
-}
-export function SetActiveFillColor(value: string) {
-    ActiveInkPen() && (ActiveInkPen().activeFillColor = value);
-}
-export function SetActiveArrowStart(value: string) {
-    ActiveInkPen() && (ActiveInkPen().activeArrowStart = value);
-}
-export function SetActiveArrowEnd(value: string) {
-    ActiveInkPen() && (ActiveInkPen().activeArrowEnd = value);
-}
-export function SetActiveArrowScale(value: number) {
-    ActiveInkPen() && (ActiveInkPen().activeArrowScale = value);
-}
-export function SetActiveDash(dash: string): void {
-    !isNaN(parseInt(dash)) && ActiveInkPen() && (ActiveInkPen().activeDash = dash);
-}
+Docs.Prototypes.TemplateMap.set(DocumentType.INK, {
+    // NOTE: this is unused!! ink fields are filled in directly within the InkDocument() method
+    layout: { view: InkingStroke, dataField: 'stroke' },
+    options: {
+        acl: '',
+        systemIcon: 'BsFillPencilFill', //
+        _layout_nativeDimEditable: true,
+        _layout_reflowVertical: true,
+        _layout_reflowHorizontal: true,
+        layout_hideDecorationTitle: true, // don't show title when selected
+        _layout_fitWidth: false,
+        layout_isSvg: true,
+    },
+});
diff --git a/src/client/views/LightboxView.tsx b/src/client/views/LightboxView.tsx
index 7bf6bb9e5..020525ef8 100644
--- a/src/client/views/LightboxView.tsx
+++ b/src/client/views/LightboxView.tsx
@@ -9,11 +9,10 @@ import { observer } from 'mobx-react';
 import * as React from 'react';
 import { ClientUtils, returnEmptyDoclist, returnEmptyFilter, returnTrue } from '../../ClientUtils';
 import { emptyFunction } from '../../Utils';
-import { Doc, DocListCast, FieldResult, Opt } from '../../fields/Doc';
+import { CreateLinkToActiveAudio, Doc, DocListCast, FieldResult, Opt } from '../../fields/Doc';
 import { Id } from '../../fields/FieldSymbols';
 import { InkTool } from '../../fields/InkField';
 import { Cast, NumCast, toList } from '../../fields/Types';
-import { DocUtils } from '../documents/Documents';
 import { DocumentManager } from '../util/DocumentManager';
 import { LinkManager } from '../util/LinkManager';
 import { SelectionManager } from '../util/SelectionManager';
@@ -27,7 +26,8 @@ import { DefaultStyleProvider, wavyBorderPath } from './StyleProvider';
 import { CollectionDockingView } from './collections/CollectionDockingView';
 import { CollectionStackedTimeline } from './collections/CollectionStackedTimeline';
 import { TabDocView } from './collections/TabDocView';
-import { DocumentView, OpenWhere, OpenWhereMod } from './nodes/DocumentView';
+import { DocumentView } from './nodes/DocumentView';
+import { OpenWhere, OpenWhereMod } from './nodes/OpenWhere';
 
 interface LightboxViewProps {
     PanelWidth: number;
@@ -88,7 +88,7 @@ export class LightboxView extends ObservableReactComponent<LightboxViewProps> {
                 savedKeys.forEach(key => {
                     this._savedState[key] = Doc.Get(doc, key, true);
                 });
-            const l = DocUtils.MakeLinkToActiveAudio(() => doc).lastElement();
+            const l = CreateLinkToActiveAudio(() => doc).lastElement();
             l && (Cast(l.link_anchor_2, Doc, null).backgroundColor = 'lightgreen');
             CollectionStackedTimeline.CurrentlyPlaying?.forEach(dv => dv.ComponentView?.Pause?.());
             this._history.push({ doc, target });
@@ -136,7 +136,7 @@ export class LightboxView extends ObservableReactComponent<LightboxViewProps> {
         const target = (this._docTarget = this._future.pop());
         const targetDocView = target && DocumentManager.Instance.getLightboxDocumentView(target);
         if (targetDocView && target) {
-            const l = DocUtils.MakeLinkToActiveAudio(() => targetDocView.ComponentView?.getAnchor?.(true) || target).lastElement();
+            const l = CreateLinkToActiveAudio(() => targetDocView.ComponentView?.getAnchor?.(true) || target).lastElement();
             l && (Cast(l.link_anchor_2, Doc, null).backgroundColor = 'lightgreen');
             DocumentManager.Instance.showDocument(target, { willZoomCentered: true, zoomScale: 0.9 });
             if (this._history.lastElement().target !== target) this._history.push({ doc: lightDoc, target });
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index d3dd51ac6..aec553baa 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -11,7 +11,7 @@ import * as React from 'react';
 import '../../../node_modules/browndash-components/dist/styles/global.min.css';
 import { ClientUtils, lightOrDark, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue, returnZero, setupMoveUpEvents } from '../../ClientUtils';
 import { emptyFunction } from '../../Utils';
-import { Doc, DocListCast, Opt } from '../../fields/Doc';
+import { Doc, DocListCast, GetDocFromUrl, Opt } from '../../fields/Doc';
 import { DocData } from '../../fields/DocSymbols';
 import { DocCast, StrCast, toList } from '../../fields/Types';
 import { DocServer } from '../DocServer';
@@ -55,28 +55,28 @@ import { CollectionDockingView } from './collections/CollectionDockingView';
 import { CollectionMenu } from './collections/CollectionMenu';
 import { TabDocView } from './collections/TabDocView';
 import './collections/TreeView.scss';
+import { CollectionFreeFormView } from './collections/collectionFreeForm';
 import { MarqueeOptionsMenu } from './collections/collectionFreeForm/MarqueeOptionsMenu';
 import { CollectionLinearView } from './collections/collectionLinear';
 import { LinkMenu } from './linking/LinkMenu';
 import { AudioBox } from './nodes/AudioBox';
 import { SchemaCSVPopUp } from './nodes/DataVizBox/SchemaCSVPopUp';
 import { DocButtonState } from './nodes/DocumentLinksButton';
-import { DocumentView, DocumentViewInternal, OpenWhere, OpenWhereMod, returnEmptyDocViewList } from './nodes/DocumentView';
+import { DocumentView, DocumentViewInternal, returnEmptyDocViewList } from './nodes/DocumentView';
 import { ImageEditorData as ImageEditor } from './nodes/ImageBox';
 import { LinkDescriptionPopup } from './nodes/LinkDescriptionPopup';
 import { LinkDocPreview, LinkInfo } from './nodes/LinkDocPreview';
 import { DirectionsAnchorMenu } from './nodes/MapBox/DirectionsAnchorMenu';
 import { MapAnchorMenu } from './nodes/MapBox/MapAnchorMenu';
+import { OpenWhere, OpenWhereMod } from './nodes/OpenWhere';
 import { TaskCompletionBox } from './nodes/TaskCompletedBox';
 import { DashFieldViewMenu } from './nodes/formattedText/DashFieldView';
-import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox';
 import { RichTextMenu } from './nodes/formattedText/RichTextMenu';
 import GenerativeFill from './nodes/generativeFill/GenerativeFill';
 import { PresBox } from './nodes/trails';
 import { AnchorMenu } from './pdf/AnchorMenu';
 import { GPTPopup } from './pdf/GPTPopup/GPTPopup';
 import { TopBar } from './topbar/TopBar';
-import { CollectionFreeFormView } from './collections/collectionFreeForm';
 
 const _global = (window /* browser */ || global) /* node */ as any;
 const { LEFT_MENU_WIDTH, TOPBAR_HEIGHT } = require('./global/globalCssVariables.module.scss'); // prettier-ignore
@@ -242,7 +242,7 @@ export class MainView extends ObservableReactComponent<{}> {
         window.addEventListener('paste', KeyManager.Instance.paste as any);
         document.addEventListener('dash', (e: any) => {
             // event used by chrome plugin to tell Dash which document to focus on
-            const id = FormattedTextBox.GetDocFromUrl(e.detail);
+            const id = GetDocFromUrl(e.detail);
             DocServer.GetRefField(id).then(doc => (doc instanceof Doc ? DocumentManager.Instance.showDocument(doc, { willPan: false }) : null));
         });
         document.addEventListener('linkAnnotationToDash', Hypothesis.linkListener);
diff --git a/src/client/views/MarqueeAnnotator.tsx b/src/client/views/MarqueeAnnotator.tsx
index ea73a53a9..06cae6d04 100644
--- a/src/client/views/MarqueeAnnotator.tsx
+++ b/src/client/views/MarqueeAnnotator.tsx
@@ -4,16 +4,16 @@ import * as React from 'react';
 import { Doc, Opt } from '../../fields/Doc';
 import { AclAdmin, AclAugment, AclEdit, AclSelfEdit, DocData } from '../../fields/DocSymbols';
 import { List } from '../../fields/List';
+import { FollowLinkScript } from '../../fields/ScriptField';
 import { NumCast } from '../../fields/Types';
 import { GetEffectiveAcl } from '../../fields/util';
 import { unimplementedFunction, Utils } from '../../Utils';
-import { Docs, DocUtils } from '../documents/Documents';
+import { Docs } from '../documents/Documents';
+import { DocUtils } from '../documents/DocUtils';
 import { DragManager } from '../util/DragManager';
-import { FollowLinkScript } from '../util/LinkFollower';
 import { undoable, undoBatch, UndoManager } from '../util/UndoManager';
 import './MarqueeAnnotator.scss';
 import { DocumentView } from './nodes/DocumentView';
-import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox';
 import { ObservableReactComponent } from './ObservableReactComponent';
 import { AnchorMenu } from './pdf/AnchorMenu';
 
@@ -197,7 +197,7 @@ export class MarqueeAnnotator extends ObservableReactComponent<MarqueeAnnotatorP
 
             const targetCreator = (annotationOn: Doc | undefined) => {
                 const target = DocUtils.GetNewTextDoc('Note linked to ' + this.props.Document.title, 0, 0, 100, 100, annotationOn, 'yellow');
-                FormattedTextBox.SetSelectOnLoad(target);
+                Doc.SetSelectOnLoad(target);
                 return target;
             };
             DragManager.StartAnchorAnnoDrag([ele], new DragManager.AnchorAnnoDragData(this.props.docView(), sourceAnchorCreator, targetCreator), e.pageX, e.pageY, {
diff --git a/src/client/views/PreviewCursor.tsx b/src/client/views/PreviewCursor.tsx
index fddb40624..034ade50b 100644
--- a/src/client/views/PreviewCursor.tsx
+++ b/src/client/views/PreviewCursor.tsx
@@ -3,7 +3,8 @@ import { observer } from 'mobx-react';
 import * as React from 'react';
 import { lightOrDark, returnFalse } from '../../ClientUtils';
 import { Doc, Opt } from '../../fields/Doc';
-import { DocUtils, Docs, DocumentOptions } from '../documents/Documents';
+import { Docs, DocumentOptions } from '../documents/Documents';
+import { DocUtils } from '../documents/DocUtils';
 import { ImageUtils } from '../util/Import & Export/ImageUtils';
 import { Transform } from '../util/Transform';
 import { UndoManager, undoBatch } from '../util/UndoManager';
diff --git a/src/client/views/PropertiesButtons.tsx b/src/client/views/PropertiesButtons.tsx
index d83fea2a5..cbbf48c75 100644
--- a/src/client/views/PropertiesButtons.tsx
+++ b/src/client/views/PropertiesButtons.tsx
@@ -20,8 +20,8 @@ import { DocData } from '../../fields/DocSymbols';
 import { ScriptField } from '../../fields/ScriptField';
 import { BoolCast, ScriptCast } from '../../fields/Types';
 import { ImageField } from '../../fields/URLField';
+import { DocUtils } from '../documents/DocUtils';
 import { CollectionViewType, DocumentType } from '../documents/DocumentTypes';
-import { DocUtils } from '../documents/Documents';
 import { IsFollowLinkScript } from '../util/LinkFollower';
 import { LinkManager } from '../util/LinkManager';
 import { SelectionManager } from '../util/SelectionManager';
@@ -30,7 +30,8 @@ import { undoBatch, undoable } from '../util/UndoManager';
 import { InkingStroke } from './InkingStroke';
 import './PropertiesButtons.scss';
 import { Colors } from './global/globalEnums';
-import { DocumentView, OpenWhere } from './nodes/DocumentView';
+import { DocumentView } from './nodes/DocumentView';
+import { OpenWhere } from './nodes/OpenWhere';
 import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox';
 
 @observer
diff --git a/src/client/views/PropertiesDocBacklinksSelector.tsx b/src/client/views/PropertiesDocBacklinksSelector.tsx
index e079b5cde..a806995f2 100644
--- a/src/client/views/PropertiesDocBacklinksSelector.tsx
+++ b/src/client/views/PropertiesDocBacklinksSelector.tsx
@@ -12,7 +12,7 @@ import { SettingsManager } from '../util/SettingsManager';
 import './PropertiesDocBacklinksSelector.scss';
 import { CollectionDockingView } from './collections/CollectionDockingView';
 import { LinkMenu } from './linking/LinkMenu';
-import { OpenWhere } from './nodes/DocumentView';
+import { OpenWhere } from './nodes/OpenWhere';
 
 type PropertiesDocBacklinksSelectorProps = {
     Document: Doc;
diff --git a/src/client/views/PropertiesDocContextSelector.tsx b/src/client/views/PropertiesDocContextSelector.tsx
index 2d04f2fe3..722b1f90a 100644
--- a/src/client/views/PropertiesDocContextSelector.tsx
+++ b/src/client/views/PropertiesDocContextSelector.tsx
@@ -11,7 +11,8 @@ import { DocFocusOrOpen } from '../util/DocumentManager';
 import { ObservableReactComponent } from './ObservableReactComponent';
 import './PropertiesDocContextSelector.scss';
 import { CollectionDockingView } from './collections/CollectionDockingView';
-import { DocumentView, OpenWhere } from './nodes/DocumentView';
+import { DocumentView } from './nodes/DocumentView';
+import { OpenWhere } from './nodes/OpenWhere';
 
 type PropertiesDocContextSelectorProps = {
     DocView?: DocumentView;
diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx
index f4ded8367..ff5dcd1b8 100644
--- a/src/client/views/PropertiesView.tsx
+++ b/src/client/views/PropertiesView.tsx
@@ -14,7 +14,7 @@ import { ColorResult, SketchPicker } from 'react-color';
 import * as Icons from 'react-icons/bs'; // {BsCollectionFill, BsFillFileEarmarkImageFill} from "react-icons/bs"
 import { ClientUtils, returnEmptyDoclist, returnEmptyFilter, returnEmptyString, returnFalse, returnTrue, setupMoveUpEvents } from '../../ClientUtils';
 import { emptyFunction } from '../../Utils';
-import { Doc, Field, FieldType, FieldResult, HierarchyMapping, NumListCast, Opt, ReverseHierarchyMap, StrListCast } from '../../fields/Doc';
+import { Doc, Field, FieldResult, FieldType, 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,9 +41,10 @@ import { PropertiesDocContextSelector } from './PropertiesDocContextSelector';
 import { PropertiesSection } from './PropertiesSection';
 import './PropertiesView.scss';
 import { DefaultStyleProvider } from './StyleProvider';
-import { DocumentView, OpenWhere } from './nodes/DocumentView';
+import { DocumentView } from './nodes/DocumentView';
 import { StyleProviderFuncType } from './nodes/FieldView';
 import { KeyValueBox } from './nodes/KeyValueBox';
+import { OpenWhere } from './nodes/OpenWhere';
 import { PresBox, PresEffect, PresEffectDirection } from './nodes/trails';
 
 const _global = (window /* browser */ || global) /* node */ as any;
diff --git a/src/client/views/SidebarAnnos.tsx b/src/client/views/SidebarAnnos.tsx
index 6195dcde8..3648e613d 100644
--- a/src/client/views/SidebarAnnos.tsx
+++ b/src/client/views/SidebarAnnos.tsx
@@ -12,7 +12,8 @@ import { List } from '../../fields/List';
 import { RichTextField } from '../../fields/RichTextField';
 import { DocCast, NumCast, StrCast } from '../../fields/Types';
 import { CollectionViewType, DocumentType } from '../documents/DocumentTypes';
-import { DocUtils, Docs } from '../documents/Documents';
+import { Docs } from '../documents/Documents';
+import { DocUtils } from '../documents/DocUtils';
 import { LinkManager } from '../util/LinkManager';
 import { SearchUtil } from '../util/SearchUtil';
 import { Transform } from '../util/Transform';
@@ -83,7 +84,7 @@ export class SidebarAnnos extends ObservableReactComponent<FieldViewProps & Extr
             _text_fontSize: StrCast(Doc.UserDoc().fontSize),
             _text_fontFamily: StrCast(Doc.UserDoc().fontFamily),
         });
-        FormattedTextBox.SetSelectOnLoad(target);
+        Doc.SetSelectOnLoad(target);
         FormattedTextBox.DontSelectInitialText = true;
         const link = DocUtils.MakeLink(anchor, target, { link_relationship: 'inline comment:comment on' });
         link && (link.link_displayLine = false);
diff --git a/src/client/views/TemplateMenu.tsx b/src/client/views/TemplateMenu.tsx
index 1d02568dd..5a7c2ef5b 100644
--- a/src/client/views/TemplateMenu.tsx
+++ b/src/client/views/TemplateMenu.tsx
@@ -8,7 +8,8 @@ import { ScriptField } from '../../fields/ScriptField';
 import { Cast, DocCast, StrCast } from '../../fields/Types';
 import { TraceMobx } from '../../fields/util';
 import { emptyFunction } from '../../Utils';
-import { Docs, DocUtils } from '../documents/Documents';
+import { Docs } from '../documents/Documents';
+import { DocUtils } from '../documents/DocUtils';
 import { ScriptingGlobals } from '../util/ScriptingGlobals';
 import { Transform } from '../util/Transform';
 import { CollectionTreeView } from './collections/CollectionTreeView';
diff --git a/src/client/views/collections/CollectionCarousel3DView.tsx b/src/client/views/collections/CollectionCarousel3DView.tsx
index 8d8f41126..d8232c132 100644
--- a/src/client/views/collections/CollectionCarousel3DView.tsx
+++ b/src/client/views/collections/CollectionCarousel3DView.tsx
@@ -14,7 +14,7 @@ import { DragManager } from '../../util/DragManager';
 import { SelectionManager } from '../../util/SelectionManager';
 import { StyleProp } from '../StyleProvider';
 import { DocumentView } from '../nodes/DocumentView';
-import { FocusViewOptions } from '../nodes/FieldView';
+import { FocusViewOptions } from '../nodes/FocusViewOptions';
 import './CollectionCarousel3DView.scss';
 import { CollectionSubView } from './CollectionSubView';
 
diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index fefaf6591..b98aceb16 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -2,7 +2,7 @@ import { action, IReactionDisposer, makeObservable, observable, reaction } from
 import { observer } from 'mobx-react';
 import * as React from 'react';
 import * as ReactDOM from 'react-dom/client';
-import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, DivHeight, DivWidth, incrementTitleCopy } from '../../../ClientUtils';
+import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, DivHeight, DivWidth, incrementTitleCopy, UpdateIcon } from '../../../ClientUtils';
 import { Doc, DocListCast, Field, Opt } from '../../../fields/Doc';
 import { AclAdmin, AclEdit, DocData } from '../../../fields/DocSymbols';
 import { Id } from '../../../fields/FieldSymbols';
@@ -25,12 +25,11 @@ import { SnappingManager } from '../../util/SnappingManager';
 import { undoable, undoBatch, UndoManager } from '../../util/UndoManager';
 import { DashboardView } from '../DashboardView';
 import { LightboxView } from '../LightboxView';
-import { OpenWhere, OpenWhereMod } from '../nodes/DocumentView';
+import { OpenWhere, OpenWhereMod } from '../nodes/OpenWhere';
 import { OverlayView } from '../OverlayView';
 import { ScriptingRepl } from '../ScriptingRepl';
 import { UndoStack } from '../UndoStack';
 import './CollectionDockingView.scss';
-import { CollectionFreeFormView } from './collectionFreeForm';
 import { CollectionSubView } from './CollectionSubView';
 import { TabDocView } from './TabDocView';
 
@@ -40,19 +39,6 @@ const _global = (window /* browser */ || global) /* node */ as any;
 export class CollectionDockingView extends CollectionSubView() {
     // eslint-disable-next-line no-use-before-define
     @observable public static Instance: CollectionDockingView | undefined = undefined;
-    public static makeDocumentConfig(document: Doc, panelName?: string, width?: number, keyValue?: boolean) {
-        return {
-            type: 'react-component',
-            component: 'DocumentFrameRenderer',
-            title: document.title,
-            width: width,
-            props: {
-                documentId: document[Id],
-                keyValue,
-                panelName, // name of tab that can be used to close or replace its contents
-            },
-        };
-    }
 
     private _reactionDisposer?: IReactionDisposer;
     private _lightboxReactionDisposer?: IReactionDisposer;
@@ -86,7 +72,7 @@ export class CollectionDockingView extends CollectionSubView() {
      */
     public StartOtherDrag = (e: { pageX: number; pageY: number }, dragDocs: Doc[], finishDrag?: (aborted: boolean) => void) => {
         this._flush = this._flush ?? UndoManager.StartBatch('golden layout drag');
-        const config = dragDocs.length === 1 ? CollectionDockingView.makeDocumentConfig(dragDocs[0]) : { type: 'row', content: dragDocs.map(doc => CollectionDockingView.makeDocumentConfig(doc)) };
+        const config = dragDocs.length === 1 ? DashboardView.makeDocumentConfig(dragDocs[0]) : { type: 'row', content: dragDocs.map(doc => DashboardView.makeDocumentConfig(doc)) };
         const dragSource = CollectionDockingView.Instance?._goldenLayout.createDragSource(document.createElement('div'), config);
         this.tabDragStart(dragSource, finishDrag);
         dragSource._dragListener.onMouseDown({ pageX: e.pageX, pageY: e.pageY, preventDefault: emptyFunction, button: 0 });
@@ -139,7 +125,7 @@ export class CollectionDockingView extends CollectionSubView() {
     public static ReplaceTab(document: Doc, mods: OpenWhereMod, stack: any, panelName: string, addToSplit?: boolean, keyValue?: boolean): boolean {
         const instance = CollectionDockingView.Instance;
         if (!instance) return false;
-        const newConfig = CollectionDockingView.makeDocumentConfig(document, panelName, undefined, keyValue);
+        const newConfig = DashboardView.makeDocumentConfig(document, panelName, undefined, keyValue);
         if (!panelName && stack) {
             const activeContentItemIndex = stack.contentItems.findIndex((item: any) => item.config === stack._activeContentItem.config);
             const newContentItem = stack.layoutManager.createContentItem(newConfig, instance._goldenLayout);
@@ -180,7 +166,7 @@ export class CollectionDockingView extends CollectionSubView() {
         const instance = CollectionDockingView.Instance;
         const glayRoot = instance._goldenLayout.root;
         if (!instance) return false;
-        const docContentConfig = CollectionDockingView.makeDocumentConfig(document, panelName, undefined, keyValue);
+        const docContentConfig = DashboardView.makeDocumentConfig(document, panelName, undefined, keyValue);
 
         CollectionDockingView.Instance._flush = CollectionDockingView.Instance._flush ?? UndoManager.StartBatch('Add Split');
         setTimeout(CollectionDockingView.Instance.endUndoBatch, 100);
@@ -461,7 +447,7 @@ export class CollectionDockingView extends CollectionSubView() {
         if (content) {
             const _width = DivWidth(content);
             const _height = DivHeight(content);
-            return CollectionFreeFormView.UpdateIcon(this.layoutDoc[Id] + '-icon' + new Date().getTime(), content, _width, _height, _width, _height, 0, 1, true, this.layoutDoc[Id] + '-icon', iconFile => {
+            return UpdateIcon(this.layoutDoc[Id] + '-icon' + new Date().getTime(), content, _width, _height, _width, _height, 0, 1, true, this.layoutDoc[Id] + '-icon', iconFile => {
                 const proto = this.dataDoc; // Cast(img.proto, Doc, null)!;
                 proto.thumb_nativeWidth = _width;
                 proto.thumb_nativeHeight = _height;
diff --git a/src/client/views/collections/CollectionMasonryViewFieldRow.tsx b/src/client/views/collections/CollectionMasonryViewFieldRow.tsx
index 8803f6f79..9a6f1e2eb 100644
--- a/src/client/views/collections/CollectionMasonryViewFieldRow.tsx
+++ b/src/client/views/collections/CollectionMasonryViewFieldRow.tsx
@@ -166,7 +166,7 @@ export class CollectionMasonryViewFieldRow extends ObservableReactComponent<CMVF
         this._createEmbeddingSelected = false;
         const { pivotField } = this._props;
         const newDoc = Docs.Create.TextDocument('', { _layout_autoHeight: true, _width: 200, _layout_fitWidth: true, title: value });
-        FormattedTextBox.SetSelectOnLoad(newDoc);
+        Doc.SetSelectOnLoad(newDoc);
         FormattedTextBox.SelectOnLoadChar = value;
         pivotField && (newDoc[DocData][pivotField] = this.getValue(this._props.heading));
         const docs = this._props.parent.childDocList;
diff --git a/src/client/views/collections/CollectionNoteTakingView.tsx b/src/client/views/collections/CollectionNoteTakingView.tsx
index 9212a6609..e940a1aef 100644
--- a/src/client/views/collections/CollectionNoteTakingView.tsx
+++ b/src/client/views/collections/CollectionNoteTakingView.tsx
@@ -11,7 +11,8 @@ import { SchemaHeaderField } from '../../../fields/SchemaHeaderField';
 import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
 import { TraceMobx } from '../../../fields/util';
 import { emptyFunction } from '../../../Utils';
-import { Docs, DocUtils } from '../../documents/Documents';
+import { Docs } from '../../documents/Documents';
+import { DocUtils } from '../../documents/DocUtils';
 import { DragManager } from '../../util/DragManager';
 import { dropActionType } from '../../util/DropActionTypes';
 import { SnappingManager } from '../../util/SnappingManager';
@@ -22,9 +23,10 @@ import { ContextMenuProps } from '../ContextMenuItem';
 import { FieldsDropdown } from '../FieldsDropdown';
 import { Colors } from '../global/globalEnums';
 import { LightboxView } from '../LightboxView';
+import { CollectionFreeFormDocumentView } from '../nodes/CollectionFreeFormDocumentView';
 import { DocumentView } from '../nodes/DocumentView';
-import { FieldViewProps, FocusViewOptions } from '../nodes/FieldView';
-import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox';
+import { FieldViewProps } from '../nodes/FieldView';
+import { FocusViewOptions } from '../nodes/FocusViewOptions';
 import { StyleProp } from '../StyleProvider';
 import './CollectionNoteTakingView.scss';
 import { CollectionNoteTakingViewColumn } from './CollectionNoteTakingViewColumn';
@@ -440,7 +442,7 @@ export class CollectionNoteTakingView extends CollectionSubView() {
             e.stopPropagation?.();
             const newDoc = Doc.MakeCopy(fieldProps.Document, true);
             newDoc[DocData].text = undefined;
-            FormattedTextBox.SetSelectOnLoad(newDoc);
+            Doc.SetSelectOnLoad(newDoc);
             return this.addDocument?.(newDoc);
         }
         return undefined;
@@ -472,7 +474,7 @@ export class CollectionNoteTakingView extends CollectionSubView() {
                 }
                 return true;
             }
-        } else if (de.complete.linkDragData?.dragDocument.embedContainer === this.Document && de.complete.linkDragData?.linkDragView?.CollectionFreeFormDocumentView) {
+        } else if (de.complete.linkDragData?.dragDocument.embedContainer === this.Document && CollectionFreeFormDocumentView.from(de.complete.linkDragData?.linkDragView)) {
             const source = Docs.Create.TextDocument('', { _width: 200, _height: 75, _layout_fitWidth: true, title: 'dropped annotation' });
             if (!this._props.addDocument?.(source)) e.preventDefault();
             de.complete.linkDocument = DocUtils.MakeLink(source, de.complete.linkDragData.linkSourceGetAnchor(), { link_relationship: 'doc annotation' }); // TODODO this is where in text links get passed
diff --git a/src/client/views/collections/CollectionNoteTakingViewColumn.tsx b/src/client/views/collections/CollectionNoteTakingViewColumn.tsx
index 95aecc7d0..c098c033b 100644
--- a/src/client/views/collections/CollectionNoteTakingViewColumn.tsx
+++ b/src/client/views/collections/CollectionNoteTakingViewColumn.tsx
@@ -11,7 +11,9 @@ import { SchemaHeaderField } from '../../../fields/SchemaHeaderField';
 import { Cast, NumCast } from '../../../fields/Types';
 import { ImageField } from '../../../fields/URLField';
 import { TraceMobx } from '../../../fields/util';
-import { DocUtils, Docs } from '../../documents/Documents';
+import { DocumentFromField } from '../../documents/DocFromField';
+import { DocUtils } from '../../documents/DocUtils';
+import { Docs } from '../../documents/Documents';
 import { DragManager } from '../../util/DragManager';
 import { SnappingManager } from '../../util/SnappingManager';
 import { Transform } from '../../util/Transform';
@@ -20,8 +22,8 @@ import { ContextMenu } from '../ContextMenu';
 import { ContextMenuProps } from '../ContextMenuItem';
 import { EditableView } from '../EditableView';
 import { ObservableReactComponent } from '../ObservableReactComponent';
-import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox';
 import './CollectionNoteTakingView.scss';
+import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox';
 
 interface CSVFieldColumnProps {
     Document: Doc;
@@ -149,7 +151,7 @@ export class CollectionNoteTakingViewColumn extends ObservableReactComponent<CSV
         const newDoc = Docs.Create.TextDocument(value, { _height: 18, _width: 200, _layout_fitWidth: true, title: value, _layout_autoHeight: true });
         const colValue = this.getValue(this._props.heading);
         newDoc[key] = colValue;
-        FormattedTextBox.SetSelectOnLoad(newDoc);
+        Doc.SetSelectOnLoad(newDoc);
         FormattedTextBox.SelectOnLoadChar = forceEmptyNote ? '' : ' ';
         return this._props.addDocument?.(newDoc) || false;
     };
@@ -178,7 +180,7 @@ export class CollectionNoteTakingViewColumn extends ObservableReactComponent<CSV
             doc => {
                 const key = this._props.pivotField;
                 doc[key] = this.getValue(this._props.heading);
-                FormattedTextBox.SetSelectOnLoad(doc);
+                Doc.SetSelectOnLoad(doc);
                 return this._props.addDocument?.(doc);
             },
             this._props.addDocument,
@@ -195,7 +197,7 @@ export class CollectionNoteTakingViewColumn extends ObservableReactComponent<CSV
                 docItems.push({
                     description: ':' + fieldKey,
                     event: () => {
-                        const created = DocUtils.DocumentFromField(dataDoc, fieldKey, Doc.GetProto(this._props.Document));
+                        const created = DocumentFromField(dataDoc, fieldKey, Doc.GetProto(this._props.Document));
                         if (created) {
                             if (this._props.Document.isTemplateDoc) {
                                 Doc.MakeMetadataFieldTemplate(created, this._props.Document);
diff --git a/src/client/views/collections/CollectionPileView.tsx b/src/client/views/collections/CollectionPileView.tsx
index 6ccbd6208..e02570d3e 100644
--- a/src/client/views/collections/CollectionPileView.tsx
+++ b/src/client/views/collections/CollectionPileView.tsx
@@ -8,11 +8,11 @@ import { Doc, DocListCast } from '../../../fields/Doc';
 import { ScriptField } from '../../../fields/ScriptField';
 import { NumCast, StrCast, toList } from '../../../fields/Types';
 import { emptyFunction } from '../../../Utils';
-import { DocUtils } from '../../documents/Documents';
+import { DocUtils } from '../../documents/DocUtils';
 import { dropActionType } from '../../util/DropActionTypes';
 import { SelectionManager } from '../../util/SelectionManager';
 import { undoBatch, UndoManager } from '../../util/UndoManager';
-import { OpenWhere } from '../nodes/DocumentView';
+import { OpenWhere } from '../nodes/OpenWhere';
 import { computePassLayout, computeStarburstLayout } from './collectionFreeForm';
 import { CollectionFreeFormView } from './collectionFreeForm/CollectionFreeFormView';
 import './CollectionPileView.scss';
diff --git a/src/client/views/collections/CollectionStackedTimeline.tsx b/src/client/views/collections/CollectionStackedTimeline.tsx
index 7adf44a5c..50a66aa41 100644
--- a/src/client/views/collections/CollectionStackedTimeline.tsx
+++ b/src/client/views/collections/CollectionStackedTimeline.tsx
@@ -13,7 +13,7 @@ import { DocData } from '../../../fields/DocSymbols';
 import { Id } from '../../../fields/FieldSymbols';
 import { List } from '../../../fields/List';
 import { listSpec } from '../../../fields/Schema';
-import { ComputedField, ScriptField } from '../../../fields/ScriptField';
+import { ComputedField, FollowLinkScript, ScriptField } from '../../../fields/ScriptField';
 import { Cast, NumCast } from '../../../fields/Types';
 import { ImageField } from '../../../fields/URLField';
 import { emptyFunction, formatTime } from '../../../Utils';
@@ -21,21 +21,23 @@ import { Docs } from '../../documents/Documents';
 import { DocumentType } from '../../documents/DocumentTypes';
 import { DocumentManager } from '../../util/DocumentManager';
 import { DragManager } from '../../util/DragManager';
-import { FollowLinkScript, IsFollowLinkScript, LinkFollower } from '../../util/LinkFollower';
+import { IsFollowLinkScript, LinkFollower } from '../../util/LinkFollower';
 import { LinkManager } from '../../util/LinkManager';
 import { ScriptingGlobals } from '../../util/ScriptingGlobals';
 import { SnappingManager } from '../../util/SnappingManager';
 import { Transform } from '../../util/Transform';
 import { undoBatch, UndoManager } from '../../util/UndoManager';
-import { CollectionSubView } from './CollectionSubView';
 import { LightboxView } from '../LightboxView';
 import { AudioWaveform } from '../nodes/audio/AudioWaveform';
-import { DocumentView, OpenWhere } from '../nodes/DocumentView';
-import { FocusFuncType, FocusViewOptions, StyleProviderFuncType } from '../nodes/FieldView';
+import { DocumentView } from '../nodes/DocumentView';
+import { FocusFuncType, StyleProviderFuncType } from '../nodes/FieldView';
+import { FocusViewOptions } from '../nodes/FocusViewOptions';
 import { LabelBox } from '../nodes/LabelBox';
+import { OpenWhere } from '../nodes/OpenWhere';
 import { VideoBox } from '../nodes/VideoBox';
 import { ObservableReactComponent } from '../ObservableReactComponent';
 import './CollectionStackedTimeline.scss';
+import { CollectionSubView } from './CollectionSubView';
 
 export type CollectionStackedTimelineProps = {
     Play: () => void;
diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx
index ad9960989..8ae0f2832 100644
--- a/src/client/views/collections/CollectionStackingView.tsx
+++ b/src/client/views/collections/CollectionStackingView.tsx
@@ -15,8 +15,9 @@ import { SchemaHeaderField } from '../../../fields/SchemaHeaderField';
 import { BoolCast, Cast, DocCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
 import { TraceMobx } from '../../../fields/util';
 import { emptyFunction } from '../../../Utils';
-import { Docs, DocUtils } from '../../documents/Documents';
+import { Docs } from '../../documents/Documents';
 import { CollectionViewType } from '../../documents/DocumentTypes';
+import { DocUtils } from '../../documents/DocUtils';
 import { DragManager } from '../../util/DragManager';
 import { dropActionType } from '../../util/DropActionTypes';
 import { SettingsManager } from '../../util/SettingsManager';
@@ -28,8 +29,8 @@ import { EditableView } from '../EditableView';
 import { LightboxView } from '../LightboxView';
 import { CollectionFreeFormDocumentView } from '../nodes/CollectionFreeFormDocumentView';
 import { DocumentView } from '../nodes/DocumentView';
-import { FieldViewProps, FocusViewOptions } from '../nodes/FieldView';
-import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox';
+import { FieldViewProps } from '../nodes/FieldView';
+import { FocusViewOptions } from '../nodes/FocusViewOptions';
 import { StyleProp } from '../StyleProvider';
 import { CollectionMasonryViewFieldRow } from './CollectionMasonryViewFieldRow';
 import './CollectionStackingView.scss';
@@ -296,7 +297,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
                 newDoc[layoutFieldKey] = fieldProps.Document[layoutFieldKey];
             }
             newDoc[DocData].text = undefined;
-            FormattedTextBox.SetSelectOnLoad(newDoc);
+            Doc.SetSelectOnLoad(newDoc);
             return this.addDocument?.(newDoc);
         }
         return false;
@@ -481,7 +482,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
                 }
                 return true;
             }
-        } else if (de.complete.linkDragData?.dragDocument.embedContainer === this.Document && de.complete.linkDragData?.linkDragView?.CollectionFreeFormDocumentView) {
+        } else if (de.complete.linkDragData?.dragDocument.embedContainer === this.Document && CollectionFreeFormDocumentView.from(de.complete.linkDragData?.linkDragView)) {
             const source = Docs.Create.TextDocument('', { _width: 200, _height: 75, _layout_fitWidth: true, title: 'dropped annotation' });
             if (!this._props.addDocument?.(source)) e.preventDefault();
             de.complete.linkDocument = DocUtils.MakeLink(source, de.complete.linkDragData.linkSourceGetAnchor(), { link_relationship: 'doc annotation' }); // TODODO this is where in text links get passed
diff --git a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
index cb463077c..e2ad5b31d 100644
--- a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
+++ b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
@@ -14,8 +14,10 @@ import { BoolCast, NumCast } from '../../../fields/Types';
 import { ImageField } from '../../../fields/URLField';
 import { TraceMobx } from '../../../fields/util';
 import { emptyFunction } from '../../../Utils';
-import { Docs, DocUtils } from '../../documents/Documents';
+import { DocumentFromField } from '../../documents/DocFromField';
+import { Docs } from '../../documents/Documents';
 import { DocumentType } from '../../documents/DocumentTypes';
+import { DocUtils } from '../../documents/DocUtils';
 import { DragManager } from '../../util/DragManager';
 import { dropActionType } from '../../util/DropActionTypes';
 import { SnappingManager } from '../../util/SnappingManager';
@@ -160,7 +162,7 @@ export class CollectionStackingViewFieldColumn extends ObservableReactComponent<
         const maxHeading = this._props.docList.reduce((prevHeading, doc) => (NumCast(doc.heading) > prevHeading ? NumCast(doc.heading) : prevHeading), 0);
         const heading = maxHeading === 0 || this._props.docList.length === 0 ? 1 : maxHeading === 1 ? 2 : 3;
         newDoc.heading = heading;
-        FormattedTextBox.SetSelectOnLoad(newDoc);
+        Doc.SetSelectOnLoad(newDoc);
         FormattedTextBox.SelectOnLoadChar = forceEmptyNote ? '' : ' ';
         return this._props.addDocument?.(newDoc) || false;
     };
@@ -240,7 +242,7 @@ export class CollectionStackingViewFieldColumn extends ObservableReactComponent<
         const height = this._ele ? DivHeight(this._ele) : 0;
         DocUtils.addDocumentCreatorMenuItems(
             doc => {
-                FormattedTextBox.SetSelectOnLoad(doc);
+                Doc.SetSelectOnLoad(doc);
                 return this._props.addDocument?.(doc);
             },
             this._props.addDocument,
@@ -255,7 +257,7 @@ export class CollectionStackingViewFieldColumn extends ObservableReactComponent<
                 docItems.push({
                     description: ':' + fieldKey,
                     event: () => {
-                        const created = DocUtils.DocumentFromField(dataDoc, fieldKey, Doc.GetProto(this._props.Document));
+                        const created = DocumentFromField(dataDoc, fieldKey, Doc.GetProto(this._props.Document));
                         if (created) {
                             if (this._props.Document.isTemplateDoc) {
                                 Doc.MakeMetadataFieldTemplate(created, this._props.Document);
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index f3dedaedf..7c08aedb1 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -3,7 +3,7 @@ import * as React from 'react';
 import * as rp from 'request-promise';
 import { ClientUtils, returnFalse } from '../../../ClientUtils';
 import CursorField from '../../../fields/CursorField';
-import { Doc, DocListCast, Opt, StrListCast } from '../../../fields/Doc';
+import { Doc, DocListCast, GetDocFromUrl, GetHrefFromHTML, Opt, RTFIsFragment, StrListCast } from '../../../fields/Doc';
 import { AclPrivate } from '../../../fields/DocSymbols';
 import { Id } from '../../../fields/FieldSymbols';
 import { List } from '../../../fields/List';
@@ -15,8 +15,9 @@ import { GetEffectiveAcl, TraceMobx } from '../../../fields/util';
 import { GestureUtils } from '../../../pen-gestures/GestureUtils';
 import { DocServer } from '../../DocServer';
 import { Networking } from '../../Network';
+import { DocUtils } from '../../documents/DocUtils';
 import { CollectionViewType, DocumentType } from '../../documents/DocumentTypes';
-import { DocUtils, Docs, DocumentOptions } from '../../documents/Documents';
+import { Docs, DocumentOptions } from '../../documents/Documents';
 import { DragManager } from '../../util/DragManager';
 import { dropActionType } from '../../util/DropActionTypes';
 import { ImageUtils } from '../../util/Import & Export/ImageUtils';
@@ -26,7 +27,6 @@ import { UndoManager, undoBatch } from '../../util/UndoManager';
 import { ViewBoxBaseComponent } from '../DocComponent';
 import { FieldViewProps } from '../nodes/FieldView';
 import { LoadingBox } from '../nodes/LoadingBox';
-import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox';
 
 export interface CollectionViewProps extends React.PropsWithChildren<FieldViewProps> {
     isAnnotationOverlay?: boolean; // is the collection an annotation overlay (eg an overlay on an image/video/etc)
@@ -317,10 +317,10 @@ export function CollectionSubView<X>() {
             const addDocument = (doc: Doc | Doc[]) => this.addDocument(doc);
 
             if (html) {
-                if (FormattedTextBox.IsFragment(html)) {
-                    const href = FormattedTextBox.GetHref(html);
+                if (RTFIsFragment(html)) {
+                    const href = GetHrefFromHTML(html);
                     if (href) {
-                        const docId = FormattedTextBox.GetDocFromUrl(href);
+                        const docId = GetDocFromUrl(href);
                         if (docId) {
                             // prosemirror text containing link to dash document
                             DocServer.GetRefField(docId).then(f => {
diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx
index 365b5acfd..d015e73ad 100644
--- a/src/client/views/collections/CollectionTreeView.tsx
+++ b/src/client/views/collections/CollectionTreeView.tsx
@@ -12,7 +12,8 @@ import { ScriptField } from '../../../fields/ScriptField';
 import { BoolCast, Cast, NumCast, ScriptCast, StrCast, toList } from '../../../fields/Types';
 import { TraceMobx } from '../../../fields/util';
 import { emptyFunction, Utils } from '../../../Utils';
-import { Docs, DocUtils } from '../../documents/Documents';
+import { Docs } from '../../documents/Documents';
+import { DocUtils } from '../../documents/DocUtils';
 import { DocumentManager } from '../../util/DocumentManager';
 import { DragManager } from '../../util/DragManager';
 import { dropActionType } from '../../util/DropActionTypes';
@@ -189,7 +190,7 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree
                 const prev = ind && DocListCast(targetDataDoc[this._props.fieldKey])[ind - 1];
                 this._props.removeDocument?.(docIn);
                 if (ind > 0 && prev) {
-                    FormattedTextBox.SetSelectOnLoad(prev);
+                    Doc.SetSelectOnLoad(prev);
                     DocumentManager.Instance.getDocumentView(prev, this.DocumentView?.())?.select(false);
                 }
                 return true;
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index d6cbe0dab..7cadd072b 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -7,14 +7,15 @@ import { Doc, DocListCast } from '../../../fields/Doc';
 import { ObjectField } from '../../../fields/ObjectField';
 import { BoolCast, Cast, ScriptCast, StrCast } from '../../../fields/Types';
 import { TraceMobx } from '../../../fields/util';
-import { CollectionViewType } from '../../documents/DocumentTypes';
-import { DocUtils } from '../../documents/Documents';
+import { DocUtils } from '../../documents/DocUtils';
+import { CollectionViewType, DocumentType } from '../../documents/DocumentTypes';
+import { Docs } from '../../documents/Documents';
 import { ImageUtils } from '../../util/Import & Export/ImageUtils';
 import { ContextMenu } from '../ContextMenu';
 import { ContextMenuProps } from '../ContextMenuItem';
 import { ViewBoxAnnotatableComponent, ViewBoxInterface } from '../DocComponent';
-import { OpenWhere } from '../nodes/DocumentView';
 import { FieldView } from '../nodes/FieldView';
+import { OpenWhere } from '../nodes/OpenWhere';
 import { CollectionCalendarView } from './CollectionCalendarView';
 import { CollectionCarousel3DView } from './CollectionCarousel3DView';
 import { CollectionCarouselView } from './CollectionCarouselView';
@@ -234,3 +235,19 @@ export class CollectionView extends ViewBoxAnnotatableComponent<CollectionViewPr
         );
     }
 }
+
+Docs.Prototypes.TemplateMap.set(DocumentType.COL, {
+    layout: { view: CollectionView, dataField: 'data' },
+    options: {
+        acl: '',
+        _layout_fitWidth: true,
+        freeform: '',
+        _freeform_panX: 0,
+        _freeform_panY: 0,
+        _freeform_scale: 1,
+        _layout_nativeDimEditable: true,
+        _layout_reflowHorizontal: true,
+        _layout_reflowVertical: true,
+        systemIcon: 'BsFillCollectionFill',
+    },
+});
diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx
index e7b8237a5..2d900bb73 100644
--- a/src/client/views/collections/TabDocView.tsx
+++ b/src/client/views/collections/TabDocView.tsx
@@ -28,12 +28,13 @@ import { UndoManager, undoable } from '../../util/UndoManager';
 import { DashboardView } from '../DashboardView';
 import { LightboxView } from '../LightboxView';
 import { ObservableReactComponent } from '../ObservableReactComponent';
-import { PinProps, PinDocView } from '../PinFuncs';
+import { PinDocView, PinProps } from '../PinFuncs';
 import { DefaultStyleProvider, StyleProp } from '../StyleProvider';
 import { Colors } from '../global/globalEnums';
-import { DocumentView, OpenWhere, OpenWhereMod, returnEmptyDocViewList } from '../nodes/DocumentView';
+import { DocumentView, returnEmptyDocViewList } from '../nodes/DocumentView';
 import { FieldViewProps } from '../nodes/FieldView';
 import { KeyValueBox } from '../nodes/KeyValueBox';
+import { OpenWhere, OpenWhereMod } from '../nodes/OpenWhere';
 import { PresBox, PresMovement } from '../nodes/trails';
 import { CollectionDockingView } from './CollectionDockingView';
 import { CollectionView } from './CollectionView';
diff --git a/src/client/views/collections/TreeView.tsx b/src/client/views/collections/TreeView.tsx
index a8dd60ae3..2f5af0564 100644
--- a/src/client/views/collections/TreeView.tsx
+++ b/src/client/views/collections/TreeView.tsx
@@ -18,8 +18,9 @@ import { listSpec } from '../../../fields/Schema';
 import { ComputedField, ScriptField } from '../../../fields/ScriptField';
 import { BoolCast, Cast, DocCast, FieldValue, NumCast, ScriptCast, StrCast, toList } from '../../../fields/Types';
 import { TraceMobx } from '../../../fields/util';
+import { DocUtils } from '../../documents/DocUtils';
 import { CollectionViewType, DocumentType } from '../../documents/DocumentTypes';
-import { DocUtils, Docs } from '../../documents/Documents';
+import { Docs } from '../../documents/Documents';
 import { DocumentManager } from '../../util/DocumentManager';
 import { DragManager } from '../../util/DragManager';
 import { dropActionType } from '../../util/DropActionTypes';
@@ -31,9 +32,10 @@ import { UndoManager, undoBatch, undoable } from '../../util/UndoManager';
 import { EditableView } from '../EditableView';
 import { ObservableReactComponent } from '../ObservableReactComponent';
 import { StyleProp } from '../StyleProvider';
-import { DocumentView, DocumentViewInternal, OpenWhere } from '../nodes/DocumentView';
+import { DocumentView, DocumentViewInternal } from '../nodes/DocumentView';
 import { FieldViewProps, StyleProviderFuncType } from '../nodes/FieldView';
 import { KeyValueBox } from '../nodes/KeyValueBox';
+import { OpenWhere } from '../nodes/OpenWhere';
 import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox';
 import { RichTextMenu } from '../nodes/formattedText/RichTextMenu';
 import { CollectionTreeView, TreeViewType } from './CollectionTreeView';
@@ -1300,7 +1302,7 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> {
                     const fieldKey = Doc.LayoutFieldKey(newParent);
                     if (remove && fieldKey && Cast(newParent[fieldKey], listSpec(Doc)) !== undefined) {
                         remove(child);
-                        FormattedTextBox.SetSelectOnLoad(child);
+                        Doc.SetSelectOnLoad(child);
                         TreeView._editTitleOnLoad = editTitle ? { id: child[Id], parent } : undefined;
                         Doc.AddDocToList(newParent, fieldKey, child, addAfter, false);
                         newParent.treeView_Open = true;
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 653a01a04..f55d5a23f 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -9,7 +9,7 @@ import { computedFn } from 'mobx-utils';
 import * as React from 'react';
 import { ClientUtils, DashColor, lightOrDark, OmitKeys, returnFalse, returnZero, setupMoveUpEvents, UpdateIcon } from '../../../../ClientUtils';
 import { DateField } from '../../../../fields/DateField';
-import { Doc, DocListCast, Field, FieldType, Opt } from '../../../../fields/Doc';
+import { ActiveInkWidth, Doc, DocListCast, Field, FieldType, Opt, SetActiveInkColor, SetActiveInkWidth } from '../../../../fields/Doc';
 import { DocData, Height, Width } from '../../../../fields/DocSymbols';
 import { Id } from '../../../../fields/FieldSymbols';
 import { InkData, InkField, InkTool, Segment } from '../../../../fields/InkField';
@@ -23,8 +23,9 @@ import { TraceMobx } from '../../../../fields/util';
 import { Gestures, PointData } from '../../../../pen-gestures/GestureTypes';
 import { GestureUtils } from '../../../../pen-gestures/GestureUtils';
 import { aggregateBounds, emptyFunction, intersectRect, Utils } from '../../../../Utils';
-import { Docs, DocUtils } from '../../../documents/Documents';
+import { Docs } from '../../../documents/Documents';
 import { CollectionViewType, DocumentType } from '../../../documents/DocumentTypes';
+import { DocUtils } from '../../../documents/DocUtils';
 import { DocumentManager } from '../../../util/DocumentManager';
 import { DragManager } from '../../../util/DragManager';
 import { dropActionType } from '../../../util/DropActionTypes';
@@ -39,13 +40,15 @@ import { undoable, undoBatch, UndoManager } from '../../../util/UndoManager';
 import { Timeline } from '../../animationtimeline/Timeline';
 import { ContextMenu } from '../../ContextMenu';
 import { GestureOverlay } from '../../GestureOverlay';
-import { ActiveInkWidth, InkingStroke, SetActiveInkColor, SetActiveInkWidth } from '../../InkingStroke';
+import { InkingStroke } from '../../InkingStroke';
 import { LightboxView } from '../../LightboxView';
 import { CollectionFreeFormDocumentView } from '../../nodes/CollectionFreeFormDocumentView';
 import { SchemaCSVPopUp } from '../../nodes/DataVizBox/SchemaCSVPopUp';
-import { DocumentView, OpenWhere } from '../../nodes/DocumentView';
-import { FieldViewProps, FocusViewOptions } from '../../nodes/FieldView';
+import { DocumentView } from '../../nodes/DocumentView';
+import { FieldViewProps } from '../../nodes/FieldView';
+import { FocusViewOptions } from '../../nodes/FocusViewOptions';
 import { FormattedTextBox } from '../../nodes/formattedText/FormattedTextBox';
+import { OpenWhere } from '../../nodes/OpenWhere';
 import { PinDocView, PinProps } from '../../PinFuncs';
 import { StyleProp } from '../../StyleProvider';
 import { CollectionSubView } from '../CollectionSubView';
@@ -285,7 +288,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     getActiveDocuments = () => this.childLayoutPairs.filter(pair => this.isCurrent(pair.layout)).map(pair => pair.layout);
     isAnyChildContentActive = () => this._props.isAnyChildContentActive();
     addLiveTextBox = (newDoc: Doc) => {
-        FormattedTextBox.SetSelectOnLoad(newDoc); // track the new text box so we can give it a prop that tells it to focus itself when it's displayed
+        Doc.SetSelectOnLoad(newDoc); // track the new text box so we can give it a prop that tells it to focus itself when it's displayed
         this.addDocument(newDoc);
     };
     selectDocuments = (docs: Doc[]) => {
@@ -1052,7 +1055,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         newDoc[DocData][Doc.LayoutFieldKey(newDoc, fieldProps.LayoutTemplateString)] = undefined; // the copy should not copy the text contents of it source, just the render style
         newDoc.x = NumCast(textDoc.x) + (below ? 0 : NumCast(textDoc._width) + 10);
         newDoc.y = NumCast(textDoc.y) + (below ? NumCast(textDoc._height) + 10 : 0);
-        FormattedTextBox.SetSelectOnLoad(newDoc);
+        Doc.SetSelectOnLoad(newDoc);
         FormattedTextBox.DontSelectInitialText = true;
         return this.addDocument?.(newDoc);
     }, 'copied text note');
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index 2f9cc49e0..3ab04f403 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -14,18 +14,19 @@ import { Cast, FieldValue, NumCast, StrCast } from '../../../../fields/Types';
 import { ImageField } from '../../../../fields/URLField';
 import { GetEffectiveAcl } from '../../../../fields/util';
 import { CognitiveServices } from '../../../cognitive_services/CognitiveServices';
+import { DocUtils } from '../../../documents/DocUtils';
 import { DocumentType } from '../../../documents/DocumentTypes';
-import { DocUtils, Docs, DocumentOptions } from '../../../documents/Documents';
+import { Docs, DocumentOptions } from '../../../documents/Documents';
 import { SelectionManager } from '../../../util/SelectionManager';
 import { freeformScrollMode } from '../../../util/SettingsManager';
 import { SnappingManager } from '../../../util/SnappingManager';
 import { Transform } from '../../../util/Transform';
 import { UndoManager, undoBatch } from '../../../util/UndoManager';
 import { ContextMenu } from '../../ContextMenu';
-import { MarqueeViewBounds } from '../../DocComponent';
 import { ObservableReactComponent } from '../../ObservableReactComponent';
+import { MarqueeViewBounds } from '../../PinFuncs';
 import { PreviewCursor } from '../../PreviewCursor';
-import { OpenWhere } from '../../nodes/DocumentView';
+import { OpenWhere } from '../../nodes/OpenWhere';
 import { pasteImageBitmap } from '../../nodes/WebBoxRenderer';
 import { FormattedTextBox } from '../../nodes/formattedText/FormattedTextBox';
 import { SubCollectionViewProps } from '../CollectionSubView';
@@ -162,7 +163,7 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
             const slide = DocUtils.copyDragFactory(DocCast(Doc.UserDoc().emptySlide))!;
             slide.x = x;
             slide.y = y;
-            FormattedTextBox.SelectOnLoad = slide[Id];
+            Doc.SetSelectOnLoad(slide);
             TreeView._editTitleOnLoad = { id: slide[Id], parent: undefined };
             this._props.addDocument?.(slide);
             e.stopPropagation();
diff --git a/src/client/views/collections/collectionGrid/CollectionGridView.tsx b/src/client/views/collections/collectionGrid/CollectionGridView.tsx
index 1634daaf7..2d9191dd7 100644
--- a/src/client/views/collections/collectionGrid/CollectionGridView.tsx
+++ b/src/client/views/collections/collectionGrid/CollectionGridView.tsx
@@ -1,11 +1,11 @@
 import { action, computed, Lambda, makeObservable, observable, reaction } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
+import { returnFalse, returnZero, setupMoveUpEvents } from '../../../../ClientUtils';
 import { Doc, Opt } from '../../../../fields/Doc';
 import { Id } from '../../../../fields/FieldSymbols';
 import { BoolCast, NumCast, ScriptCast, StrCast } from '../../../../fields/Types';
 import { emptyFunction } from '../../../../Utils';
-import { returnFalse, returnZero, setupMoveUpEvents } from '../../../../ClientUtils';
 import { Docs } from '../../../documents/Documents';
 import { DragManager } from '../../../util/DragManager';
 import { Transform } from '../../../util/Transform';
@@ -13,7 +13,6 @@ import { undoBatch } from '../../../util/UndoManager';
 import { ContextMenu } from '../../ContextMenu';
 import { ContextMenuProps } from '../../ContextMenuItem';
 import { DocumentView } from '../../nodes/DocumentView';
-import { FormattedTextBox } from '../../nodes/formattedText/FormattedTextBox';
 import { CollectionSubView } from '../CollectionSubView';
 import './CollectionGridView.scss';
 import Grid, { Layout } from './Grid';
@@ -364,7 +363,7 @@ export class CollectionGridView extends CollectionSubView() {
                         undoBatch(
                             action(() => {
                                 const text = Docs.Create.TextDocument('', { _width: 150, _height: 50 });
-                                FormattedTextBox.SetSelectOnLoad(text); // track the new text box so we can give it a prop that tells it to focus itself when it's displayed
+                                Doc.SetSelectOnLoad(text); // track the new text box so we can give it a prop that tells it to focus itself when it's displayed
                                 Doc.AddDocToList(this.Document, this._props.fieldKey, text);
                                 this.setLayoutList(this.addLayoutItem(this.savedLayoutList, this.makeLayoutItem(text, this.screenToCell(clickEv.clientX, clickEv.clientY))));
                             })
diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx
index ee79812a1..406a7d626 100644
--- a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx
+++ b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx
@@ -13,7 +13,8 @@ import { List } from '../../../../fields/List';
 import { listSpec } from '../../../../fields/Schema';
 import { ColumnType } from '../../../../fields/SchemaHeaderField';
 import { BoolCast, Cast, DocCast, NumCast, StrCast } from '../../../../fields/Types';
-import { DocUtils, Docs, DocumentOptions, FInfo } from '../../../documents/Documents';
+import { DocUtils } from '../../../documents/DocUtils';
+import { Docs, DocumentOptions, FInfo } from '../../../documents/Documents';
 import { DocumentManager } from '../../../util/DocumentManager';
 import { DragManager } from '../../../util/DragManager';
 import { dropActionType } from '../../../util/DropActionTypes';
@@ -26,7 +27,8 @@ 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 { FieldViewProps } from '../../nodes/FieldView';
+import { FocusViewOptions } from '../../nodes/FocusViewOptions';
 import { KeyValueBox } from '../../nodes/KeyValueBox';
 import { CollectionSubView } from '../CollectionSubView';
 import './CollectionSchemaView.scss';
diff --git a/src/client/views/collections/collectionSchema/SchemaRowBox.tsx b/src/client/views/collections/collectionSchema/SchemaRowBox.tsx
index 61afe08cf..32b48e4d1 100644
--- a/src/client/views/collections/collectionSchema/SchemaRowBox.tsx
+++ b/src/client/views/collections/collectionSchema/SchemaRowBox.tsx
@@ -5,8 +5,8 @@ import { computedFn } from 'mobx-utils';
 import * as React from 'react';
 import { CgClose, CgLock, CgLockUnlock } from 'react-icons/cg';
 import { FaExternalLinkAlt } from 'react-icons/fa';
-import { emptyFunction } from '../../../../Utils';
 import { returnFalse, setupMoveUpEvents } from '../../../../ClientUtils';
+import { emptyFunction } from '../../../../Utils';
 import { Doc } from '../../../../fields/Doc';
 import { BoolCast } from '../../../../fields/Types';
 import { DragManager } from '../../../util/DragManager';
@@ -15,8 +15,8 @@ import { Transform } from '../../../util/Transform';
 import { undoable } from '../../../util/UndoManager';
 import { ViewBoxBaseComponent } from '../../DocComponent';
 import { Colors } from '../../global/globalEnums';
-import { OpenWhere } from '../../nodes/DocumentView';
 import { FieldView, FieldViewProps } from '../../nodes/FieldView';
+import { OpenWhere } from '../../nodes/OpenWhere';
 import { CollectionSchemaView } from './CollectionSchemaView';
 import './CollectionSchemaView.scss';
 import { SchemaTableCell } from './SchemaTableCell';
diff --git a/src/client/views/global/globalScripts.ts b/src/client/views/global/globalScripts.ts
index 231bac541..d17d4ff7c 100644
--- a/src/client/views/global/globalScripts.ts
+++ b/src/client/views/global/globalScripts.ts
@@ -1,7 +1,7 @@
 import { Colors } from 'browndash-components';
 import { action, runInAction } from 'mobx';
 import { aggregateBounds } from '../../../Utils';
-import { Doc, Opt } from '../../../fields/Doc';
+import { ActiveFillColor, ActiveInkColor, ActiveInkHideTextLabels, ActiveInkWidth, ActiveIsInkMask, Doc, Opt, SetActiveFillColor, SetActiveInkColor, SetActiveInkHideTextLabels, SetActiveInkWidth, SetActiveIsInkMask } from '../../../fields/Doc';
 import { DocData } from '../../../fields/DocSymbols';
 import { InkTool } from '../../../fields/InkField';
 import { BoolCast, Cast, NumCast, StrCast } from '../../../fields/Types';
@@ -13,7 +13,7 @@ import { ScriptingGlobals } from '../../util/ScriptingGlobals';
 import { SelectionManager } from '../../util/SelectionManager';
 import { UndoManager, undoable } from '../../util/UndoManager';
 import { GestureOverlay } from '../GestureOverlay';
-import { ActiveFillColor, ActiveInkColor, ActiveInkHideTextLabels, ActiveInkWidth, ActiveIsInkMask, InkingStroke, SetActiveFillColor, SetActiveInkColor, SetActiveInkHideTextLabels, SetActiveInkWidth, SetActiveIsInkMask } from '../InkingStroke';
+import { InkingStroke } from '../InkingStroke';
 import { CollectionFreeFormView } from '../collections/collectionFreeForm';
 import { CollectionFreeFormDocumentView } from '../nodes/CollectionFreeFormDocumentView';
 import { DocumentView } from '../nodes/DocumentView';
@@ -111,7 +111,7 @@ ScriptingGlobals.add(function toggleOverlay(checkResult?: boolean) {
         if (NumCast(selected?.Document.z) >= 1) return true;
         return false;
     }
-    selected ? selected.CollectionFreeFormDocumentView?.float() : console.log('[FontIconBox.tsx] toggleOverlay failed');
+    selected ? CollectionFreeFormDocumentView.from(selected)?.float() : console.log('[FontIconBox.tsx] toggleOverlay failed');
     return undefined;
 });
 
diff --git a/src/client/views/linking/LinkMenuItem.tsx b/src/client/views/linking/LinkMenuItem.tsx
index 278d30d6a..df3accb0d 100644
--- a/src/client/views/linking/LinkMenuItem.tsx
+++ b/src/client/views/linking/LinkMenuItem.tsx
@@ -20,8 +20,9 @@ import { SelectionManager } from '../../util/SelectionManager';
 import { SettingsManager } from '../../util/SettingsManager';
 import { undoBatch } from '../../util/UndoManager';
 import { ObservableReactComponent } from '../ObservableReactComponent';
-import { DocumentView, DocumentViewInternal, OpenWhere } from '../nodes/DocumentView';
+import { DocumentView, DocumentViewInternal } from '../nodes/DocumentView';
 import { LinkInfo } from '../nodes/LinkDocPreview';
+import { OpenWhere } from '../nodes/OpenWhere';
 import './LinkMenuItem.scss';
 
 interface LinkMenuItemProps {
diff --git a/src/client/views/newlightbox/ButtonMenu/ButtonMenu.tsx b/src/client/views/newlightbox/ButtonMenu/ButtonMenu.tsx
index bce2b296f..72d81e9eb 100644
--- a/src/client/views/newlightbox/ButtonMenu/ButtonMenu.tsx
+++ b/src/client/views/newlightbox/ButtonMenu/ButtonMenu.tsx
@@ -5,7 +5,7 @@ import { InkTool } from '../../../../fields/InkField';
 import { SelectionManager } from '../../../util/SelectionManager';
 import { SnappingManager } from '../../../util/SnappingManager';
 import { CollectionDockingView } from '../../collections/CollectionDockingView';
-import { OpenWhereMod } from '../../nodes/DocumentView';
+import { OpenWhereMod } from '../../nodes/OpenWhere';
 import { NewLightboxView } from '../NewLightboxView';
 import './ButtonMenu.scss';
 import { IButtonMenu } from './utils';
diff --git a/src/client/views/newlightbox/NewLightboxView.tsx b/src/client/views/newlightbox/NewLightboxView.tsx
index 6616abad1..dc6bf3e9c 100644
--- a/src/client/views/newlightbox/NewLightboxView.tsx
+++ b/src/client/views/newlightbox/NewLightboxView.tsx
@@ -6,10 +6,9 @@ import { observer } from 'mobx-react';
 import * as React from 'react';
 import { returnEmptyDoclist, returnEmptyFilter, returnTrue } from '../../../ClientUtils';
 import { emptyFunction } from '../../../Utils';
-import { Doc, DocListCast, Opt } from '../../../fields/Doc';
+import { CreateLinkToActiveAudio, Doc, DocListCast, Opt } from '../../../fields/Doc';
 import { InkTool } from '../../../fields/InkField';
 import { Cast, NumCast, StrCast, toList } from '../../../fields/Types';
-import { DocUtils } from '../../documents/Documents';
 import { DocumentManager } from '../../util/DocumentManager';
 import { LinkManager } from '../../util/LinkManager';
 import { SelectionManager } from '../../util/SelectionManager';
@@ -20,7 +19,8 @@ import { LightboxView } from '../LightboxView';
 import { DefaultStyleProvider } from '../StyleProvider';
 import { CollectionStackedTimeline } from '../collections/CollectionStackedTimeline';
 import { TabDocView } from '../collections/TabDocView';
-import { DocumentView, OpenWhere } from '../nodes/DocumentView';
+import { DocumentView } from '../nodes/DocumentView';
+import { OpenWhere } from '../nodes/OpenWhere';
 import { ExploreView } from './ExploreView';
 import { IBounds, emptyBounds } from './ExploreView/utils';
 import { NewLightboxHeader } from './Header';
@@ -108,7 +108,7 @@ export class NewLightboxView extends React.Component<LightboxViewProps> {
             Doc.ActiveTool = InkTool.None;
             SnappingManager.SetExploreMode(false);
         } else {
-            const l = DocUtils.MakeLinkToActiveAudio(() => doc).lastElement();
+            const l = CreateLinkToActiveAudio(() => doc).lastElement();
             l && (Cast(l.link_anchor_2, Doc, null).backgroundColor = 'lightgreen');
             CollectionStackedTimeline.CurrentlyPlaying?.forEach(dv => dv.ComponentView?.Pause?.());
             // TabDocView.PinDoc(doc, { hidePresBox: true });
@@ -150,7 +150,7 @@ export class NewLightboxView extends React.Component<LightboxViewProps> {
         const target = (NewLightboxView._docTarget = this._future?.pop());
         const targetDocView = target && DocumentManager.Instance.getLightboxDocumentView(target);
         if (targetDocView && target) {
-            const l = DocUtils.MakeLinkToActiveAudio(() => targetDocView.ComponentView?.getAnchor?.(true) || target).lastElement();
+            const l = CreateLinkToActiveAudio(() => targetDocView.ComponentView?.getAnchor?.(true) || target).lastElement();
             l && (Cast(l.link_anchor_2, Doc, null).backgroundColor = 'lightgreen');
             DocumentManager.Instance.showDocument(target, { willZoomCentered: true, zoomScale: 0.9 });
             if (NewLightboxView._history?.lastElement().target !== target) NewLightboxView._history?.push({ doc, target });
diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx
index 1f618135f..4697491e0 100644
--- a/src/client/views/nodes/AudioBox.tsx
+++ b/src/client/views/nodes/AudioBox.tsx
@@ -13,7 +13,9 @@ import { ComputedField } from '../../../fields/ScriptField';
 import { Cast, DateCast, NumCast } from '../../../fields/Types';
 import { AudioField, nullAudio } from '../../../fields/URLField';
 import { formatTime } from '../../../Utils';
-import { Docs, DocUtils } from '../../documents/Documents';
+import { Docs } from '../../documents/Documents';
+import { DocumentType } from '../../documents/DocumentTypes';
+import { DocUtils } from '../../documents/DocUtils';
 import { Networking } from '../../Network';
 import { DragManager } from '../../util/DragManager';
 import { LinkManager } from '../../util/LinkManager';
@@ -24,8 +26,8 @@ import { ContextMenuProps } from '../ContextMenuItem';
 import { ViewBoxAnnotatableComponent } from '../DocComponent';
 import { PinDocView, PinProps } from '../PinFuncs';
 import './AudioBox.scss';
-import { OpenWhere } from './DocumentView';
 import { FieldView, FieldViewProps } from './FieldView';
+import { OpenWhere } from './OpenWhere';
 
 /**
  * AudioBox
@@ -761,3 +763,8 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
         );
     }
 }
+
+Docs.Prototypes.TemplateMap.set(DocumentType.AUDIO, {
+    layout: { view: AudioBox, dataField: 'data' },
+    options: { acl: '', _height: 100, _layout_fitWidth: true, _layout_reflowHorizontal: true, _layout_reflowVertical: true, _layout_nativeDimEditable: true, systemIcon: 'BsFillVolumeUpFill' },
+});
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
index 9c4d748bd..691d07e31 100644
--- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
@@ -11,14 +11,16 @@ import { ComputedField } from '../../../fields/ScriptField';
 import { Cast, NumCast, StrCast } from '../../../fields/Types';
 import { TraceMobx } from '../../../fields/util';
 import { DocumentManager } from '../../util/DocumentManager';
+import { DragManager } from '../../util/DragManager';
 import { ScriptingGlobals } from '../../util/ScriptingGlobals';
 import { SelectionManager } from '../../util/SelectionManager';
 import { DocComponent } from '../DocComponent';
 import { StyleProp } from '../StyleProvider';
 import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView';
 import './CollectionFreeFormDocumentView.scss';
-import { DocumentView, DocumentViewProps, OpenWhere } from './DocumentView';
+import { DocumentView, DocumentViewProps } from './DocumentView';
 import { FieldViewProps } from './FieldView';
+import { OpenWhere } from './OpenWhere';
 
 /// 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
@@ -48,7 +50,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
     get displayName() { // this makes mobx trace() statements more descriptive
         return 'CollectionFreeFormDocumentView(' + this.Document.title + ')';
     } // prettier-ignore
-    public static CollectionFreeFormDocViewClassName = 'collectionFreeFormDocumentView-container';
+    public static CollectionFreeFormDocViewClassName = DragManager.dragClassName;
     public static animFields: { key: string; val?: number }[] = [
         { key: 'x' },
         { key: 'y' },
diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx
index bc151f6a5..c42ca4468 100644
--- a/src/client/views/nodes/ComparisonBox.tsx
+++ b/src/client/views/nodes/ComparisonBox.tsx
@@ -7,12 +7,14 @@ import { emptyFunction } from '../../../Utils';
 import { Doc, Opt } from '../../../fields/Doc';
 import { RichTextField } from '../../../fields/RichTextField';
 import { DocCast, NumCast, RTFCast, StrCast, toList } from '../../../fields/Types';
-import { DocUtils, Docs } from '../../documents/Documents';
+import { DocUtils } from '../../documents/DocUtils';
+import { DocumentType } from '../../documents/DocumentTypes';
+import { Docs } from '../../documents/Documents';
 import { DragManager } from '../../util/DragManager';
 import { dropActionType } from '../../util/DropActionTypes';
 import { undoBatch } from '../../util/UndoManager';
 import { ViewBoxAnnotatableComponent, ViewBoxInterface } from '../DocComponent';
-import { PinProps, PinDocView } from '../PinFuncs';
+import { PinDocView, PinProps } from '../PinFuncs';
 import { StyleProp } from '../StyleProvider';
 import './ComparisonBox.scss';
 import { DocumentView } from './DocumentView';
@@ -272,3 +274,18 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
         );
     }
 }
+
+Docs.Prototypes.TemplateMap.set(DocumentType.COMPARISON, {
+    data: '',
+    layout: { view: ComparisonBox, dataField: 'data' },
+    options: {
+        acl: '',
+        backgroundColor: 'gray',
+        dropAction: dropActionType.move,
+        waitForDoubleClickToClick: 'always',
+        _layout_reflowHorizontal: true,
+        _layout_reflowVertical: true,
+        _layout_nativeDimEditable: true,
+        systemIcon: 'BsLayoutSplit',
+    },
+});
diff --git a/src/client/views/nodes/DataVizBox/DataVizBox.tsx b/src/client/views/nodes/DataVizBox/DataVizBox.tsx
index 113a857c3..ecfdcc229 100644
--- a/src/client/views/nodes/DataVizBox/DataVizBox.tsx
+++ b/src/client/views/nodes/DataVizBox/DataVizBox.tsx
@@ -13,22 +13,26 @@ import { List } from '../../../../fields/List';
 import { Cast, CsvCast, DocCast, NumCast, StrCast } from '../../../../fields/Types';
 import { CsvField } from '../../../../fields/URLField';
 import { TraceMobx } from '../../../../fields/util';
-import { DocUtils, Docs } from '../../../documents/Documents';
+import { DocUtils } from '../../../documents/DocUtils';
+import { DocumentType } from '../../../documents/DocumentTypes';
+import { Docs } from '../../../documents/Documents';
 import { DocumentManager } from '../../../util/DocumentManager';
 import { UndoManager, undoable } from '../../../util/UndoManager';
-import { PinProps, ViewBoxAnnotatableComponent, ViewBoxInterface } from '../../DocComponent';
+import { ContextMenu } from '../../ContextMenu';
+import { ViewBoxAnnotatableComponent, ViewBoxInterface } from '../../DocComponent';
 import { MarqueeAnnotator } from '../../MarqueeAnnotator';
+import { PinProps } from '../../PinFuncs';
 import { SidebarAnnos } from '../../SidebarAnnos';
 import { AnchorMenu } from '../../pdf/AnchorMenu';
 import { GPTPopup, GPTPopupMode } from '../../pdf/GPTPopup/GPTPopup';
 import { DocumentView } from '../DocumentView';
-import { FieldView, FieldViewProps, FocusViewOptions } from '../FieldView';
+import { FieldView, FieldViewProps } from '../FieldView';
+import { FocusViewOptions } from '../FocusViewOptions';
 import './DataVizBox.scss';
 import { Histogram } from './components/Histogram';
 import { LineChart } from './components/LineChart';
 import { PieChart } from './components/PieChart';
 import { TableBox } from './components/TableBox';
-import { ContextMenu } from '../../ContextMenu';
 
 export enum DataVizView {
     TABLE = 'table',
@@ -522,3 +526,18 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() im
         );
     }
 }
+Docs.Prototypes.TemplateMap.set(DocumentType.DATAVIZ, {
+    layout: { view: DataVizBox, dataField: 'data' },
+    options: {
+        acl: '',
+        dataViz_title: '',
+        dataViz_line: '',
+        dataViz_pie: '',
+        dataViz_histogram: '',
+        dataViz: 'table',
+        _layout_fitWidth: true,
+        _layout_reflowHorizontal: true,
+        _layout_reflowVertical: true,
+        _layout_nativeDimEditable: true,
+    },
+});
diff --git a/src/client/views/nodes/DocumentLinksButton.tsx b/src/client/views/nodes/DocumentLinksButton.tsx
index 9b4e36509..977899589 100644
--- a/src/client/views/nodes/DocumentLinksButton.tsx
+++ b/src/client/views/nodes/DocumentLinksButton.tsx
@@ -10,13 +10,13 @@ import { emptyFunction } from '../../../Utils';
 import { Doc } from '../../../fields/Doc';
 import { DocData } from '../../../fields/DocSymbols';
 import { StrCast } from '../../../fields/Types';
-import { DocUtils } from '../../documents/Documents';
+import { DocUtils } from '../../documents/DocUtils';
 import { DragManager } from '../../util/DragManager';
 import { Hypothesis } from '../../util/HypothesisUtils';
 import { LinkManager } from '../../util/LinkManager';
 import { UndoManager, undoBatch } from '../../util/UndoManager';
-import { PinProps } from '../DocComponent';
 import { ObservableReactComponent } from '../ObservableReactComponent';
+import { PinProps } from '../PinFuncs';
 import './DocumentLinksButton.scss';
 import { DocumentView } from './DocumentView';
 import { LinkDescriptionPopup } from './LinkDescriptionPopup';
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index a603de10b..e8d1e582e 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -16,20 +16,20 @@ import { InkTool } from '../../../fields/InkField';
 import { List } from '../../../fields/List';
 import { PrefetchProxy } from '../../../fields/Proxy';
 import { listSpec } from '../../../fields/Schema';
-import { ScriptField } from '../../../fields/ScriptField';
+import { FollowLinkScript, ScriptField } from '../../../fields/ScriptField';
 import { BoolCast, Cast, DocCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
 import { AudioField } from '../../../fields/URLField';
 import { GetEffectiveAcl, TraceMobx } from '../../../fields/util';
 import { DocServer } from '../../DocServer';
 import { Networking } from '../../Network';
+import { DocUtils } from '../../documents/DocUtils';
 import { CollectionViewType, DocumentType } from '../../documents/DocumentTypes';
-import { DocUtils, Docs } from '../../documents/Documents';
+import { Docs } from '../../documents/Documents';
 import { DictationManager } from '../../util/DictationManager';
 import { DocumentManager } from '../../util/DocumentManager';
 import { DragManager } from '../../util/DragManager';
 import { dropActionType } from '../../util/DropActionTypes';
 import { MakeTemplate, makeUserTemplateButton } from '../../util/DropConverter';
-import { FollowLinkScript } from '../../util/LinkFollower';
 import { LinkManager, UPDATE_SERVER_CACHE } from '../../util/LinkManager';
 import { ScriptingGlobals } from '../../util/ScriptingGlobals';
 import { SearchUtil } from '../../util/SearchUtil';
@@ -48,38 +48,14 @@ import { AudioAnnoState, StyleProp } from '../StyleProvider';
 import { DocumentContentsView, ObserverJsxParser } from './DocumentContentsView';
 import { DocumentLinksButton } from './DocumentLinksButton';
 import './DocumentView.scss';
-import { FieldViewProps, FieldViewSharedProps, FocusViewOptions } from './FieldView';
+import { FieldViewProps, FieldViewSharedProps } from './FieldView';
+import { FocusViewOptions } from './FocusViewOptions';
 import { KeyValueBox } from './KeyValueBox';
 import { LinkAnchorBox } from './LinkAnchorBox';
+import { OpenWhere } from './OpenWhere';
 import { FormattedTextBox } from './formattedText/FormattedTextBox';
 import { PresEffect, PresEffectDirection } from './trails';
 
-export enum OpenWhereMod {
-    none = '',
-    left = 'left',
-    right = 'right',
-    top = 'top',
-    bottom = 'bottom',
-    keyvalue = 'keyValue',
-}
-export enum OpenWhere {
-    lightbox = 'lightbox',
-    add = 'add',
-    addLeft = 'add:left',
-    addRight = 'add:right',
-    addBottom = 'add:bottom',
-    close = 'close',
-    toggle = 'toggle',
-    toggleRight = 'toggle:right',
-    replace = 'replace',
-    replaceRight = 'replace:right',
-    replaceLeft = 'replace:left',
-    inParent = 'inParent',
-    inParentFromScreen = 'inParentFromScreen',
-    overlay = 'overlay',
-    addRightKeyvalue = 'add:right:keyValue',
-}
-
 export interface DocumentViewProps extends FieldViewSharedProps {
     hideDecorations?: boolean; // whether to suppress all DocumentDecorations when doc is selected
     hideResizeHandles?: boolean; // whether to suppress resized handles on doc decorations when this document is selected
@@ -292,7 +268,8 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
             dragData.dropAction = dropAction;
             dragData.removeDocument = this._props.removeDocument;
             dragData.moveDocument = this._props.moveDocument;
-            dragData.draggedViews = [docView];
+            dragData.dragEnding = () => docView.props.dragEnding?.();
+            dragData.dragStarting = () => docView.props.dragStarting?.();
             dragData.canEmbed = !!(this.Document.dragAction ?? this._props.dragAction);
             (this._props.dragConfig ?? this._componentView?.dragConfig)?.(dragData);
             DragManager.StartDocumentDrag(
diff --git a/src/client/views/nodes/EquationBox.tsx b/src/client/views/nodes/EquationBox.tsx
index 9c216cba4..32d08fbe7 100644
--- a/src/client/views/nodes/EquationBox.tsx
+++ b/src/client/views/nodes/EquationBox.tsx
@@ -3,10 +3,12 @@ import { action, makeObservable, reaction } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
 import { DivHeight, DivWidth } from '../../../ClientUtils';
-import { Id } from '../../../fields/FieldSymbols';
+import { Doc } from '../../../fields/Doc';
 import { NumCast, StrCast } from '../../../fields/Types';
 import { TraceMobx } from '../../../fields/util';
-import { DocUtils, Docs } from '../../documents/Documents';
+import { DocUtils } from '../../documents/DocUtils';
+import { DocumentType } from '../../documents/DocumentTypes';
+import { Docs } from '../../documents/Documents';
 import { undoBatch } from '../../util/UndoManager';
 import { ViewBoxBaseComponent } from '../DocComponent';
 import { LightboxView } from '../LightboxView';
@@ -19,7 +21,6 @@ export class EquationBox extends ViewBoxBaseComponent<FieldViewProps>() {
     public static LayoutString(fieldKey: string) {
         return FieldView.LayoutString(EquationBox, fieldKey);
     }
-    public static SelectOnLoad: string = '';
     _ref: React.RefObject<EquationEditor> = React.createRef();
 
     constructor(props: FieldViewProps) {
@@ -29,12 +30,12 @@ export class EquationBox extends ViewBoxBaseComponent<FieldViewProps>() {
 
     componentDidMount() {
         this._props.setContentViewBox?.(this);
-        if (EquationBox.SelectOnLoad === this.Document[Id] && (!LightboxView.LightboxDoc || LightboxView.Contains(this.DocumentView?.()))) {
+        if (Doc.SelectOnLoad === this.Document && (!LightboxView.LightboxDoc || LightboxView.Contains(this.DocumentView?.()))) {
             this._props.select(false);
 
             this._ref.current!.mathField.focus();
             this.dataDoc.text === 'x' && this._ref.current!.mathField.select();
-            EquationBox.SelectOnLoad = '';
+            Doc.SetSelectOnLoad(undefined);
         }
         reaction(
             () => StrCast(this.dataDoc.text),
@@ -69,7 +70,7 @@ export class EquationBox extends ViewBoxBaseComponent<FieldViewProps>() {
                 x: NumCast(this.layoutDoc.x),
                 y: NumCast(this.layoutDoc.y) + _height + 10,
             });
-            EquationBox.SelectOnLoad = nextEq[Id];
+            Doc.SetSelectOnLoad(nextEq);
             this._props.addDocument?.(nextEq);
             e.stopPropagation();
         }
@@ -130,3 +131,8 @@ export class EquationBox extends ViewBoxBaseComponent<FieldViewProps>() {
         );
     }
 }
+
+Docs.Prototypes.TemplateMap.set(DocumentType.EQUATION, {
+    layout: { view: EquationBox, dataField: 'text' },
+    options: { acl: '', fontSize: '14px', _layout_reflowHorizontal: true, _layout_reflowVertical: true, _layout_nativeDimEditable: true, layout_hideDecorationTitle: true, systemIcon: 'BsCalculatorFill' }, // systemIcon: 'BsSuperscript' + BsSubscript
+});
diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx
index ab0850790..c6c77d8d2 100644
--- a/src/client/views/nodes/FieldView.tsx
+++ b/src/client/views/nodes/FieldView.tsx
@@ -10,29 +10,12 @@ import { ScriptField } from '../../../fields/ScriptField';
 import { WebField } from '../../../fields/URLField';
 import { dropActionType } from '../../util/DropActionTypes';
 import { Transform } from '../../util/Transform';
-import { PinProps, ViewBoxInterface } from '../DocComponent';
-import { DocumentView, OpenWhere } from './DocumentView';
+import { ViewBoxInterface } from '../DocComponent';
+import { PinProps } from '../PinFuncs';
+import { DocumentView } from './DocumentView';
+import { FocusViewOptions } from './FocusViewOptions';
+import { OpenWhere } from './OpenWhere';
 
-export interface FocusViewOptions {
-    willPan?: boolean; // determines whether to pan to target document
-    willZoomCentered?: boolean; // determines whether to zoom in on target document.  if zoomScale is 0, this just centers the document
-    zoomScale?: number; // percent of containing frame to zoom into document
-    zoomTime?: number;
-    didMove?: boolean; // whether a document was changed during the showDocument process
-    docTransform?: Transform; // when a document can't be panned and zoomed within its own container (say a group), then we need to continue to move up the render hierarchy to find something that can pan and zoom.  when this happens the docTransform must accumulate all the transforms of each level of the hierarchy
-    instant?: boolean; // whether focus should happen instantly (as opposed to smooth zoom)
-    preview?: boolean; // whether changes should be previewed by the componentView or written to the document
-    effect?: Doc; // animation effect for focus // bcz: needs to be changed to something more generic than a Doc
-    noSelect?: boolean; // whether target should be selected after focusing
-    playAudio?: boolean; // whether to play audio annotation on focus
-    playMedia?: boolean; // whether to play start target videos
-    openLocation?: OpenWhere; // where to open a missing document
-    zoomTextSelections?: boolean; // whether to display a zoomed overlay of anchor text selections
-    toggleTarget?: boolean; // whether to toggle target on and off
-    easeFunc?: 'linear' | 'ease'; // transition method for scrolling
-    pointFocus?: { X: number; Y: number }; // clientX and clientY coordinates to focus on instead of a document target (used by explore mode)
-    contextPath?: Doc[]; // path of inner documents that will also be focused
-}
 export type FocusFuncType = (doc: Doc, options: FocusViewOptions) => Opt<number>;
 // eslint-disable-next-line no-use-before-define
 export type StyleProviderFuncType = (doc: Opt<Doc>, props: Opt<FieldViewProps>, property: string) => any;
diff --git a/src/client/views/nodes/FocusViewOptions.ts b/src/client/views/nodes/FocusViewOptions.ts
new file mode 100644
index 000000000..bb0d2b03c
--- /dev/null
+++ b/src/client/views/nodes/FocusViewOptions.ts
@@ -0,0 +1,24 @@
+import { Doc } from '../../../fields/Doc';
+import { Transform } from '../../util/Transform';
+import { OpenWhere } from './OpenWhere';
+
+export interface FocusViewOptions {
+    willPan?: boolean; // determines whether to pan to target document
+    willZoomCentered?: boolean; // determines whether to zoom in on target document.  if zoomScale is 0, this just centers the document
+    zoomScale?: number; // percent of containing frame to zoom into document
+    zoomTime?: number;
+    didMove?: boolean; // whether a document was changed during the showDocument process
+    docTransform?: Transform; // when a document can't be panned and zoomed within its own container (say a group), then we need to continue to move up the render hierarchy to find something that can pan and zoom.  when this happens the docTransform must accumulate all the transforms of each level of the hierarchy
+    instant?: boolean; // whether focus should happen instantly (as opposed to smooth zoom)
+    preview?: boolean; // whether changes should be previewed by the componentView or written to the document
+    effect?: Doc; // animation effect for focus // bcz: needs to be changed to something more generic than a Doc
+    noSelect?: boolean; // whether target should be selected after focusing
+    playAudio?: boolean; // whether to play audio annotation on focus
+    playMedia?: boolean; // whether to play start target videos
+    openLocation?: OpenWhere; // where to open a missing document
+    zoomTextSelections?: boolean; // whether to display a zoomed overlay of anchor text selections
+    toggleTarget?: boolean; // whether to toggle target on and off
+    easeFunc?: 'linear' | 'ease'; // transition method for scrolling
+    pointFocus?: { X: number; Y: number }; // clientX and clientY coordinates to focus on instead of a document target (used by explore mode)
+    contextPath?: Doc[]; // path of inner documents that will also be focused
+}
diff --git a/src/client/views/nodes/FontIconBox/FontIconBox.tsx b/src/client/views/nodes/FontIconBox/FontIconBox.tsx
index 1b2aefbe2..d83690cdd 100644
--- a/src/client/views/nodes/FontIconBox/FontIconBox.tsx
+++ b/src/client/views/nodes/FontIconBox/FontIconBox.tsx
@@ -9,6 +9,7 @@ import { ClientUtils, returnTrue, setupMoveUpEvents } from '../../../../ClientUt
 import { Doc, DocListCast, StrListCast } from '../../../../fields/Doc';
 import { BoolCast, DocCast, NumCast, ScriptCast, StrCast } from '../../../../fields/Types';
 import { emptyFunction } from '../../../../Utils';
+import { Docs } from '../../../documents/Documents';
 import { CollectionViewType, DocumentType } from '../../../documents/DocumentTypes';
 import { SelectionManager } from '../../../util/SelectionManager';
 import { SettingsManager } from '../../../util/SettingsManager';
@@ -18,8 +19,8 @@ import { ViewBoxBaseComponent } from '../../DocComponent';
 import { EditableView } from '../../EditableView';
 import { SelectedDocView } from '../../selectedDoc';
 import { StyleProp } from '../../StyleProvider';
-import { OpenWhere } from '../DocumentView';
 import { FieldView, FieldViewProps } from '../FieldView';
+import { OpenWhere } from '../OpenWhere';
 import './FontIconBox.scss';
 import TrailsIcon from './TrailsIcon';
 
@@ -51,15 +52,6 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() {
         super(props);
         makeObservable(this);
     }
-    //
-    // This controls whether fontIconButtons will display labels under their icons or not
-    //
-    public static get ShowIconLabels() {
-        return BoolCast(Doc.UserDoc()._showLabel);
-    }
-    public static set ShowIconLabels(show: boolean) {
-        Doc.UserDoc()._showLabel = show;
-    }
 
     @observable noTooltip = false;
 
@@ -397,3 +389,8 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() {
         );
     }
 }
+
+Docs.Prototypes.TemplateMap.set(DocumentType.FONTICON, {
+    layout: { view: FontIconBox, dataField: 'icon' },
+    options: { acl: '', defaultDoubleClick: 'ignore', waitForDoubleClickToClick: 'never', layout_hideContextMenu: true, layout_hideLinkButton: true, _width: 40, _height: 40 },
+});
diff --git a/src/client/views/nodes/FunctionPlotBox.tsx b/src/client/views/nodes/FunctionPlotBox.tsx
index 31faa7ac3..f32d39aaf 100644
--- a/src/client/views/nodes/FunctionPlotBox.tsx
+++ b/src/client/views/nodes/FunctionPlotBox.tsx
@@ -7,12 +7,14 @@ import { List } from '../../../fields/List';
 import { listSpec } from '../../../fields/Schema';
 import { Cast, StrCast } from '../../../fields/Types';
 import { TraceMobx } from '../../../fields/util';
-import { DocUtils, Docs } from '../../documents/Documents';
+import { DocUtils } from '../../documents/DocUtils';
+import { DocumentType } from '../../documents/DocumentTypes';
+import { Docs } from '../../documents/Documents';
 import { DragManager } from '../../util/DragManager';
 import { LinkManager } from '../../util/LinkManager';
 import { undoBatch } from '../../util/UndoManager';
 import { ViewBoxAnnotatableComponent } from '../DocComponent';
-import { PinProps, PinDocView } from '../PinFuncs';
+import { PinDocView, PinProps } from '../PinFuncs';
 import { FieldView, FieldViewProps } from './FieldView';
 
 @observer
@@ -138,3 +140,8 @@ export class FunctionPlotBox extends ViewBoxAnnotatableComponent<FieldViewProps>
         );
     }
 }
+
+Docs.Prototypes.TemplateMap.set(DocumentType.FUNCPLOT, {
+    layout: { view: FunctionPlotBox, dataField: 'data' },
+    options: { acl: '', _layout_reflowHorizontal: true, _layout_reflowVertical: true, _layout_nativeDimEditable: true },
+});
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 8bd5ab03d..d317f46bb 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -15,8 +15,9 @@ import { Cast, ImageCast, NumCast, StrCast } from '../../../fields/Types';
 import { ImageField } from '../../../fields/URLField';
 import { TraceMobx } from '../../../fields/util';
 import { emptyFunction } from '../../../Utils';
-import { Docs, DocUtils } from '../../documents/Documents';
+import { Docs } from '../../documents/Documents';
 import { DocumentType } from '../../documents/DocumentTypes';
+import { DocUtils } from '../../documents/DocUtils';
 import { Networking } from '../../Network';
 import { DocumentManager } from '../../util/DocumentManager';
 import { DragManager } from '../../util/DragManager';
@@ -30,9 +31,10 @@ import { OverlayView } from '../OverlayView';
 import { AnchorMenu } from '../pdf/AnchorMenu';
 import { PinDocView, PinProps } from '../PinFuncs';
 import { StyleProp } from '../StyleProvider';
-import { OpenWhere } from './DocumentView';
-import { FieldView, FieldViewProps, FocusViewOptions } from './FieldView';
+import { FieldView, FieldViewProps } from './FieldView';
+import { FocusViewOptions } from './FocusViewOptions';
 import './ImageBox.scss';
+import { OpenWhere } from './OpenWhere';
 
 export class ImageEditorData {
     // eslint-disable-next-line no-use-before-define
@@ -546,3 +548,8 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl
         input.click();
     };
 }
+
+Docs.Prototypes.TemplateMap.set(DocumentType.IMG, {
+    layout: { view: ImageBox, dataField: 'data' },
+    options: { acl: '', freeform: '', systemIcon: 'BsFileEarmarkImageFill' },
+});
diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx
index b8296ce51..46bb16e50 100644
--- a/src/client/views/nodes/KeyValueBox.tsx
+++ b/src/client/views/nodes/KeyValueBox.tsx
@@ -3,12 +3,13 @@ import { action, computed, makeObservable, observable } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
 import { returnAlways, returnTrue } from '../../../ClientUtils';
-import { Doc, Field, FieldType, FieldResult } from '../../../fields/Doc';
+import { Doc, Field, FieldResult, FieldType } from '../../../fields/Doc';
 import { List } from '../../../fields/List';
 import { RichTextField } from '../../../fields/RichTextField';
 import { ComputedField, ScriptField } from '../../../fields/ScriptField';
 import { DocCast } from '../../../fields/Types';
 import { ImageField } from '../../../fields/URLField';
+import { DocumentType } from '../../documents/DocumentTypes';
 import { Docs } from '../../documents/Documents';
 import { SetupDrag } from '../../util/DragManager';
 import { CompiledScript } from '../../util/Scripting';
@@ -17,11 +18,11 @@ import { ContextMenu } from '../ContextMenu';
 import { ContextMenuProps } from '../ContextMenuItem';
 import { ObservableReactComponent } from '../ObservableReactComponent';
 import { DocumentIconContainer } from './DocumentIcon';
-import { OpenWhere } from './DocumentView';
 import { FieldView, FieldViewProps } from './FieldView';
 import { ImageBox } from './ImageBox';
 import './KeyValueBox.scss';
 import { KeyValuePair } from './KeyValuePair';
+import { OpenWhere } from './OpenWhere';
 import { FormattedTextBox } from './formattedText/FormattedTextBox';
 
 export type KVPScript = {
@@ -339,3 +340,8 @@ export class KeyValueBox extends ObservableReactComponent<FieldViewProps> {
         );
     }
 }
+
+Docs.Prototypes.TemplateMap.set(DocumentType.KVP, {
+    layout: { view: KeyValueBox, dataField: 'data' },
+    options: { acl: '', _layout_fitWidth: true, _height: 150 },
+});
diff --git a/src/client/views/nodes/KeyValuePair.tsx b/src/client/views/nodes/KeyValuePair.tsx
index a9aa017a1..878b0e54c 100644
--- a/src/client/views/nodes/KeyValuePair.tsx
+++ b/src/client/views/nodes/KeyValuePair.tsx
@@ -14,10 +14,11 @@ import { ContextMenu } from '../ContextMenu';
 import { EditableView } from '../EditableView';
 import { ObservableReactComponent } from '../ObservableReactComponent';
 import { DefaultStyleProvider } from '../StyleProvider';
-import { OpenWhere, returnEmptyDocViewList } from './DocumentView';
+import { returnEmptyDocViewList } from './DocumentView';
 import { KeyValueBox } from './KeyValueBox';
 import './KeyValueBox.scss';
 import './KeyValuePair.scss';
+import { OpenWhere } from './OpenWhere';
 
 // Represents one row in a key value plane
 
diff --git a/src/client/views/nodes/LabelBox.tsx b/src/client/views/nodes/LabelBox.tsx
index 89270652c..4dec3506c 100644
--- a/src/client/views/nodes/LabelBox.tsx
+++ b/src/client/views/nodes/LabelBox.tsx
@@ -5,13 +5,14 @@ import { Doc, DocListCast, Field, FieldType } from '../../../fields/Doc';
 import { List } from '../../../fields/List';
 import { listSpec } from '../../../fields/Schema';
 import { BoolCast, Cast, NumCast, StrCast } from '../../../fields/Types';
+import { DocumentType } from '../../documents/DocumentTypes';
 import { Docs } from '../../documents/Documents';
 import { DragManager } from '../../util/DragManager';
 import { undoBatch } from '../../util/UndoManager';
 import { ContextMenu } from '../ContextMenu';
 import { ContextMenuProps } from '../ContextMenuItem';
 import { ViewBoxBaseComponent } from '../DocComponent';
-import { PinProps, PinDocView } from '../PinFuncs';
+import { PinDocView, PinProps } from '../PinFuncs';
 import { StyleProp } from '../StyleProvider';
 import { FieldView, FieldViewProps } from './FieldView';
 import BigText from './LabelBigText';
@@ -203,3 +204,12 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps>() {
         );
     }
 }
+
+Docs.Prototypes.TemplateMap.set(DocumentType.LABEL, {
+    layout: { view: LabelBox, dataField: 'title' },
+    options: { acl: '', _singleLine: true, _layout_nativeDimEditable: true, _layout_reflowHorizontal: true, _layout_reflowVertical: true },
+});
+Docs.Prototypes.TemplateMap.set(DocumentType.BUTTON, {
+    layout: { view: LabelBox, dataField: 'title' },
+    options: { acl: '', _layout_nativeDimEditable: true, _layout_reflowHorizontal: true, _layout_reflowVertical: true },
+});
diff --git a/src/client/views/nodes/LinkBox.tsx b/src/client/views/nodes/LinkBox.tsx
index f01905ee1..559b1fcae 100644
--- a/src/client/views/nodes/LinkBox.tsx
+++ b/src/client/views/nodes/LinkBox.tsx
@@ -7,9 +7,12 @@ import { DashColor, lightOrDark, returnFalse } from '../../../ClientUtils';
 import { FieldResult } from '../../../fields/Doc';
 import { DocCss, DocData } from '../../../fields/DocSymbols';
 import { Id } from '../../../fields/FieldSymbols';
+import { List } from '../../../fields/List';
 import { DocCast, NumCast, StrCast } from '../../../fields/Types';
 import { TraceMobx } from '../../../fields/util';
 import { emptyFunction } from '../../../Utils';
+import { Docs } from '../../documents/Documents';
+import { DocumentType } from '../../documents/DocumentTypes';
 import { DocumentManager } from '../../util/DocumentManager';
 import { SnappingManager } from '../../util/SnappingManager';
 import { ViewBoxBaseComponent } from '../DocComponent';
@@ -263,3 +266,18 @@ export class LinkBox extends ViewBoxBaseComponent<FieldViewProps>() {
         );
     }
 }
+
+Docs.Prototypes.TemplateMap.set(DocumentType.LINK, {
+    layout: { view: LinkBox, dataField: 'link' },
+    options: {
+        acl: '',
+        childDontRegisterViews: true,
+        layout_hideLinkAnchors: true,
+        _height: 1,
+        _width: 1,
+        link: '',
+        link_description: '',
+        color: 'lightBlue', // lightblue is default color for linking dot and link documents text comment area
+        _dropPropertiesToRemove: new List(['onClick']),
+    },
+});
diff --git a/src/client/views/nodes/LinkDocPreview.tsx b/src/client/views/nodes/LinkDocPreview.tsx
index a9cfe6c0e..0936acc15 100644
--- a/src/client/views/nodes/LinkDocPreview.tsx
+++ b/src/client/views/nodes/LinkDocPreview.tsx
@@ -19,9 +19,10 @@ import { SearchUtil } from '../../util/SearchUtil';
 import { SettingsManager } from '../../util/SettingsManager';
 import { Transform } from '../../util/Transform';
 import { ObservableReactComponent } from '../ObservableReactComponent';
-import { DocumentView, OpenWhere } from './DocumentView';
+import { DocumentView } from './DocumentView';
 import { StyleProviderFuncType } from './FieldView';
 import './LinkDocPreview.scss';
+import { OpenWhere } from './OpenWhere';
 
 interface LinkDocPreviewProps {
     linkDoc?: Doc;
diff --git a/src/client/views/nodes/LoadingBox.tsx b/src/client/views/nodes/LoadingBox.tsx
index 501831bca..aa89398f3 100644
--- a/src/client/views/nodes/LoadingBox.tsx
+++ b/src/client/views/nodes/LoadingBox.tsx
@@ -10,6 +10,8 @@ import { DocumentManager } from '../../util/DocumentManager';
 import { ViewBoxAnnotatableComponent } from '../DocComponent';
 import { FieldView, FieldViewProps } from './FieldView';
 import './LoadingBox.scss';
+import { DocumentType } from '../../documents/DocumentTypes';
+import { Docs } from '../../documents/Documents';
 
 /**
  * LoadingBox Class represents a placeholder doc for documents that are currently
@@ -89,3 +91,7 @@ export class LoadingBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
         );
     }
 }
+Docs.Prototypes.TemplateMap.set(DocumentType.LOADING, {
+    layout: { view: LoadingBox, dataField: '' },
+    options: { acl: '', _layout_fitWidth: true, _layout_nativeDimEditable: true },
+});
diff --git a/src/client/views/nodes/MapBox/MapBox.tsx b/src/client/views/nodes/MapBox/MapBox.tsx
index 60dad314f..50831f8ea 100644
--- a/src/client/views/nodes/MapBox/MapBox.tsx
+++ b/src/client/views/nodes/MapBox/MapBox.tsx
@@ -18,20 +18,21 @@ import { ClientUtils, setupMoveUpEvents } from '../../../../ClientUtils';
 import { emptyFunction } from '../../../../Utils';
 import { Doc, DocListCast, Field, LinkedTo, Opt } from '../../../../fields/Doc';
 import { DocCast, NumCast, StrCast, toList } from '../../../../fields/Types';
+import { DocUtils } from '../../../documents/DocUtils';
 import { DocumentType } from '../../../documents/DocumentTypes';
-import { DocUtils, Docs } from '../../../documents/Documents';
+import { Docs } from '../../../documents/Documents';
 import { DocumentManager } from '../../../util/DocumentManager';
 import { DragManager } from '../../../util/DragManager';
 import { LinkManager } from '../../../util/LinkManager';
 import { UndoManager, undoable } from '../../../util/UndoManager';
 import { ViewBoxAnnotatableComponent, ViewBoxInterface } from '../../DocComponent';
-import { PinProps, PinDocView } from '../../PinFuncs';
+import { PinDocView, PinProps } from '../../PinFuncs';
 import { SidebarAnnos } from '../../SidebarAnnos';
 import { MarqueeOptionsMenu } from '../../collections/collectionFreeForm';
 import { Colors } from '../../global/globalEnums';
 import { DocumentView } from '../DocumentView';
-import { FieldView, FieldViewProps, FocusViewOptions } from '../FieldView';
-import { FormattedTextBox } from '../formattedText/FormattedTextBox';
+import { FieldView, FieldViewProps } from '../FieldView';
+import { FocusViewOptions } from '../FocusViewOptions';
 import { fastSpeedIcon, mediumSpeedIcon, slowSpeedIcon } from './AnimationSpeedIcons';
 import { AnimationSpeed, AnimationStatus, AnimationUtility } from './AnimationUtility';
 import { MapAnchorMenu } from './MapAnchorMenu';
@@ -355,7 +356,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
 
         const targetCreator = (annotationOn: Doc | undefined) => {
             const target = DocUtils.GetNewTextDoc('Note linked to ' + this.Document.title, 0, 0, 100, 100, annotationOn, 'yellow');
-            FormattedTextBox.SetSelectOnLoad(target);
+            Doc.SetSelectOnLoad(target);
             return target;
         };
         const docView = this.DocumentView?.();
@@ -1360,3 +1361,8 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
         );
     }
 }
+
+Docs.Prototypes.TemplateMap.set(DocumentType.MAP, {
+    layout: { view: MapBox, dataField: 'data' },
+    options: { acl: '', map: '', _height: 600, _width: 800, _layout_reflowHorizontal: true, _layout_reflowVertical: true, _layout_nativeDimEditable: true, systemIcon: 'BsFillPinMapFill' },
+});
diff --git a/src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx b/src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx
index 6ccbbbe1c..c69cd8e89 100644
--- a/src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx
+++ b/src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx
@@ -32,7 +32,7 @@
 //     addNoteClick = (e: React.PointerEvent) => {
 //         setupMoveUpEvents(this, e, returnFalse, emptyFunction, e => {
 //             const newDoc = Docs.Create.TextDocument('Note', { _layout_autoHeight: true });
-//             FormattedTextBox.SetSelectOnLoad(newDoc); // track the new text box so we can give it a prop that tells it to focus itself when it's displayed
+//             Doc.SetSelectOnLoad(newDoc); // track the new text box so we can give it a prop that tells it to focus itself when it's displayed
 //             Doc.AddDocToList(this.props.place, 'data', newDoc);
 //             this._stack?.scrollToBottom();
 //             e.stopPropagation();
diff --git a/src/client/views/nodes/MapBox/MapPushpinBox.tsx b/src/client/views/nodes/MapBox/MapPushpinBox.tsx
index 8ebc90157..f3dc44755 100644
--- a/src/client/views/nodes/MapBox/MapPushpinBox.tsx
+++ b/src/client/views/nodes/MapBox/MapPushpinBox.tsx
@@ -2,6 +2,8 @@ import * as React from 'react';
 import { ViewBoxBaseComponent } from '../../DocComponent';
 import { FieldView, FieldViewProps } from '../FieldView';
 import { MapBoxContainer } from '../MapboxMapBox/MapboxContainer';
+import { Docs } from '../../../documents/Documents';
+import { DocumentType } from '../../../documents/DocumentTypes';
 
 /**
  * Map Pushpin doc class
@@ -28,3 +30,8 @@ export class MapPushpinBox extends ViewBoxBaseComponent<FieldViewProps>() {
         return <div />;
     }
 }
+
+Docs.Prototypes.TemplateMap.set(DocumentType.PUSHPIN, {
+    layout: { view: MapPushpinBox, dataField: 'data' },
+    options: { acl: '' },
+});
diff --git a/src/client/views/nodes/MapboxMapBox/MapboxContainer.tsx b/src/client/views/nodes/MapboxMapBox/MapboxContainer.tsx
index d899fcb9a..3b4ffd4bd 100644
--- a/src/client/views/nodes/MapboxMapBox/MapboxContainer.tsx
+++ b/src/client/views/nodes/MapboxMapBox/MapboxContainer.tsx
@@ -10,23 +10,24 @@ import { Doc, DocListCast, Field, LinkedTo, Opt } from '../../../../fields/Doc';
 import { DocCss, Highlight } from '../../../../fields/DocSymbols';
 import { Id } from '../../../../fields/FieldSymbols';
 import { DocCast, NumCast, StrCast, toList } from '../../../../fields/Types';
+import { DocUtils } from '../../../documents/DocUtils';
 import { DocumentType } from '../../../documents/DocumentTypes';
-import { DocUtils, Docs } from '../../../documents/Documents';
+import { Docs } from '../../../documents/Documents';
 import { DocumentManager } from '../../../util/DocumentManager';
 import { DragManager } from '../../../util/DragManager';
 import { LinkManager } from '../../../util/LinkManager';
 import { Transform } from '../../../util/Transform';
 import { UndoManager, undoable } from '../../../util/UndoManager';
 import { ViewBoxAnnotatableComponent } from '../../DocComponent';
-import { PinProps, PinDocView } from '../../PinFuncs';
+import { PinDocView, PinProps } from '../../PinFuncs';
 import { SidebarAnnos } from '../../SidebarAnnos';
 import { MarqueeOptionsMenu } from '../../collections/collectionFreeForm';
 import { Colors } from '../../global/globalEnums';
 import { DocumentView } from '../DocumentView';
-import { FieldView, FieldViewProps, FocusViewOptions } from '../FieldView';
+import { FieldView, FieldViewProps } from '../FieldView';
+import { FocusViewOptions } from '../FocusViewOptions';
 import { MapAnchorMenu } from '../MapBox/MapAnchorMenu';
 import '../MapBox/MapBox.scss';
-import { FormattedTextBox } from '../formattedText/FormattedTextBox';
 
 /**
  * MapBox architecture:
@@ -237,7 +238,7 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent<FieldViewProps>
 
         const targetCreator = (annotationOn: Doc | undefined) => {
             const target = DocUtils.GetNewTextDoc('Note linked to ' + this.Document.title, 0, 0, 100, 100, annotationOn, 'yellow');
-            FormattedTextBox.SetSelectOnLoad(target);
+            Doc.SetSelectOnLoad(target);
             return target;
         };
         const docView = this.DocumentView?.();
diff --git a/src/client/views/nodes/OpenWhere.ts b/src/client/views/nodes/OpenWhere.ts
new file mode 100644
index 000000000..e2a5f1f2a
--- /dev/null
+++ b/src/client/views/nodes/OpenWhere.ts
@@ -0,0 +1,25 @@
+export enum OpenWhereMod {
+    none = '',
+    left = 'left',
+    right = 'right',
+    top = 'top',
+    bottom = 'bottom',
+    keyvalue = 'keyValue',
+}
+export enum OpenWhere {
+    lightbox = 'lightbox',
+    add = 'add',
+    addLeft = 'add:left',
+    addRight = 'add:right',
+    addBottom = 'add:bottom',
+    close = 'close',
+    toggle = 'toggle',
+    toggleRight = 'toggle:right',
+    replace = 'replace',
+    replaceRight = 'replace:right',
+    replaceLeft = 'replace:left',
+    inParent = 'inParent',
+    inParentFromScreen = 'inParentFromScreen',
+    overlay = 'overlay',
+    addRightKeyvalue = 'add:right:keyValue',
+}
diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx
index fbf5e018c..5d1874aca 100644
--- a/src/client/views/nodes/PDFBox.tsx
+++ b/src/client/views/nodes/PDFBox.tsx
@@ -16,8 +16,9 @@ import { Cast, FieldValue, ImageCast, NumCast, StrCast, toList } from '../../../
 import { ImageField, PdfField } from '../../../fields/URLField';
 import { TraceMobx } from '../../../fields/util';
 import { emptyFunction } from '../../../Utils';
-import { Docs, DocUtils } from '../../documents/Documents';
+import { Docs } from '../../documents/Documents';
 import { CollectionViewType, DocumentType } from '../../documents/DocumentTypes';
+import { DocUtils } from '../../documents/DocUtils';
 import { DocumentManager } from '../../util/DocumentManager';
 import { KeyCodes } from '../../util/KeyCodes';
 import { SelectionManager } from '../../util/SelectionManager';
@@ -31,9 +32,11 @@ import { Colors } from '../global/globalEnums';
 import { PDFViewer } from '../pdf/PDFViewer';
 import { PinDocView, PinProps } from '../PinFuncs';
 import { SidebarAnnos } from '../SidebarAnnos';
-import { DocumentView, OpenWhere } from './DocumentView';
-import { FieldView, FieldViewProps, FocusViewOptions } from './FieldView';
+import { DocumentView } from './DocumentView';
+import { FieldView, FieldViewProps } from './FieldView';
+import { FocusViewOptions } from './FocusViewOptions';
 import { ImageBox } from './ImageBox';
+import { OpenWhere } from './OpenWhere';
 import './PDFBox.scss';
 import { CreateImage } from './WebBoxRenderer';
 
@@ -665,3 +668,8 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
         return pdfView ?? this.renderTitleBox;
     }
 }
+
+Docs.Prototypes.TemplateMap.set(DocumentType.PDF, {
+    layout: { view: PDFBox, dataField: 'data' },
+    options: { acl: '', _layout_curPage: 1, _layout_fitWidth: true, _layout_nativeDimEditable: true, _layout_reflowVertical: true, systemIcon: 'BsFileEarmarkPdfFill' },
+});
diff --git a/src/client/views/nodes/PhysicsBox/PhysicsSimulationBox.tsx b/src/client/views/nodes/PhysicsBox/PhysicsSimulationBox.tsx
index ae674d604..f88eb3bca 100644
--- a/src/client/views/nodes/PhysicsBox/PhysicsSimulationBox.tsx
+++ b/src/client/views/nodes/PhysicsBox/PhysicsSimulationBox.tsx
@@ -1,3 +1,11 @@
+/* eslint-disable camelcase */
+/* eslint-disable jsx-a11y/control-has-associated-label */
+/* eslint-disable @typescript-eslint/no-unused-vars */
+/* eslint-disable jsx-a11y/no-noninteractive-element-interactions */
+/* eslint-disable jsx-a11y/click-events-have-key-events */
+/* eslint-disable react/no-array-index-key */
+/* eslint-disable react/jsx-props-no-spreading */
+/* eslint-disable no-return-assign */
 import ArrowLeftIcon from '@mui/icons-material/ArrowLeft';
 import ArrowRightIcon from '@mui/icons-material/ArrowRight';
 import PauseIcon from '@mui/icons-material/Pause';
@@ -13,13 +21,15 @@ import { NumListCast } from '../../../../fields/Doc';
 import { List } from '../../../../fields/List';
 import { BoolCast, NumCast, StrCast } from '../../../../fields/Types';
 import { ViewBoxAnnotatableComponent } from '../../DocComponent';
-import { FieldView, FieldViewProps } from './../FieldView';
+import { FieldView, FieldViewProps } from '../FieldView';
 import './PhysicsSimulationBox.scss';
 import InputField from './PhysicsSimulationInputField';
 import questions from './PhysicsSimulationQuestions.json';
 import tutorials from './PhysicsSimulationTutorial.json';
 import Wall from './PhysicsSimulationWall';
 import Weight from './PhysicsSimulationWeight';
+import { Docs } from '../../../documents/Documents';
+import { DocumentType } from '../../../documents/DocumentTypes';
 
 interface IWallProps {
     length: number;
@@ -204,7 +214,7 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP
 
     componentDidUpdate(prevProps: Readonly<FieldViewProps>) {
         super.componentDidUpdate(prevProps);
-        if (this.xMax !== this._props.PanelWidth() * 0.6 || this.yMax != this._props.PanelHeight()) {
+        if (this.xMax !== this._props.PanelWidth() * 0.6 || this.yMax !== this._props.PanelHeight()) {
             this.xMax = this._props.PanelWidth() * 0.6;
             this.yMax = this._props.PanelHeight();
             this.setupSimulation();
@@ -219,16 +229,16 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP
 
     @action
     setupSimulation = () => {
-        const simulationType = this.simulationType;
+        const { simulationType } = this;
         const mode = this.simulationMode;
         this.dataDoc.simulation_paused = true;
-        if (simulationType != 'Circular Motion') {
+        if (simulationType !== 'Circular Motion') {
             this.dataDoc.mass1_velocityXstart = 0;
             this.dataDoc.mass1_velocityYstart = 0;
             this.dataDoc.mass1_velocityX = 0;
             this.dataDoc.mass1_velocityY = 0;
         }
-        if (mode == 'Freeform') {
+        if (mode === 'Freeform') {
             this.dataDoc.simulation_showForceMagnitudes = true;
             // prettier-ignore
             switch (simulationType) {
@@ -247,9 +257,10 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP
                 case 'Circular Motion': this.setupCircular(20); break;
                 case 'Pulley':          this.setupPulley(); break;
                 case 'Suspension':      this.setupSuspension();break;
+                default:
             }
             this._simReset++;
-        } else if (mode == 'Review') {
+        } else if (mode === 'Review') {
             this.dataDoc.simulation_showComponentForces = false;
             this.dataDoc.simulation_showForceMagnitudes = true;
             this.dataDoc.simulation_showAcceleration = false;
@@ -265,12 +276,13 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP
                 case 'Circular Motion':  this.setupCircular(0); break;  // TODO - circular motion review problems
                 case 'Pulley':  this.setupPulley(); break; // TODO - pulley tutorial review problems
                 case 'Suspension': this.setupSuspension(); break;  // TODO - suspension tutorial review problems
+                default:
             }
-        } else if (mode == 'Tutorial') {
+        } else if (mode === 'Tutorial') {
             this.dataDoc.simulation_showComponentForces = false;
             this.dataDoc.tutorial_stepNumber = 0;
             this.dataDoc.simulation_showAcceleration = false;
-            if (this.simulationType != 'Circular Motion') {
+            if (this.simulationType !== 'Circular Motion') {
                 this.dataDoc.mass1_velocityX = 0;
                 this.dataDoc.mass1_velocityY = 0;
                 this.dataDoc.simulation_showVelocity = false;
@@ -333,6 +345,7 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP
                     this.dataDoc.mass1_forcesStart = JSON.stringify(tutorials.suspension.steps[0].forces);
                     this.dataDoc.simulation_showForceMagnitudes = tutorials.suspension.steps[0].showMagnitude;
                     break;
+                default:
             }
             this._simReset++;
         }
@@ -349,7 +362,7 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP
             magnitude: Math.abs(this.gravity) * Math.cos(Math.atan(height / width)) * this.mass1,
             directionInDegrees: 180 - 90 - (Math.atan(height / width) * 180) / Math.PI,
         };
-        let frictionForce: IForce = {
+        const frictionForce: IForce = {
             description: 'Static Friction Force',
             magnitude: coefficient * Math.abs(this.gravity) * Math.cos(Math.atan(height / width)) * this.mass1,
             directionInDegrees: 180 - (Math.atan(height / width) * 180) / Math.PI,
@@ -378,7 +391,7 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP
             directionInDegrees: 360 - (Math.atan(height / width) * 180) / Math.PI,
         };
         const gravityForce = this.gravityForce(this.mass1);
-        if (coefficient != 0) {
+        if (coefficient !== 0) {
             this.dataDoc.mass1_forcesStart = JSON.stringify([gravityForce, normalForce, frictionForce]);
             this.dataDoc.mass1_forcesUpdated = JSON.stringify([gravityForce, normalForce, frictionForce]);
             this.dataDoc.mass1_componentForces = JSON.stringify([frictionForce, normalForceComponent, gravityParallel, gravityPerpendicular]);
@@ -396,12 +409,12 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP
         this.dataDoc.wedge_height = Math.tan(radAng) * this.dataDoc.wedge_width;
 
         // update weight position based on updated wedge width/height
-        let yPos = this.yMax - this.dataDoc.wedge_height - this.mass1Radius * Math.cos(radAng) - this.mass1Radius;
-        let xPos = this.xMax * 0.25 + this.mass1Radius * Math.sin(radAng) - this.mass1Radius;
+        const yPos = this.yMax - this.dataDoc.wedge_height - this.mass1Radius * Math.cos(radAng) - this.mass1Radius;
+        const xPos = this.xMax * 0.25 + this.mass1Radius * Math.sin(radAng) - this.mass1Radius;
 
         this.dataDoc.mass1_positionXstart = xPos;
         this.dataDoc.mass1_positionYstart = yPos;
-        if (this.simulationMode == 'Freeform') {
+        if (this.simulationMode === 'Freeform') {
             this.updateForcesWithFriction(NumCast(this.dataDoc.coefficientOfStaticFriction), this.dataDoc.wedge_width, Math.tan(radAng) * this.dataDoc.wedge_width);
         }
     };
@@ -409,7 +422,7 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP
     // In review mode, update forces when coefficient of static friction changed
     updateReviewForcesBasedOnCoefficient = (coefficient: number) => {
         let theta = this.wedgeAngle;
-        let index = this.selectedQuestion.variablesForQuestionSetup.indexOf('theta - max 45');
+        const index = this.selectedQuestion.variablesForQuestionSetup.indexOf('theta - max 45');
         if (index >= 0) {
             theta = NumListCast(this.dataDoc.questionVariables)[index];
         }
@@ -467,26 +480,26 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP
             const description = question.answerSolutionDescriptions[i];
             if (!isNaN(NumCast(description))) {
                 solutions.push(NumCast(description));
-            } else if (description == 'solve normal force angle from wedge angle') {
+            } else if (description === 'solve normal force angle from wedge angle') {
                 solutions.push(90 - theta);
-            } else if (description == 'solve normal force magnitude from wedge angle') {
+            } else if (description === 'solve normal force magnitude from wedge angle') {
                 solutions.push(Math.abs(this.gravity) * Math.cos((theta / 180) * Math.PI));
-            } else if (description == 'solve static force magnitude from wedge angle given equilibrium') {
-                let normalForceMagnitude = Math.abs(this.gravity) * Math.cos((theta / 180) * Math.PI);
-                let normalForceAngle = 90 - theta;
-                let frictionForceAngle = 180 - theta;
-                let frictionForceMagnitude = (-normalForceMagnitude * Math.sin((normalForceAngle * Math.PI) / 180) + Math.abs(this.gravity)) / Math.sin((frictionForceAngle * Math.PI) / 180);
+            } else if (description === 'solve static force magnitude from wedge angle given equilibrium') {
+                const normalForceMagnitude = Math.abs(this.gravity) * Math.cos((theta / 180) * Math.PI);
+                const normalForceAngle = 90 - theta;
+                const frictionForceAngle = 180 - theta;
+                const frictionForceMagnitude = (-normalForceMagnitude * Math.sin((normalForceAngle * Math.PI) / 180) + Math.abs(this.gravity)) / Math.sin((frictionForceAngle * Math.PI) / 180);
                 solutions.push(frictionForceMagnitude);
-            } else if (description == 'solve static force angle from wedge angle given equilibrium') {
+            } else if (description === 'solve static force angle from wedge angle given equilibrium') {
                 solutions.push(180 - theta);
-            } else if (description == 'solve minimum static coefficient from wedge angle given equilibrium') {
-                let normalForceMagnitude = Math.abs(this.gravity) * Math.cos((theta / 180) * Math.PI);
-                let normalForceAngle = 90 - theta;
-                let frictionForceAngle = 180 - theta;
-                let frictionForceMagnitude = (-normalForceMagnitude * Math.sin((normalForceAngle * Math.PI) / 180) + Math.abs(this.gravity)) / Math.sin((frictionForceAngle * Math.PI) / 180);
-                let frictionCoefficient = frictionForceMagnitude / normalForceMagnitude;
+            } else if (description === 'solve minimum static coefficient from wedge angle given equilibrium') {
+                const normalForceMagnitude = Math.abs(this.gravity) * Math.cos((theta / 180) * Math.PI);
+                const normalForceAngle = 90 - theta;
+                const frictionForceAngle = 180 - theta;
+                const frictionForceMagnitude = (-normalForceMagnitude * Math.sin((normalForceAngle * Math.PI) / 180) + Math.abs(this.gravity)) / Math.sin((frictionForceAngle * Math.PI) / 180);
+                const frictionCoefficient = frictionForceMagnitude / normalForceMagnitude;
                 solutions.push(frictionCoefficient);
-            } else if (description == 'solve maximum wedge angle from coefficient of static friction given equilibrium') {
+            } else if (description === 'solve maximum wedge angle from coefficient of static friction given equilibrium') {
                 solutions.push((Math.atan(muS) * 180) / Math.PI);
             }
         }
@@ -497,38 +510,38 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP
     // In review mode, check if input answers match correct answers and optionally generate alert
     checkAnswers = (showAlert: boolean = true) => {
         let error: boolean = false;
-        let epsilon: number = 0.01;
+        const epsilon: number = 0.01;
         if (this.selectedQuestion) {
             for (let i = 0; i < this.selectedQuestion.answerParts.length; i++) {
-                if (this.selectedQuestion.answerParts[i] == 'force of gravity') {
+                if (this.selectedQuestion.answerParts[i] === 'force of gravity') {
                     if (Math.abs(NumCast(this.dataDoc.review_GravityMagnitude) - this.selectedSolutions[i]) > epsilon) {
                         error = true;
                     }
-                } else if (this.selectedQuestion.answerParts[i] == 'angle of gravity') {
+                } else if (this.selectedQuestion.answerParts[i] === 'angle of gravity') {
                     if (Math.abs(NumCast(this.dataDoc.review_GravityAngle) - this.selectedSolutions[i]) > epsilon) {
                         error = true;
                     }
-                } else if (this.selectedQuestion.answerParts[i] == 'normal force') {
+                } else if (this.selectedQuestion.answerParts[i] === 'normal force') {
                     if (Math.abs(NumCast(this.dataDoc.review_NormalMagnitude) - this.selectedSolutions[i]) > epsilon) {
                         error = true;
                     }
-                } else if (this.selectedQuestion.answerParts[i] == 'angle of normal force') {
+                } else if (this.selectedQuestion.answerParts[i] === 'angle of normal force') {
                     if (Math.abs(NumCast(this.dataDoc.review_NormalAngle) - this.selectedSolutions[i]) > epsilon) {
                         error = true;
                     }
-                } else if (this.selectedQuestion.answerParts[i] == 'force of static friction') {
+                } else if (this.selectedQuestion.answerParts[i] === 'force of static friction') {
                     if (Math.abs(NumCast(this.dataDoc.review_StaticMagnitude) - this.selectedSolutions[i]) > epsilon) {
                         error = true;
                     }
-                } else if (this.selectedQuestion.answerParts[i] == 'angle of static friction') {
+                } else if (this.selectedQuestion.answerParts[i] === 'angle of static friction') {
                     if (Math.abs(NumCast(this.dataDoc.review_StaticAngle) - this.selectedSolutions[i]) > epsilon) {
                         error = true;
                     }
-                } else if (this.selectedQuestion.answerParts[i] == 'coefficient of static friction') {
+                } else if (this.selectedQuestion.answerParts[i] === 'coefficient of static friction') {
                     if (Math.abs(NumCast(this.dataDoc.coefficientOfStaticFriction) - this.selectedSolutions[i]) > epsilon) {
                         error = true;
                     }
-                } else if (this.selectedQuestion.answerParts[i] == 'wedge angle') {
+                } else if (this.selectedQuestion.answerParts[i] === 'wedge angle') {
                     if (Math.abs(this.wedgeAngle - this.selectedSolutions[i]) > epsilon) {
                         error = true;
                     }
@@ -539,7 +552,7 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP
             this.dataDoc.simulation_paused = false;
             setTimeout(() => (this.dataDoc.simulation_paused = true), 3000);
         }
-        if (this.selectedQuestion.goal == 'noMovement') {
+        if (this.selectedQuestion.goal === 'noMovement') {
             this.dataDoc.noMovement = !error;
         }
     };
@@ -571,12 +584,12 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP
             let wedge_angle = 0;
 
             for (let i = 0; i < question.variablesForQuestionSetup.length; i++) {
-                if (question.variablesForQuestionSetup[i] == 'theta - max 45') {
-                    let randValue = Math.floor(Math.random() * 44 + 1);
+                if (question.variablesForQuestionSetup[i] === 'theta - max 45') {
+                    const randValue = Math.floor(Math.random() * 44 + 1);
                     vars.push(randValue);
                     wedge_angle = randValue;
-                } else if (question.variablesForQuestionSetup[i] == 'coefficient of static friction') {
-                    let randValue = Math.round(Math.random() * 1000) / 1000;
+                } else if (question.variablesForQuestionSetup[i] === 'coefficient of static friction') {
+                    const randValue = Math.round(Math.random() * 1000) / 1000;
                     vars.push(randValue);
                     coefficient = randValue;
                 }
@@ -589,7 +602,7 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP
         let q = '';
         for (let i = 0; i < question.questionSetup.length; i++) {
             q += question.questionSetup[i];
-            if (i != question.questionSetup.length - 1) {
+            if (i !== question.questionSetup.length - 1) {
                 q += vars[i];
                 if (question.variablesForQuestionSetup[i].includes('theta')) {
                     q += ' degree (≈' + Math.round((1000 * (vars[i] * Math.PI)) / 180) / 1000 + ' rad)';
@@ -601,7 +614,7 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP
         this.dataDoc.questionPartOne = q;
         this.dataDoc.questionPartTwo = question.question;
         this.dataDoc.answers = new List<number>(this.getAnswersToQuestion(question, vars));
-        //this.dataDoc.simulation_reset = (!this.dataDoc.simulation_reset);
+        // this.dataDoc.simulation_reset = (!this.dataDoc.simulation_reset);
     };
 
     // Default setup for uniform circular motion simulation
@@ -610,8 +623,8 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP
         this.dataDoc.simulation_showComponentForces = false;
         this.dataDoc.mass1_velocityYstart = 0;
         this.dataDoc.mass1_velocityXstart = value;
-        let xPos = (this.xMax + this.xMin) / 2 - this.mass1Radius;
-        let yPos = (this.yMax + this.yMin) / 2 + this.circularMotionRadius - this.mass1Radius;
+        const xPos = (this.xMax + this.xMin) / 2 - this.mass1Radius;
+        const yPos = (this.yMax + this.yMin) / 2 + this.circularMotionRadius - this.mass1Radius;
         this.dataDoc.mass1_positionYstart = yPos;
         this.dataDoc.mass1_positionXstart = xPos;
         const tensionForce: IForce = {
@@ -680,13 +693,13 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP
     // Default setup for suspension simulation
     @action
     setupSuspension = () => {
-        let xPos = (this.xMax + this.xMin) / 2 - this.mass1Radius;
-        let yPos = this.yMin + 200;
+        const xPos = (this.xMax + this.xMin) / 2 - this.mass1Radius;
+        const yPos = this.yMin + 200;
         this.dataDoc.mass1_positionYstart = yPos;
         this.dataDoc.mass1_positionXstart = xPos;
         this.dataDoc.mass1_positionY = this.getDisplayYPos(yPos);
         this.dataDoc.mass1_positionX = xPos;
-        let tensionMag = (this.mass1 * Math.abs(this.gravity)) / (2 * Math.sin(Math.PI / 4));
+        const tensionMag = (this.mass1 * Math.abs(this.gravity)) / (2 * Math.sin(Math.PI / 4));
         const tensionForce1: IForce = {
             description: 'Tension',
             magnitude: tensionMag,
@@ -891,7 +904,7 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP
                                     setVelocity={this.setVelocity1}
                                     setAcceleration={this.setAcceleration1}
                                 />
-                                {this.simulationType == 'Pulley' && (
+                                {this.simulationType === 'Pulley' && (
                                     <Weight
                                         {...commonWeightProps}
                                         color="green"
@@ -916,7 +929,7 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP
                                 )}
                             </div>
                             <div style={{ position: 'absolute', transformOrigin: 'top left', top: 0, left: 0, width: '100%', height: '100%' }}>
-                                {(this.simulationType == 'One Weight' || this.simulationType == 'Inclined Plane') &&
+                                {(this.simulationType === 'One Weight' || this.simulationType === 'Inclined Plane') &&
                                     this.wallPositions?.map((element, index) => <Wall key={index} length={element.length} xPos={element.xPos} yPos={element.yPos} angleInDegrees={element.angleInDegrees} />)}
                             </div>
                         </div>
@@ -927,17 +940,17 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP
                         style={{ overflow: 'auto', height: `${Math.max(1, 800 / this._props.PanelWidth()) * 100}%`, transform: `scale(${Math.min(1, this._props.PanelWidth() / 850)})` }}>
                         <div className="mechanicsSimulationControls">
                             <Stack direction="row" spacing={1}>
-                                {this.dataDoc.simulation_paused && this.simulationMode != 'Tutorial' && (
+                                {this.dataDoc.simulation_paused && this.simulationMode !== 'Tutorial' && (
                                     <IconButton onClick={() => (this.dataDoc.simulation_paused = false)}>
                                         <PlayArrowIcon />
                                     </IconButton>
                                 )}
-                                {!this.dataDoc.simulation_paused && this.simulationMode != 'Tutorial' && (
+                                {!this.dataDoc.simulation_paused && this.simulationMode !== 'Tutorial' && (
                                     <IconButton onClick={() => (this.dataDoc.simulation_paused = true)}>
                                         <PauseIcon />
                                     </IconButton>
                                 )}
-                                {this.dataDoc.simulation_paused && this.simulationMode != 'Tutorial' && (
+                                {this.dataDoc.simulation_paused && this.simulationMode !== 'Tutorial' && (
                                     <IconButton onClick={action(() => this._simReset++)}>
                                         <ReplayIcon />
                                     </IconButton>
@@ -974,15 +987,13 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP
                                 </select>
                             </div>
                         </div>
-                        {this.simulationMode == 'Review' && this.simulationType != 'Inclined Plane' && (
+                        {this.simulationMode === 'Review' && this.simulationType !== 'Inclined Plane' && (
                             <div className="wordProblemBox">
-                                <p>
-                                    <>{this.simulationType} review problems in progress!</>
-                                </p>
+                                <p>{this.simulationType} review problems in progress!</p>
                                 <hr />
                             </div>
                         )}
-                        {this.simulationMode == 'Review' && this.simulationType == 'Inclined Plane' && (
+                        {this.simulationMode === 'Review' && this.simulationType === 'Inclined Plane' && (
                             <div>
                                 {!this.dataDoc.hintDialogueOpen && (
                                     <IconButton
@@ -995,7 +1006,7 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP
                                         <QuestionMarkIcon />
                                     </IconButton>
                                 )}
-                                <Dialog maxWidth={'sm'} fullWidth={true} open={BoolCast(this.dataDoc.hintDialogueOpen)} onClose={() => (this.dataDoc.hintDialogueOpen = false)}>
+                                <Dialog maxWidth="sm" fullWidth open={BoolCast(this.dataDoc.hintDialogueOpen)} onClose={() => (this.dataDoc.hintDialogueOpen = false)}>
                                     <DialogTitle>Hints</DialogTitle>
                                     <DialogContent>
                                         {this.selectedQuestion.hints?.map((hint: any, index: number) => (
@@ -1030,12 +1041,12 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP
                                                 dataDoc={this.dataDoc}
                                                 prop="review_GravityMagnitude"
                                                 step={0.1}
-                                                unit={'N'}
+                                                unit="N"
                                                 upperBound={50}
                                                 value={NumCast(this.dataDoc.review_GravityMagnitude)}
                                                 showIcon={BoolCast(this.dataDoc.simulation_showIcon)}
                                                 correctValue={NumListCast(this.dataDoc.answers)[this.selectedQuestion.answerParts.indexOf('force of gravity')]}
-                                                labelWidth={'7em'}
+                                                labelWidth="7em"
                                             />
                                         )}
                                         {this.selectedQuestion.answerParts.includes('angle of gravity') && (
@@ -1045,13 +1056,13 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP
                                                 dataDoc={this.dataDoc}
                                                 prop="review_GravityAngle"
                                                 step={1}
-                                                unit={'°'}
+                                                unit="°"
                                                 upperBound={360}
                                                 value={NumCast(this.dataDoc.review_GravityAngle)}
-                                                radianEquivalent={true}
+                                                radianEquivalent
                                                 showIcon={BoolCast(this.dataDoc.simulation_showIcon)}
                                                 correctValue={NumListCast(this.dataDoc.answers)[this.selectedQuestion.answerParts.indexOf('angle of gravity')]}
-                                                labelWidth={'7em'}
+                                                labelWidth="7em"
                                             />
                                         )}
                                         {this.selectedQuestion.answerParts.includes('normal force') && (
@@ -1061,12 +1072,12 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP
                                                 dataDoc={this.dataDoc}
                                                 prop="review_NormalMagnitude"
                                                 step={0.1}
-                                                unit={'N'}
+                                                unit="N"
                                                 upperBound={50}
                                                 value={NumCast(this.dataDoc.review_NormalMagnitude)}
                                                 showIcon={BoolCast(this.dataDoc.simulation_showIcon)}
                                                 correctValue={NumListCast(this.dataDoc.answers)[this.selectedQuestion.answerParts.indexOf('normal force')]}
-                                                labelWidth={'7em'}
+                                                labelWidth="7em"
                                             />
                                         )}
                                         {this.selectedQuestion.answerParts.includes('angle of normal force') && (
@@ -1076,13 +1087,13 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP
                                                 dataDoc={this.dataDoc}
                                                 prop="review_NormalAngle"
                                                 step={1}
-                                                unit={'°'}
+                                                unit="°"
                                                 upperBound={360}
                                                 value={NumCast(this.dataDoc.review_NormalAngle)}
-                                                radianEquivalent={true}
+                                                radianEquivalent
                                                 showIcon={BoolCast(this.dataDoc.simulation_showIcon)}
                                                 correctValue={NumListCast(this.dataDoc.answers)[this.selectedQuestion.answerParts.indexOf('angle of normal force')]}
-                                                labelWidth={'7em'}
+                                                labelWidth="7em"
                                             />
                                         )}
                                         {this.selectedQuestion.answerParts.includes('force of static friction') && (
@@ -1092,12 +1103,12 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP
                                                 dataDoc={this.dataDoc}
                                                 prop="review_StaticMagnitude"
                                                 step={0.1}
-                                                unit={'N'}
+                                                unit="N"
                                                 upperBound={50}
                                                 value={NumCast(this.dataDoc.review_StaticMagnitude)}
                                                 showIcon={BoolCast(this.dataDoc.simulation_showIcon)}
                                                 correctValue={NumListCast(this.dataDoc.answers)[this.selectedQuestion.answerParts.indexOf('force of static friction')]}
-                                                labelWidth={'7em'}
+                                                labelWidth="7em"
                                             />
                                         )}
                                         {this.selectedQuestion.answerParts.includes('angle of static friction') && (
@@ -1107,13 +1118,13 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP
                                                 dataDoc={this.dataDoc}
                                                 prop="review_StaticAngle"
                                                 step={1}
-                                                unit={'°'}
+                                                unit="°"
                                                 upperBound={360}
                                                 value={NumCast(this.dataDoc.review_StaticAngle)}
-                                                radianEquivalent={true}
+                                                radianEquivalent
                                                 showIcon={BoolCast(this.dataDoc.simulation_showIcon)}
                                                 correctValue={NumListCast(this.dataDoc.answers)[this.selectedQuestion.answerParts.indexOf('angle of static friction')]}
-                                                labelWidth={'7em'}
+                                                labelWidth="7em"
                                             />
                                         )}
                                         {this.selectedQuestion.answerParts.includes('coefficient of static friction') && (
@@ -1127,7 +1138,7 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP
                                                 dataDoc={this.dataDoc}
                                                 prop="coefficientOfStaticFriction"
                                                 step={0.1}
-                                                unit={''}
+                                                unit=""
                                                 upperBound={1}
                                                 value={NumCast(this.dataDoc.coefficientOfStaticFriction)}
                                                 effect={this.updateReviewForcesBasedOnCoefficient}
@@ -1142,14 +1153,14 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP
                                                 dataDoc={this.dataDoc}
                                                 prop="wedge_angle"
                                                 step={1}
-                                                unit={'°'}
+                                                unit="°"
                                                 upperBound={49}
                                                 value={this.wedgeAngle}
                                                 effect={(val: number) => {
                                                     this.changeWedgeBasedOnNewAngle(val);
                                                     this.updateReviewForcesBasedOnAngle(val);
                                                 }}
-                                                radianEquivalent={true}
+                                                radianEquivalent
                                                 showIcon={BoolCast(this.dataDoc.simulation_showIcon)}
                                                 correctValue={NumListCast(this.dataDoc.answers)[this.selectedQuestion.answerParts.indexOf('wedge angle')]}
                                             />
@@ -1158,7 +1169,7 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP
                                 </div>
                             </div>
                         )}
-                        {this.simulationMode == 'Tutorial' && (
+                        {this.simulationMode === 'Tutorial' && (
                             <div className="wordProblemBox">
                                 <div className="question">
                                     <h2>Problem</h2>
@@ -1180,7 +1191,7 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP
                                             this.dataDoc.mass1_forcesUpdated = JSON.stringify(this.tutorial.steps[step].forces);
                                             this.dataDoc.simulation_showForceMagnitudes = this.tutorial.steps[step].showMagnitude;
                                         }}
-                                        disabled={this.dataDoc.tutorial_stepNumber == 0}>
+                                        disabled={this.dataDoc.tutorial_stepNumber === 0}>
                                         <ArrowLeftIcon />
                                     </IconButton>
                                     <div>
@@ -1204,8 +1215,8 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP
                                     </IconButton>
                                 </div>
                                 <div>
-                                    {(this.simulationType == 'One Weight' || this.simulationType == 'Inclined Plane' || this.simulationType == 'Pendulum') && <p>Resources</p>}
-                                    {this.simulationType == 'One Weight' && (
+                                    {(this.simulationType === 'One Weight' || this.simulationType === 'Inclined Plane' || this.simulationType === 'Pendulum') && <p>Resources</p>}
+                                    {this.simulationType === 'One Weight' && (
                                         <ul>
                                             <li>
                                                 <a
@@ -1233,7 +1244,7 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP
                                             </li>
                                         </ul>
                                     )}
-                                    {this.simulationType == 'Inclined Plane' && (
+                                    {this.simulationType === 'Inclined Plane' && (
                                         <ul>
                                             <li>
                                                 <a
@@ -1261,7 +1272,7 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP
                                             </li>
                                         </ul>
                                     )}
-                                    {this.simulationType == 'Pendulum' && (
+                                    {this.simulationType === 'Pendulum' && (
                                         <ul>
                                             <li>
                                                 <a
@@ -1280,7 +1291,7 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP
                                 </div>
                             </div>
                         )}
-                        {this.simulationMode == 'Review' && this.simulationType == 'Inclined Plane' && (
+                        {this.simulationMode === 'Review' && this.simulationType === 'Inclined Plane' && (
                             <div
                                 style={{
                                     display: 'flex',
@@ -1318,11 +1329,11 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP
                                 </div>
                             </div>
                         )}
-                        {this.simulationMode == 'Freeform' && (
+                        {this.simulationMode === 'Freeform' && (
                             <div className="vars">
                                 <FormControl component="fieldset">
                                     <FormGroup>
-                                        {this.simulationType == 'One Weight' && (
+                                        {this.simulationType === 'One Weight' && (
                                             <FormControlLabel
                                                 control={<Checkbox checked={BoolCast(this.dataDoc.elasticCollisions)} onChange={() => (this.dataDoc.elasticCollisions = !this.dataDoc.elasticCollisions)} />}
                                                 label="Make collisions elastic"
@@ -1334,7 +1345,7 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP
                                             label="Show force vectors"
                                             labelPlacement="start"
                                         />
-                                        {(this.simulationType == 'Inclined Plane' || this.simulationType == 'Pendulum') && (
+                                        {(this.simulationType === 'Inclined Plane' || this.simulationType === 'Pendulum') && (
                                             <FormControlLabel
                                                 control={<Checkbox checked={BoolCast(this.dataDoc.simulation_showComponentForces)} onChange={() => (this.dataDoc.simulation_showComponentForces = !this.dataDoc.simulation_showComponentForces)} />}
                                                 label="Show component force vectors"
@@ -1351,80 +1362,80 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP
                                             label="Show velocity vector"
                                             labelPlacement="start"
                                         />
-                                        <InputField label={<Box>Speed</Box>} lowerBound={1} dataDoc={this.dataDoc} prop="simulation_speed" step={1} unit={'x'} upperBound={10} value={NumCast(this.dataDoc.simulation_speed, 2)} labelWidth={'5em'} />
-                                        {this.dataDoc.simulation_paused && this.simulationType != 'Circular Motion' && (
+                                        <InputField label={<Box>Speed</Box>} lowerBound={1} dataDoc={this.dataDoc} prop="simulation_speed" step={1} unit="x" upperBound={10} value={NumCast(this.dataDoc.simulation_speed, 2)} labelWidth="5em" />
+                                        {this.dataDoc.simulation_paused && this.simulationType !== 'Circular Motion' && (
                                             <InputField
                                                 label={<Box>Gravity</Box>}
                                                 lowerBound={-30}
                                                 dataDoc={this.dataDoc}
                                                 prop="gravity"
                                                 step={0.01}
-                                                unit={'m/s2'}
+                                                unit="m/s2"
                                                 upperBound={0}
                                                 value={NumCast(this.dataDoc.simulation_gravity, -9.81)}
                                                 effect={(val: number) => this.setupSimulation()}
-                                                labelWidth={'5em'}
+                                                labelWidth="5em"
                                             />
                                         )}
-                                        {this.dataDoc.simulation_paused && this.simulationType != 'Pulley' && (
+                                        {this.dataDoc.simulation_paused && this.simulationType !== 'Pulley' && (
                                             <InputField
                                                 label={<Box>Mass</Box>}
                                                 lowerBound={1}
                                                 dataDoc={this.dataDoc}
                                                 prop="mass1"
                                                 step={0.1}
-                                                unit={'kg'}
+                                                unit="kg"
                                                 upperBound={5}
                                                 value={this.mass1 ?? 1}
                                                 effect={(val: number) => this.setupSimulation()}
-                                                labelWidth={'5em'}
+                                                labelWidth="5em"
                                             />
                                         )}
-                                        {this.dataDoc.simulation_paused && this.simulationType == 'Pulley' && (
+                                        {this.dataDoc.simulation_paused && this.simulationType === 'Pulley' && (
                                             <InputField
                                                 label={<Box>Red mass</Box>}
                                                 lowerBound={1}
                                                 dataDoc={this.dataDoc}
                                                 prop="mass1"
                                                 step={0.1}
-                                                unit={'kg'}
+                                                unit="kg"
                                                 upperBound={5}
                                                 value={this.mass1 ?? 1}
                                                 effect={(val: number) => this.setupSimulation()}
-                                                labelWidth={'5em'}
+                                                labelWidth="5em"
                                             />
                                         )}
-                                        {this.dataDoc.simulation_paused && this.simulationType == 'Pulley' && (
+                                        {this.dataDoc.simulation_paused && this.simulationType === 'Pulley' && (
                                             <InputField
                                                 label={<Box>Blue mass</Box>}
                                                 lowerBound={1}
                                                 dataDoc={this.dataDoc}
                                                 prop="mass2"
                                                 step={0.1}
-                                                unit={'kg'}
+                                                unit="kg"
                                                 upperBound={5}
                                                 value={this.mass2 ?? 1}
                                                 effect={(val: number) => this.setupSimulation()}
-                                                labelWidth={'5em'}
+                                                labelWidth="5em"
                                             />
                                         )}
-                                        {this.dataDoc.simulation_paused && this.simulationType == 'Circular Motion' && (
+                                        {this.dataDoc.simulation_paused && this.simulationType === 'Circular Motion' && (
                                             <InputField
                                                 label={<Box>Rod length</Box>}
                                                 lowerBound={100}
                                                 dataDoc={this.dataDoc}
                                                 prop="circularMotionRadius"
                                                 step={5}
-                                                unit={'m'}
+                                                unit="m"
                                                 upperBound={250}
                                                 value={this.circularMotionRadius}
                                                 effect={(val: number) => this.setupSimulation()}
-                                                labelWidth={'5em'}
+                                                labelWidth="5em"
                                             />
                                         )}
                                     </FormGroup>
                                 </FormControl>
-                                {this.simulationType == 'Spring' && this.dataDoc.simulation_paused && (
+                                {this.simulationType === 'Spring' && this.dataDoc.simulation_paused && (
                                     <div>
                                         <InputField
                                             label={<Typography color="inherit">Spring stiffness</Typography>}
@@ -1432,13 +1443,13 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP
                                             dataDoc={this.dataDoc}
                                             prop="spring_constant"
                                             step={1}
-                                            unit={'N/m'}
+                                            unit="N/m"
                                             upperBound={500}
                                             value={this.springConstant}
                                             effect={action(() => this._simReset++)}
                                             radianEquivalent={false}
-                                            mode={'Freeform'}
-                                            labelWidth={'7em'}
+                                            mode="Freeform"
+                                            labelWidth="7em"
                                         />
                                         <InputField
                                             label={<Typography color="inherit">Rest length</Typography>}
@@ -1452,7 +1463,7 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP
                                             effect={action(() => this._simReset++)}
                                             radianEquivalent={false}
                                             mode="Freeform"
-                                            labelWidth={'7em'}
+                                            labelWidth="7em"
                                         />
                                         <InputField
                                             label={<Typography color="inherit">Starting displacement</Typography>}
@@ -1470,11 +1481,11 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP
                                             })}
                                             radianEquivalent={false}
                                             mode="Freeform"
-                                            labelWidth={'7em'}
+                                            labelWidth="7em"
                                         />
                                     </div>
                                 )}
-                                {this.simulationType == 'Inclined Plane' && this.dataDoc.simulation_paused && (
+                                {this.simulationType === 'Inclined Plane' && this.dataDoc.simulation_paused && (
                                     <div>
                                         <InputField
                                             label={<Box>&theta;</Box>}
@@ -1482,16 +1493,16 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP
                                             dataDoc={this.dataDoc}
                                             prop="wedge_angle"
                                             step={1}
-                                            unit={'°'}
+                                            unit="°"
                                             upperBound={49}
                                             value={this.wedgeAngle}
                                             effect={action((val: number) => {
                                                 this.changeWedgeBasedOnNewAngle(val);
                                                 this._simReset++;
                                             })}
-                                            radianEquivalent={true}
-                                            mode={'Freeform'}
-                                            labelWidth={'2em'}
+                                            radianEquivalent
+                                            mode="Freeform"
+                                            labelWidth="2em"
                                         />
                                         <InputField
                                             label={
@@ -1503,7 +1514,7 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP
                                             dataDoc={this.dataDoc}
                                             prop="coefficientOfStaticFriction"
                                             step={0.1}
-                                            unit={''}
+                                            unit=""
                                             upperBound={1}
                                             value={NumCast(this.dataDoc.coefficientOfStaticFriction) ?? 0}
                                             effect={action((val: number) => {
@@ -1513,8 +1524,8 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP
                                                 }
                                                 this._simReset++;
                                             })}
-                                            mode={'Freeform'}
-                                            labelWidth={'2em'}
+                                            mode="Freeform"
+                                            labelWidth="2em"
                                         />
                                         <InputField
                                             label={
@@ -1526,16 +1537,16 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP
                                             dataDoc={this.dataDoc}
                                             prop="coefficientOfKineticFriction"
                                             step={0.1}
-                                            unit={''}
+                                            unit=""
                                             upperBound={NumCast(this.dataDoc.coefficientOfStaticFriction)}
                                             value={NumCast(this.dataDoc.coefficientOfKineticFriction) ?? 0}
                                             effect={action(() => this._simReset++)}
-                                            mode={'Freeform'}
-                                            labelWidth={'2em'}
+                                            mode="Freeform"
+                                            labelWidth="2em"
                                         />
                                     </div>
                                 )}
-                                {this.simulationType == 'Inclined Plane' && !this.dataDoc.simulation_paused && (
+                                {this.simulationType === 'Inclined Plane' && !this.dataDoc.simulation_paused && (
                                     <Typography>
                                         <>
                                             &theta;: {Math.round(this.wedgeAngle * 100) / 100}° ≈ {Math.round(((this.wedgeAngle * Math.PI) / 180) * 100) / 100} rad
@@ -1546,12 +1557,12 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP
                                         </>
                                     </Typography>
                                 )}
-                                {this.simulationType == 'Pendulum' && !this.dataDoc.simulation_paused && (
+                                {this.simulationType === 'Pendulum' && !this.dataDoc.simulation_paused && (
                                     <Typography>
                                         &theta;: {Math.round(this.pendulumAngle * 100) / 100}° ≈ {Math.round(((this.pendulumAngle * Math.PI) / 180) * 100) / 100} rad
                                     </Typography>
                                 )}
-                                {this.simulationType == 'Pendulum' && this.dataDoc.simulation_paused && (
+                                {this.simulationType === 'Pendulum' && this.dataDoc.simulation_paused && (
                                     <div>
                                         <InputField
                                             label={<Box>Angle</Box>}
@@ -1559,13 +1570,13 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP
                                             dataDoc={this.dataDoc}
                                             prop="pendulum_angle"
                                             step={1}
-                                            unit={'°'}
+                                            unit="°"
                                             upperBound={59}
                                             value={NumCast(this.dataDoc.pendulum_angle, 30)}
                                             effect={action(value => {
                                                 this.dataDoc.pendulum_angleStart = value;
                                                 this.dataDoc.pendulum_lengthStart = this.dataDoc.pendulum_length;
-                                                if (this.simulationType == 'Pendulum') {
+                                                if (this.simulationType === 'Pendulum') {
                                                     const mag = this.mass1 * Math.abs(this.gravity) * Math.cos((value * Math.PI) / 180);
 
                                                     const forceOfTension: IForce = {
@@ -1598,7 +1609,7 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP
                                                     this._simReset++;
                                                 }
                                             })}
-                                            radianEquivalent={true}
+                                            radianEquivalent
                                             mode="Freeform"
                                             labelWidth="5em"
                                         />
@@ -1612,7 +1623,7 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP
                                             upperBound={400}
                                             value={Math.round(this.pendulumLength)}
                                             effect={action(value => {
-                                                if (this.simulationType == 'Pendulum') {
+                                                if (this.simulationType === 'Pendulum') {
                                                     this.dataDoc.pendulum_angleStart = this.pendulumAngle;
                                                     this.dataDoc.pendulum_lengthStart = value;
                                                     this._simReset++;
@@ -1627,11 +1638,11 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP
                             </div>
                         )}
                         <div className="mechanicsSimulationEquation">
-                            {this.simulationMode == 'Freeform' && (
+                            {this.simulationMode === 'Freeform' && (
                                 <table>
                                     <tbody>
                                         <tr>
-                                            <td>{this.simulationType == 'Pulley' ? 'Red Weight' : ''}</td>
+                                            <td>{this.simulationType === 'Pulley' ? 'Red Weight' : ''}</td>
                                             <td>X</td>
                                             <td>Y</td>
                                         </tr>
@@ -1646,36 +1657,34 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP
                                             >
                                                 <Box>Position</Box>
                                             </td>
-                                            {(!this.dataDoc.simulation_paused || this.simulationType == 'Inclined Plane' || this.simulationType == 'Circular Motion' || this.simulationType == 'Pulley') && (
-                                                <td style={{ cursor: 'default' }}>
-                                                    <>{this.dataDoc.mass1_positionX} m</>
-                                                </td>
+                                            {(!this.dataDoc.simulation_paused || this.simulationType === 'Inclined Plane' || this.simulationType === 'Circular Motion' || this.simulationType === 'Pulley') && (
+                                                <td style={{ cursor: 'default' }}>{this.dataDoc.mass1_positionX + ''} m</td>
                                             )}{' '}
-                                            {this.dataDoc.simulation_paused && this.simulationType != 'Inclined Plane' && this.simulationType != 'Circular Motion' && this.simulationType != 'Pulley' && (
+                                            {this.dataDoc.simulation_paused && this.simulationType !== 'Inclined Plane' && this.simulationType !== 'Circular Motion' && this.simulationType !== 'Pulley' && (
                                                 <td
                                                     style={{
                                                         cursor: 'default',
                                                     }}>
                                                     <InputField
-                                                        lowerBound={this.simulationType == 'Projectile' ? 1 : (this.xMax + this.xMin) / 4 - this.radius - 15}
+                                                        lowerBound={this.simulationType === 'Projectile' ? 1 : (this.xMax + this.xMin) / 4 - this.radius - 15}
                                                         dataDoc={this.dataDoc}
                                                         prop="mass1_positionX"
                                                         step={1}
-                                                        unit={'m'}
-                                                        upperBound={this.simulationType == 'Projectile' ? this.xMax - 110 : (3 * (this.xMax + this.xMin)) / 4 - this.radius / 2 - 15}
+                                                        unit="m"
+                                                        upperBound={this.simulationType === 'Projectile' ? this.xMax - 110 : (3 * (this.xMax + this.xMin)) / 4 - this.radius / 2 - 15}
                                                         value={NumCast(this.dataDoc.mass1_positionX)}
                                                         effect={value => {
                                                             this.dataDoc.mass1_xChange = value;
-                                                            if (this.simulationType == 'Suspension') {
-                                                                let x1rod = (this.xMax + this.xMin) / 2 - this.radius - this.yMin - 200;
-                                                                let x2rod = (this.xMax + this.xMin) / 2 + this.yMin + 200 + this.radius;
-                                                                let deltaX1 = value + this.radius - x1rod;
-                                                                let deltaX2 = x2rod - (value + this.radius);
-                                                                let deltaY = this.getYPosFromDisplay(NumCast(this.dataDoc.mass1_positionY)) + this.radius;
+                                                            if (this.simulationType === 'Suspension') {
+                                                                const x1rod = (this.xMax + this.xMin) / 2 - this.radius - this.yMin - 200;
+                                                                const x2rod = (this.xMax + this.xMin) / 2 + this.yMin + 200 + this.radius;
+                                                                const deltaX1 = value + this.radius - x1rod;
+                                                                const deltaX2 = x2rod - (value + this.radius);
+                                                                const deltaY = this.getYPosFromDisplay(NumCast(this.dataDoc.mass1_positionY)) + this.radius;
                                                                 let dir1T = Math.PI - Math.atan(deltaY / deltaX1);
                                                                 let dir2T = Math.atan(deltaY / deltaX2);
-                                                                let tensionMag2 = (this.mass1 * Math.abs(this.gravity)) / ((-Math.cos(dir2T) / Math.cos(dir1T)) * Math.sin(dir1T) + Math.sin(dir2T));
-                                                                let tensionMag1 = (-tensionMag2 * Math.cos(dir2T)) / Math.cos(dir1T);
+                                                                const tensionMag2 = (this.mass1 * Math.abs(this.gravity)) / ((-Math.cos(dir2T) / Math.cos(dir1T)) * Math.sin(dir1T) + Math.sin(dir2T));
+                                                                const tensionMag1 = (-tensionMag2 * Math.cos(dir2T)) / Math.cos(dir1T);
                                                                 dir1T = (dir1T * 180) / Math.PI;
                                                                 dir2T = (dir2T * 180) / Math.PI;
                                                                 const tensionForce1: IForce = {
@@ -1692,15 +1701,15 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP
                                                                 this.dataDoc.mass1_forcesUpdated = JSON.stringify([tensionForce1, tensionForce2, gravity]);
                                                             }
                                                         }}
-                                                        small={true}
+                                                        small
                                                         mode="Freeform"
                                                     />
                                                 </td>
                                             )}{' '}
-                                            {(!this.dataDoc.simulation_paused || this.simulationType == 'Inclined Plane' || this.simulationType == 'Circular Motion' || this.simulationType == 'Pulley') && (
+                                            {(!this.dataDoc.simulation_paused || this.simulationType === 'Inclined Plane' || this.simulationType === 'Circular Motion' || this.simulationType === 'Pulley') && (
                                                 <td style={{ cursor: 'default' }}>{`${NumCast(this.dataDoc.mass1_positionY)} m`}</td>
                                             )}{' '}
-                                            {this.dataDoc.simulation_paused && this.simulationType != 'Inclined Plane' && this.simulationType != 'Circular Motion' && this.simulationType != 'Pulley' && (
+                                            {this.dataDoc.simulation_paused && this.simulationType !== 'Inclined Plane' && this.simulationType !== 'Circular Motion' && this.simulationType !== 'Pulley' && (
                                                 <td
                                                     style={{
                                                         cursor: 'default',
@@ -1715,16 +1724,16 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP
                                                         value={NumCast(this.dataDoc.mass1_positionY)}
                                                         effect={value => {
                                                             this.dataDoc.mass1_yChange = value;
-                                                            if (this.simulationType == 'Suspension') {
-                                                                let x1rod = (this.xMax + this.xMin) / 2 - this.radius - this.yMin - 200;
-                                                                let x2rod = (this.xMax + this.xMin) / 2 + this.yMin + 200 + this.radius;
-                                                                let deltaX1 = NumCast(this.dataDoc.mass1_positionX) + this.radius - x1rod;
-                                                                let deltaX2 = x2rod - (NumCast(this.dataDoc.mass1_positionX) + this.radius);
-                                                                let deltaY = this.getYPosFromDisplay(value) + this.radius;
+                                                            if (this.simulationType === 'Suspension') {
+                                                                const x1rod = (this.xMax + this.xMin) / 2 - this.radius - this.yMin - 200;
+                                                                const x2rod = (this.xMax + this.xMin) / 2 + this.yMin + 200 + this.radius;
+                                                                const deltaX1 = NumCast(this.dataDoc.mass1_positionX) + this.radius - x1rod;
+                                                                const deltaX2 = x2rod - (NumCast(this.dataDoc.mass1_positionX) + this.radius);
+                                                                const deltaY = this.getYPosFromDisplay(value) + this.radius;
                                                                 let dir1T = Math.PI - Math.atan(deltaY / deltaX1);
                                                                 let dir2T = Math.atan(deltaY / deltaX2);
-                                                                let tensionMag2 = (this.mass1 * Math.abs(this.gravity)) / ((-Math.cos(dir2T) / Math.cos(dir1T)) * Math.sin(dir1T) + Math.sin(dir2T));
-                                                                let tensionMag1 = (-tensionMag2 * Math.cos(dir2T)) / Math.cos(dir1T);
+                                                                const tensionMag2 = (this.mass1 * Math.abs(this.gravity)) / ((-Math.cos(dir2T) / Math.cos(dir1T)) * Math.sin(dir1T) + Math.sin(dir2T));
+                                                                const tensionMag1 = (-tensionMag2 * Math.cos(dir2T)) / Math.cos(dir1T);
                                                                 dir1T = (dir1T * 180) / Math.PI;
                                                                 dir2T = (dir2T * 180) / Math.PI;
                                                                 const tensionForce1: IForce = {
@@ -1741,7 +1750,7 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP
                                                                 this.dataDoc.mass1_forcesUpdated = JSON.stringify([tensionForce1, tensionForce2, gravity]);
                                                             }
                                                         }}
-                                                        small={true}
+                                                        small
                                                         mode="Freeform"
                                                     />
                                                 </td>
@@ -1758,10 +1767,10 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP
                                             >
                                                 <Box>Velocity</Box>
                                             </td>
-                                            {(!this.dataDoc.simulation_paused || (this.simulationType != 'One Weight' && this.simulationType != 'Circular Motion')) && (
+                                            {(!this.dataDoc.simulation_paused || (this.simulationType !== 'One Weight' && this.simulationType !== 'Circular Motion')) && (
                                                 <td style={{ cursor: 'default' }}>{`${NumCast(this.dataDoc.mass1_velocityX)} m/s`}</td>
                                             )}{' '}
-                                            {this.dataDoc.simulation_paused && (this.simulationType == 'One Weight' || this.simulationType == 'Circular Motion') && (
+                                            {this.dataDoc.simulation_paused && (this.simulationType === 'One Weight' || this.simulationType === 'Circular Motion') && (
                                                 <td
                                                     style={{
                                                         cursor: 'default',
@@ -1771,24 +1780,20 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP
                                                         dataDoc={this.dataDoc}
                                                         prop="mass1_velocityX"
                                                         step={1}
-                                                        unit={'m/s'}
+                                                        unit="m/s"
                                                         upperBound={50}
                                                         value={NumCast(this.dataDoc.mass1_velocityX)}
                                                         effect={action(value => {
                                                             this.dataDoc.mass1_velocityXstart = value;
                                                             this._simReset++;
                                                         })}
-                                                        small={true}
+                                                        small
                                                         mode="Freeform"
                                                     />
                                                 </td>
                                             )}{' '}
-                                            {(!this.dataDoc.simulation_paused || this.simulationType != 'One Weight') && (
-                                                <td style={{ cursor: 'default' }}>
-                                                    <>{this.dataDoc.mass1_velocityY} m/s</>
-                                                </td>
-                                            )}{' '}
-                                            {this.dataDoc.simulation_paused && this.simulationType == 'One Weight' && (
+                                            {(!this.dataDoc.simulation_paused || this.simulationType !== 'One Weight') && <td style={{ cursor: 'default' }}>{this.dataDoc.mass1_velocityY + ''} m/s</td>}{' '}
+                                            {this.dataDoc.simulation_paused && this.simulationType === 'One Weight' && (
                                                 <td
                                                     style={{
                                                         cursor: 'default',
@@ -1804,7 +1809,7 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP
                                                         effect={value => {
                                                             this.dataDoc.mass1_velocityYstart = -value;
                                                         }}
-                                                        small={true}
+                                                        small
                                                         mode="Freeform"
                                                     />
                                                 </td>
@@ -1822,14 +1827,10 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP
                                                 <Box>Acceleration</Box>
                                             </td>
                                             <td style={{ cursor: 'default' }}>
-                                                <>
-                                                    {this.dataDoc.mass1_accelerationX} m/s<sup>2</sup>
-                                                </>
+                                                {this.dataDoc.mass1_accelerationX + ''} m/s<sup>2</sup>
                                             </td>
                                             <td style={{ cursor: 'default' }}>
-                                                <>
-                                                    {this.dataDoc.mass1_accelerationY} m/s<sup>2</sup>
-                                                </>
+                                                {this.dataDoc.mass1_accelerationY + ''} m/s<sup>2</sup>
                                             </td>
                                         </tr>
                                         <tr>
@@ -1842,7 +1843,7 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP
                                     </tbody>
                                 </table>
                             )}
-                            {this.simulationMode == 'Freeform' && this.simulationType == 'Pulley' && (
+                            {this.simulationMode === 'Freeform' && this.simulationType === 'Pulley' && (
                                 <table>
                                     <tbody>
                                         <tr>
@@ -1869,14 +1870,10 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP
                                                 <Box>Acceleration</Box>
                                             </td>
                                             <td style={{ cursor: 'default' }}>
-                                                <>
-                                                    {this.dataDoc.mass2_accelerationX} m/s<sup>2</sup>
-                                                </>
+                                                {this.dataDoc.mass2_accelerationX + ''} m/s<sup>2</sup>
                                             </td>
                                             <td style={{ cursor: 'default' }}>
-                                                <>
-                                                    {this.dataDoc.mass2_accelerationY} m/s<sup>2</sup>
-                                                </>
+                                                {this.dataDoc.mass2_accelerationY + ''} m/s<sup>2</sup>
                                             </td>
                                         </tr>
                                         <tr>
@@ -1890,7 +1887,7 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP
                                 </table>
                             )}
                         </div>
-                        {this.simulationType != 'Pendulum' && this.simulationType != 'Spring' && (
+                        {this.simulationType !== 'Pendulum' && this.simulationType !== 'Spring' && (
                             <div>
                                 <p>Kinematic Equations</p>
                                 <ul>
@@ -1907,7 +1904,7 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP
                                 </ul>
                             </div>
                         )}
-                        {this.simulationType == 'Spring' && (
+                        {this.simulationType === 'Spring' && (
                             <div>
                                 <p>Harmonic Motion Equations: Spring</p>
                                 <ul>
@@ -1936,7 +1933,7 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP
                                 </ul>
                             </div>
                         )}
-                        {this.simulationType == 'Pendulum' && (
+                        {this.simulationType === 'Pendulum' && (
                             <div>
                                 <p>Harmonic Motion Equations: Pendulum</p>
                                 <ul>
@@ -1959,11 +1956,11 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP
                     <svg width={100 + 'px'} height={100 + 'px'}>
                         <defs>
                             <marker id="miniArrow" markerWidth="20" markerHeight="20" refX="0" refY="3" orient="auto" markerUnits="strokeWidth">
-                                <path d="M0,0 L0,6 L9,3 z" fill={'#000000'} />
+                                <path d="M0,0 L0,6 L9,3 z" fill="#000000" />
                             </marker>
                         </defs>
-                        <line x1={20} y1={70} x2={70} y2={70} stroke={'#000000'} strokeWidth="2" markerEnd="url(#miniArrow)" />
-                        <line x1={20} y1={70} x2={20} y2={20} stroke={'#000000'} strokeWidth="2" markerEnd="url(#miniArrow)" />
+                        <line x1={20} y1={70} x2={70} y2={70} stroke="#000000" strokeWidth="2" markerEnd="url(#miniArrow)" />
+                        <line x1={20} y1={70} x2={20} y2={20} stroke="#000000" strokeWidth="2" markerEnd="url(#miniArrow)" />
                     </svg>
                     <p
                         style={{
@@ -1971,7 +1968,7 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP
                             top: this.yMax - 120 + 40 + 'px',
                             left: this.xMin + 90 - 80 + 'px',
                         }}>
-                        {this.simulationType == 'Circular Motion' ? 'Z' : 'Y'}
+                        {this.simulationType === 'Circular Motion' ? 'Z' : 'Y'}
                     </p>
                     <p
                         style={{
@@ -1986,3 +1983,9 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP
         );
     }
 }
+
+Docs.Prototypes.TemplateMap.set(DocumentType.SIMULATION, {
+    data: '',
+    layout: { view: PhysicsSimulationBox, dataField: 'data' },
+    options: { acl: '', _width: 1000, _height: 800, mass1: '', mass2: '', layout_nativeDimEditable: true, position: '', acceleration: '', pendulum: '', spring: '', wedge: '', simulation: '', review: '', systemIcon: 'BsShareFill' },
+});
diff --git a/src/client/views/nodes/RecordingBox/RecordingBox.tsx b/src/client/views/nodes/RecordingBox/RecordingBox.tsx
index 2bc749e1b..ae6caf9f4 100644
--- a/src/client/views/nodes/RecordingBox/RecordingBox.tsx
+++ b/src/client/views/nodes/RecordingBox/RecordingBox.tsx
@@ -257,3 +257,8 @@ ScriptingGlobals.add(function renderDropdown() {
     }
     return false;
 });
+
+Docs.Prototypes.TemplateMap.set(DocumentType.WEBCAM, {
+    layout: { view: RecordingBox, dataField: 'data' },
+    options: { acl: '', systemIcon: 'BsFillCameraVideoFill' },
+});
diff --git a/src/client/views/nodes/ScreenshotBox.tsx b/src/client/views/nodes/ScreenshotBox.tsx
index 882f6ba9e..8eb55b2f8 100644
--- a/src/client/views/nodes/ScreenshotBox.tsx
+++ b/src/client/views/nodes/ScreenshotBox.tsx
@@ -16,8 +16,9 @@ import { Cast, DocCast, NumCast } from '../../../fields/Types';
 import { AudioField, VideoField } from '../../../fields/URLField';
 import { TraceMobx } from '../../../fields/util';
 import { Networking } from '../../Network';
+import { DocUtils } from '../../documents/DocUtils';
 import { DocumentType } from '../../documents/DocumentTypes';
-import { DocUtils } from '../../documents/Documents';
+import { Docs } from '../../documents/Documents';
 import { CaptureManager } from '../../util/CaptureManager';
 import { SettingsManager } from '../../util/SettingsManager';
 import { TrackMovements } from '../../util/TrackMovements';
@@ -27,9 +28,9 @@ import { CollectionStackedTimeline } from '../collections/CollectionStackedTimel
 import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView';
 import { mediaState } from './AudioBox';
 import { FieldView, FieldViewProps } from './FieldView';
-import { FormattedTextBox } from './formattedText/FormattedTextBox';
 import './ScreenshotBox.scss';
 import { VideoBox } from './VideoBox';
+import { FormattedTextBox } from './formattedText/FormattedTextBox';
 
 declare class MediaRecorder {
     constructor(e: any, options?: any); // whatever MediaRecorder has
@@ -357,3 +358,8 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
         );
     }
 }
+
+Docs.Prototypes.TemplateMap.set(DocumentType.SCREENSHOT, {
+    layout: { view: ScreenshotBox, dataField: 'data' },
+    options: { acl: '', _layout_nativeDimEditable: true, systemIcon: 'BsCameraFill' },
+});
diff --git a/src/client/views/nodes/ScriptingBox.tsx b/src/client/views/nodes/ScriptingBox.tsx
index 5ebc50a1b..bc19d7ad1 100644
--- a/src/client/views/nodes/ScriptingBox.tsx
+++ b/src/client/views/nodes/ScriptingBox.tsx
@@ -21,6 +21,8 @@ import { OverlayView } from '../OverlayView';
 import { FieldView, FieldViewProps } from './FieldView';
 import { DocumentIconContainer } from './DocumentIcon';
 import './ScriptingBox.scss';
+import { Docs } from '../../documents/Documents';
+import { DocumentType } from '../../documents/DocumentTypes';
 
 const _global = (window /* browser */ || global) /* node */ as any;
 const ReactTextareaAutocomplete = require('@webscopeio/react-textarea-autocomplete').default;
@@ -845,3 +847,8 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
         );
     }
 }
+
+Docs.Prototypes.TemplateMap.set(DocumentType.SCRIPTING, {
+    layout: { view: ScriptingBox, dataField: 'data' },
+    options: { acl: '', systemIcon: 'BsFileEarmarkCodeFill' },
+});
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index 13ee3250e..16767d11e 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -10,14 +10,15 @@ import { DocData } from '../../../fields/DocSymbols';
 import { InkTool } from '../../../fields/InkField';
 import { List } from '../../../fields/List';
 import { ObjectField } from '../../../fields/ObjectField';
+import { FollowLinkScript } from '../../../fields/ScriptField';
 import { Cast, NumCast, StrCast, toList } from '../../../fields/Types';
 import { AudioField, ImageField, VideoField } from '../../../fields/URLField';
 import { emptyFunction, formatTime } from '../../../Utils';
-import { Docs, DocUtils } from '../../documents/Documents';
+import { Docs } from '../../documents/Documents';
 import { DocumentType } from '../../documents/DocumentTypes';
+import { DocUtils } from '../../documents/DocUtils';
 import { DocumentManager } from '../../util/DocumentManager';
 import { dropActionType } from '../../util/DropActionTypes';
-import { FollowLinkScript } from '../../util/LinkFollower';
 import { LinkManager } from '../../util/LinkManager';
 import { ReplayMovements } from '../../util/ReplayMovements';
 import { undoBatch } from '../../util/UndoManager';
@@ -31,7 +32,8 @@ import { AnchorMenu } from '../pdf/AnchorMenu';
 import { PinDocView, PinProps } from '../PinFuncs';
 import { StyleProp } from '../StyleProvider';
 import { DocumentView } from './DocumentView';
-import { FieldView, FieldViewProps, FocusViewOptions } from './FieldView';
+import { FieldView, FieldViewProps } from './FieldView';
+import { FocusViewOptions } from './FocusViewOptions';
 import { RecordingBox } from './RecordingBox';
 import './VideoBox.scss';
 
@@ -1162,3 +1164,12 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl
         );
     }
 }
+
+Docs.Prototypes.TemplateMap.set(DocumentType.VID, {
+    layout: { view: VideoBox, dataField: 'data' },
+    options: { acl: '', _layout_currentTimecode: 0, systemIcon: 'BsFileEarmarkPlayFill' },
+});
+Docs.Prototypes.TemplateMap.set(DocumentType.REC, {
+    layout: { view: VideoBox, dataField: 'data' },
+    options: { acl: '', _height: 100, backgroundColor: 'pink', systemIcon: 'BsFillMicFill' },
+});
diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx
index dfe237a86..54f246b20 100644
--- a/src/client/views/nodes/WebBox.tsx
+++ b/src/client/views/nodes/WebBox.tsx
@@ -18,7 +18,9 @@ import { Cast, NumCast, StrCast, toList, WebCast } from '../../../fields/Types';
 import { ImageField, WebField } from '../../../fields/URLField';
 import { TraceMobx } from '../../../fields/util';
 import { emptyFunction, stringHash } from '../../../Utils';
-import { Docs, DocUtils } from '../../documents/Documents';
+import { Docs } from '../../documents/Documents';
+import { DocumentType } from '../../documents/DocumentTypes';
+import { DocUtils } from '../../documents/DocUtils';
 import { DocumentManager } from '../../util/DocumentManager';
 import { ScriptingGlobals } from '../../util/ScriptingGlobals';
 import { SnappingManager } from '../../util/SnappingManager';
@@ -37,9 +39,11 @@ import { GPTPopup } from '../pdf/GPTPopup/GPTPopup';
 import { PinDocView, PinProps } from '../PinFuncs';
 import { SidebarAnnos } from '../SidebarAnnos';
 import { StyleProp } from '../StyleProvider';
-import { DocumentView, OpenWhere } from './DocumentView';
-import { FieldView, FieldViewProps, FocusViewOptions } from './FieldView';
+import { DocumentView } from './DocumentView';
+import { FieldView, FieldViewProps } from './FieldView';
+import { FocusViewOptions } from './FocusViewOptions';
 import { LinkInfo } from './LinkDocPreview';
+import { OpenWhere } from './OpenWhere';
 import './WebBox.scss';
 
 const { CreateImage } = require('./WebBoxRenderer');
@@ -1242,3 +1246,8 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
 ScriptingGlobals.add(function urlHash(url: string) {
     return stringHash(url);
 });
+
+Docs.Prototypes.TemplateMap.set(DocumentType.WEB, {
+    layout: { view: WebBox, dataField: 'data' },
+    options: { acl: '', _height: 300, _layout_fitWidth: true, _layout_nativeDimEditable: true, _layout_reflowVertical: true, waitForDoubleClickToClick: 'always', systemIcon: 'BsGlobe' },
+});
diff --git a/src/client/views/nodes/calendarBox/CalendarBox.tsx b/src/client/views/nodes/calendarBox/CalendarBox.tsx
index 8b9e3cc5d..bd66941c3 100644
--- a/src/client/views/nodes/calendarBox/CalendarBox.tsx
+++ b/src/client/views/nodes/calendarBox/CalendarBox.tsx
@@ -7,6 +7,8 @@ import * as React from 'react';
 import { dateRangeStrToDates } from '../../../../ClientUtils';
 import { Doc } from '../../../../fields/Doc';
 import { StrCast } from '../../../../fields/Types';
+import { DocumentType } from '../../../documents/DocumentTypes';
+import { Docs } from '../../../documents/Documents';
 import { ViewBoxBaseComponent } from '../../DocComponent';
 import { FieldView, FieldViewProps } from '../FieldView';
 
@@ -119,3 +121,7 @@ export class CalendarBox extends ViewBoxBaseComponent<FieldViewProps>() {
         );
     }
 }
+Docs.Prototypes.TemplateMap.set(DocumentType.CALENDAR, {
+    layout: { view: CalendarBox, dataField: 'data' },
+    options: { acl: '' },
+});
diff --git a/src/client/views/nodes/formattedText/DashDocView.tsx b/src/client/views/nodes/formattedText/DashDocView.tsx
index f311b3cdd..93371685d 100644
--- a/src/client/views/nodes/formattedText/DashDocView.tsx
+++ b/src/client/views/nodes/formattedText/DashDocView.tsx
@@ -9,11 +9,12 @@ import { Doc } from '../../../../fields/Doc';
 import { Height, Width } from '../../../../fields/DocSymbols';
 import { NumCast } from '../../../../fields/Types';
 import { DocServer } from '../../../DocServer';
-import { Docs, DocUtils } from '../../../documents/Documents';
+import { Docs } from '../../../documents/Documents';
+import { DocUtils } from '../../../documents/DocUtils';
 import { Transform } from '../../../util/Transform';
 import { ObservableReactComponent } from '../../ObservableReactComponent';
 import { DocumentView } from '../DocumentView';
-import { FocusViewOptions } from '../FieldView';
+import { FocusViewOptions } from '../FocusViewOptions';
 import { FormattedTextBox } from './FormattedTextBox';
 
 const horizPadding = 3; // horizontal padding to container to allow cursor to show up on either side.
diff --git a/src/client/views/nodes/formattedText/DashFieldView.tsx b/src/client/views/nodes/formattedText/DashFieldView.tsx
index dc388b22a..1c5ea2dd4 100644
--- a/src/client/views/nodes/formattedText/DashFieldView.tsx
+++ b/src/client/views/nodes/formattedText/DashFieldView.tsx
@@ -23,7 +23,7 @@ import { AntimodeMenu, AntimodeMenuProps } from '../../AntimodeMenu';
 import { SchemaTableCell } from '../../collections/collectionSchema/SchemaTableCell';
 import { FilterPanel } from '../../FilterPanel';
 import { ObservableReactComponent } from '../../ObservableReactComponent';
-import { OpenWhere } from '../DocumentView';
+import { OpenWhere } from '../OpenWhere';
 import './DashFieldView.scss';
 import { FormattedTextBox } from './FormattedTextBox';
 
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 9bcf5027f..ad6629fc9 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -15,7 +15,7 @@ import * as React from 'react';
 import { BsMarkdownFill } from 'react-icons/bs';
 import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, ClientUtils, DivWidth, returnFalse, returnZero, setupMoveUpEvents, smoothScroll, StopEvent } from '../../../../ClientUtils';
 import { DateField } from '../../../../fields/DateField';
-import { Doc, DocListCast, Field, FieldType, Opt, StrListCast } from '../../../../fields/Doc';
+import { CreateLinkToActiveAudio, Doc, DocListCast, Field, FieldType, Opt, StrListCast } from '../../../../fields/Doc';
 import { AclAdmin, AclAugment, AclEdit, AclSelfEdit, DocCss, DocData, ForceServerWrite, UpdatingFromServer } from '../../../../fields/DocSymbols';
 import { Id } from '../../../../fields/FieldSymbols';
 import { InkTool } from '../../../../fields/InkField';
@@ -28,8 +28,9 @@ import { GetEffectiveAcl, TraceMobx } from '../../../../fields/util';
 import { emptyFunction, numberRange, unimplementedFunction, Utils } from '../../../../Utils';
 import { gptAPICall, GPTCallType } from '../../../apis/gpt/GPT';
 import { DocServer } from '../../../DocServer';
-import { Docs, DocUtils } from '../../../documents/Documents';
-import { CollectionViewType } from '../../../documents/DocumentTypes';
+import { Docs } from '../../../documents/Documents';
+import { CollectionViewType, DocumentType } from '../../../documents/DocumentTypes';
+import { DocUtils } from '../../../documents/DocUtils';
 import { DictationManager } from '../../../util/DictationManager';
 import { DocumentManager } from '../../../util/DocumentManager';
 import { DragManager } from '../../../util/DragManager';
@@ -54,9 +55,11 @@ import { PinDocView, PinProps } from '../../PinFuncs';
 import { SidebarAnnos } from '../../SidebarAnnos';
 import { styleFromLayoutString, StyleProp } from '../../StyleProvider';
 import { mediaState } from '../AudioBox';
-import { DocumentView, DocumentViewInternal, OpenWhere } from '../DocumentView';
-import { FieldView, FieldViewProps, FocusViewOptions } from '../FieldView';
+import { DocumentView, DocumentViewInternal } from '../DocumentView';
+import { FieldView, FieldViewProps } from '../FieldView';
+import { FocusViewOptions } from '../FocusViewOptions';
 import { LinkInfo } from '../LinkDocPreview';
+import { OpenWhere } from '../OpenWhere';
 import { DashDocCommentView } from './DashDocCommentView';
 import { DashDocView } from './DashDocView';
 import { DashFieldView } from './DashFieldView';
@@ -182,26 +185,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
     private gptRes: string = '';
 
     public static PasteOnLoad: ClipboardEvent | undefined;
-    private static SelectOnLoad: Doc | undefined;
-    public static SetSelectOnLoad(doc: Doc) {
-        FormattedTextBox.SelectOnLoad = doc;
-    }
     public static DontSelectInitialText = false; // whether initial text should be selected or not
     public static SelectOnLoadChar = '';
-    public static IsFragment(html: string) {
-        return html.indexOf('data-pm-slice') !== -1;
-    }
-    public static GetHref(html: string): string {
-        const parser = new DOMParser();
-        const parsedHtml = parser.parseFromString(html, 'text/html');
-        if (parsedHtml.body.childNodes.length === 1 && parsedHtml.body.childNodes[0].childNodes.length === 1 && (parsedHtml.body.childNodes[0].childNodes[0] as any).href) {
-            return (parsedHtml.body.childNodes[0].childNodes[0] as any).href;
-        }
-        return '';
-    }
-    public static GetDocFromUrl(url: string) {
-        return url.startsWith(document.location.origin) ? new URL(url).pathname.split('doc/').lastElement() : ''; // docId
-    }
 
     constructor(props: FormattedTextBoxProps) {
         super(props);
@@ -305,7 +290,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
             e.stopPropagation();
             const targetCreator = (annotationOn?: Doc) => {
                 const target = DocUtils.GetNewTextDoc('Note linked to ' + this.Document.title, 0, 0, 100, 100, annotationOn);
-                FormattedTextBox.SetSelectOnLoad(target);
+                Doc.SetSelectOnLoad(target);
                 return target;
             };
 
@@ -992,7 +977,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
         !options && cm.addItem({ description: 'Options...', subitems: optionItems, icon: 'eye' });
         const help = cm.findByDescription('Help...');
         const helpItems = help && 'subitems' in help ? help.subitems : [];
-        helpItems.push({ description: `show markdown options`, event: RTFMarkup.Instance.open, icon: <BsMarkdownFill /> });
+        helpItems.push({ description: `show markdown options`, event: () => RTFMarkup.Instance.setOpen(true), icon: <BsMarkdownFill /> });
         !help && cm.addItem({ description: 'Help...', subitems: helpItems, icon: 'eye' });
     };
 
@@ -1062,7 +1047,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
                     const tanch = Docs.Create.ConfigDocument({ title: 'dictation anchor' });
                     return this.addDocument(tanch) ? tanch : undefined;
                 };
-                const link = DocUtils.MakeLinkToActiveAudio(textanchorFunc, false).lastElement();
+                const link = CreateLinkToActiveAudio(textanchorFunc, false).lastElement();
                 if (link) {
                     link[DocData].isDictation = true;
                     const audioanchor = Cast(link.link_anchor_2, Doc, null);
@@ -1480,10 +1465,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
             (this._editorView as any).TextView = this;
         }
 
-        const selectOnLoad = Doc.AreProtosEqual(this._props.TemplateDataDocument ?? this.Document, FormattedTextBox.SelectOnLoad) && (!LightboxView.LightboxDoc || LightboxView.Contains(this.DocumentView?.()));
+        const selectOnLoad = Doc.AreProtosEqual(this._props.TemplateDataDocument ?? this.Document, Doc.SelectOnLoad) && (!LightboxView.LightboxDoc || LightboxView.Contains(this.DocumentView?.()));
         const selLoadChar = FormattedTextBox.SelectOnLoadChar;
         if (selectOnLoad) {
-            FormattedTextBox.SelectOnLoad = undefined;
+            Doc.SetSelectOnLoad(undefined);
             FormattedTextBox.SelectOnLoadChar = '';
         }
         if (this._editorView && selectOnLoad && !this._props.dontRegisterView && !this._props.dontSelectOnLoad && this.isActiveTab(this.ProseRef)) {
@@ -2137,3 +2122,18 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
         );
     }
 }
+
+Docs.Prototypes.TemplateMap.set(DocumentType.RTF, {
+    layout: { view: FormattedTextBox, dataField: 'text' },
+    options: {
+        acl: '',
+        _height: 35,
+        _xMargin: 10,
+        _yMargin: 10,
+        _layout_nativeDimEditable: true,
+        _layout_reflowVertical: true,
+        _layout_reflowHorizontal: true,
+        defaultDoubleClick: 'ignore',
+        systemIcon: 'BsFileEarmarkTextFill',
+    },
+});
diff --git a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts
index 5d448d40e..bc6454146 100644
--- a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts
+++ b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts
@@ -12,7 +12,7 @@ import { GetEffectiveAcl } from '../../../../fields/util';
 import { Docs } from '../../../documents/Documents';
 import { RTFMarkup } from '../../../util/RTFMarkup';
 import { SelectionManager } from '../../../util/SelectionManager';
-import { OpenWhere } from '../DocumentView';
+import { OpenWhere } from '../OpenWhere';
 
 const mac = typeof navigator !== 'undefined' ? /Mac/.test(navigator.platform) : false;
 
@@ -182,7 +182,7 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any): KeyMa
         return true;
     });
     bind('Cmd-?', () => {
-        RTFMarkup.Instance.open();
+        RTFMarkup.Instance.setOpen(true);
         return true;
     });
     bind('Cmd-e', (state: EditorState, dispatch: (tx: Transaction) => void) => {
diff --git a/src/client/views/nodes/formattedText/RichTextRules.ts b/src/client/views/nodes/formattedText/RichTextRules.ts
index 72a2b706d..1ff862859 100644
--- a/src/client/views/nodes/formattedText/RichTextRules.ts
+++ b/src/client/views/nodes/formattedText/RichTextRules.ts
@@ -8,7 +8,8 @@ import { List } from '../../../../fields/List';
 import { NumCast, StrCast } from '../../../../fields/Types';
 import { Utils } from '../../../../Utils';
 import { DocServer } from '../../../DocServer';
-import { Docs, DocUtils } from '../../../documents/Documents';
+import { Docs } from '../../../documents/Documents';
+import { DocUtils } from '../../../documents/DocUtils';
 import { CollectionViewType } from '../../../documents/DocumentTypes';
 import { CollectionView } from '../../collections/CollectionView';
 import { ContextMenu } from '../../ContextMenu';
diff --git a/src/client/views/nodes/generativeFill/GenerativeFill.tsx b/src/client/views/nodes/generativeFill/GenerativeFill.tsx
index 87b7a4069..d5fad296f 100644
--- a/src/client/views/nodes/generativeFill/GenerativeFill.tsx
+++ b/src/client/views/nodes/generativeFill/GenerativeFill.tsx
@@ -14,12 +14,13 @@ import { Doc, DocListCast } from '../../../../fields/Doc';
 import { List } from '../../../../fields/List';
 import { NumCast } from '../../../../fields/Types';
 import { Networking } from '../../../Network';
-import { DocUtils, Docs } from '../../../documents/Documents';
+import { DocUtils } from '../../../documents/DocUtils';
+import { Docs } from '../../../documents/Documents';
 import { DocumentManager } from '../../../util/DocumentManager';
 import { CollectionDockingView } from '../../collections/CollectionDockingView';
 import { CollectionFreeFormView } from '../../collections/collectionFreeForm';
-import { OpenWhereMod } from '../DocumentView';
 import { ImageEditorData } from '../ImageBox';
+import { OpenWhereMod } from '../OpenWhere';
 import './GenerativeFill.scss';
 import Buttons from './GenerativeFillButtons';
 import { BrushHandler } from './generativeFillUtils/BrushHandler';
diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx
index 4cf9e99fa..485ba7367 100644
--- a/src/client/views/nodes/trails/PresBox.tsx
+++ b/src/client/views/nodes/trails/PresBox.tsx
@@ -36,8 +36,10 @@ import { ViewBoxBaseComponent } from '../../DocComponent';
 import { Colors } from '../../global/globalEnums';
 import { LightboxView } from '../../LightboxView';
 import { pinDataTypes as dataTypes } from '../../PinFuncs';
-import { DocumentView, OpenWhere, OpenWhereMod } from '../DocumentView';
-import { FieldView, FieldViewProps, FocusViewOptions } from '../FieldView';
+import { DocumentView } from '../DocumentView';
+import { FieldView, FieldViewProps } from '../FieldView';
+import { FocusViewOptions } from '../FocusViewOptions';
+import { OpenWhere, OpenWhereMod } from '../OpenWhere';
 import { ScriptingBox } from '../ScriptingBox';
 import './PresBox.scss';
 import { PresEffect, PresEffectDirection, PresMovement, PresStatus } from './PresEnums';
@@ -423,7 +425,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
             const acontext = activeItem.config_activeFrame !== undefined ? DocCast(DocCast(activeItem.presentation_targetDoc).embedContainer) : DocCast(activeItem.presentation_targetDoc);
             const context = DocCast(acontext)?.annotationOn ? DocCast(DocCast(acontext).annotationOn) : acontext;
             if (context) {
-                const ffview = DocumentManager.Instance.getFirstDocumentView(context)?.CollectionFreeFormView;
+                const ffview = CollectionFreeFormView.from(DocumentManager.Instance.getFirstDocumentView(context));
                 if (ffview?.childDocs) {
                     PresBox.Instance._keyTimer = CollectionFreeFormView.gotoKeyframe(PresBox.Instance._keyTimer, ffview.childDocs, frameTime);
                     ffview.layoutDoc._currentFrame = NumCast(activeFrame);
@@ -745,7 +747,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
     };
 
     _exitTrail: Opt<() => void>;
-    PlayTrail = (docs: Doc[]) => {
+    playTrail = (docs: Doc[]) => {
         const savedStates = docs.map(doc => {
             switch (doc.type) {
                 case DocumentType.COL:
@@ -2688,3 +2690,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
 ScriptingGlobals.add(function navigateToDoc(bestTarget: Doc, activeItem: Doc) {
     PresBox.NavigateToTarget(bestTarget, activeItem);
 });
+
+Docs.Prototypes.TemplateMap.set(DocumentType.PRES, {
+    layout: { view: PresBox, dataField: 'data' },
+    options: { acl: '', defaultDoubleClick: 'ignore', hideClickBehaviors: true, layout_hideLinkAnchors: true },
+});
diff --git a/src/client/views/nodes/trails/PresElementBox.tsx b/src/client/views/nodes/trails/PresElementBox.tsx
index cf78a45b7..af0ab3b53 100644
--- a/src/client/views/nodes/trails/PresElementBox.tsx
+++ b/src/client/views/nodes/trails/PresElementBox.tsx
@@ -12,7 +12,7 @@ import { List } from '../../../../fields/List';
 import { BoolCast, Cast, DocCast, NumCast, StrCast } from '../../../../fields/Types';
 import { emptyFunction } from '../../../../Utils';
 import { Docs } from '../../../documents/Documents';
-import { CollectionViewType } from '../../../documents/DocumentTypes';
+import { CollectionViewType, DocumentType } from '../../../documents/DocumentTypes';
 import { DocumentManager } from '../../../util/DocumentManager';
 import { DragManager } from '../../../util/DragManager';
 import { SettingsManager } from '../../../util/SettingsManager';
@@ -606,3 +606,8 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
         return !(this.slideDoc instanceof Doc) || this.targetDoc instanceof Promise ? null : this.mainItem;
     }
 }
+
+Docs.Prototypes.TemplateMap.set(DocumentType.PRESELEMENT, {
+    layout: { view: PresElementBox, dataField: 'data' },
+    options: { acl: '', title: 'pres element template', _layout_fitWidth: true, _xMargin: 0, isTemplateDoc: true, isTemplateForField: 'data' },
+});
diff --git a/src/client/views/pdf/Annotation.tsx b/src/client/views/pdf/Annotation.tsx
index f1cd1a4f7..8b2b179d3 100644
--- a/src/client/views/pdf/Annotation.tsx
+++ b/src/client/views/pdf/Annotation.tsx
@@ -9,8 +9,8 @@ import { LinkFollower } from '../../util/LinkFollower';
 import { LinkManager } from '../../util/LinkManager';
 import { undoable } from '../../util/UndoManager';
 import { ObservableReactComponent } from '../ObservableReactComponent';
-import { OpenWhere } from '../nodes/DocumentView';
 import { FieldViewProps } from '../nodes/FieldView';
+import { OpenWhere } from '../nodes/OpenWhere';
 import { AnchorMenu } from './AnchorMenu';
 import './Annotation.scss';
 
diff --git a/src/client/views/pdf/GPTPopup/GPTPopup.tsx b/src/client/views/pdf/GPTPopup/GPTPopup.tsx
index 56ff2959c..c1bfdf176 100644
--- a/src/client/views/pdf/GPTPopup/GPTPopup.tsx
+++ b/src/client/views/pdf/GPTPopup/GPTPopup.tsx
@@ -12,7 +12,8 @@ import { Doc } from '../../../../fields/Doc';
 import { NumCast, StrCast } from '../../../../fields/Types';
 import { Networking } from '../../../Network';
 import { GPTCallType, gptAPICall, gptImageCall } from '../../../apis/gpt/GPT';
-import { DocUtils, Docs } from '../../../documents/Documents';
+import { Docs } from '../../../documents/Documents';
+import { DocUtils } from '../../../documents/DocUtils';
 import { ObservableReactComponent } from '../../ObservableReactComponent';
 import { AnchorMenu } from '../AnchorMenu';
 import './GPTPopup.scss';
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index 22355bc57..15ac73f78 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -6,21 +6,22 @@ import * as Pdfjs from 'pdfjs-dist';
 import 'pdfjs-dist/web/pdf_viewer.css';
 import * as PDFJSViewer from 'pdfjs-dist/web/pdf_viewer.mjs';
 import * as React from 'react';
-import { Doc, DocListCast, Opt } from '../../../fields/Doc';
+import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, ClientUtils, returnAll, returnFalse, returnNone, returnZero, smoothScroll } from '../../../ClientUtils';
+import { CreateLinkToActiveAudio, Doc, DocListCast, Opt } from '../../../fields/Doc';
 import { DocData, Height } from '../../../fields/DocSymbols';
 import { Id } from '../../../fields/FieldSymbols';
 import { InkTool } from '../../../fields/InkField';
 import { Cast, NumCast, StrCast } from '../../../fields/Types';
 import { TraceMobx } from '../../../fields/util';
 import { emptyFunction } from '../../../Utils';
-import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, ClientUtils, returnAll, returnFalse, returnNone, returnZero, smoothScroll } from '../../../ClientUtils';
-import { DocUtils } from '../../documents/Documents';
+import { DocUtils } from '../../documents/DocUtils';
 import { SelectionManager } from '../../util/SelectionManager';
 import { SnappingManager } from '../../util/SnappingManager';
 import { MarqueeOptionsMenu } from '../collections/collectionFreeForm';
 import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView';
 import { MarqueeAnnotator } from '../MarqueeAnnotator';
-import { FocusViewOptions, FieldViewProps } from '../nodes/FieldView';
+import { FieldViewProps } from '../nodes/FieldView';
+import { FocusViewOptions } from '../nodes/FocusViewOptions';
 import { LinkInfo } from '../nodes/LinkDocPreview';
 import { PDFBox } from '../nodes/PDFBox';
 import { ObservableReactComponent } from '../ObservableReactComponent';
@@ -331,7 +332,7 @@ export class PDFViewer extends ObservableReactComponent<IViewerProps> {
             this._ignoreScroll = false;
             if (this._scrollTimer) clearTimeout(this._scrollTimer); // wait until a scrolling pause, then create an anchor to audio
             this._scrollTimer = setTimeout(() => {
-                DocUtils.MakeLinkToActiveAudio(() => this._props.pdfBox.getAnchor(true)!, false);
+                CreateLinkToActiveAudio(() => this._props.pdfBox.getAnchor(true)!, false);
                 this._scrollTimer = undefined;
             }, 200);
         }
diff --git a/src/client/views/search/SearchBox.tsx b/src/client/views/search/SearchBox.tsx
index 384e6d654..5df934231 100644
--- a/src/client/views/search/SearchBox.tsx
+++ b/src/client/views/search/SearchBox.tsx
@@ -4,11 +4,14 @@ import { Tooltip } from '@mui/material';
 import { action, computed, makeObservable, observable } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
+import { ClientUtils } from '../../../ClientUtils';
 import { Doc, DocListCastAsync, Field, FieldType } from '../../../fields/Doc';
 import { DirectLinks, DocData } from '../../../fields/DocSymbols';
+import { Id } from '../../../fields/FieldSymbols';
 import { DocCast, StrCast } from '../../../fields/Types';
+import { DocUtils } from '../../documents/DocUtils';
 import { DocumentType } from '../../documents/DocumentTypes';
-import { DocUtils } from '../../documents/Documents';
+import { Docs } from '../../documents/Documents';
 import { DocumentManager } from '../../util/DocumentManager';
 import { LinkManager } from '../../util/LinkManager';
 import { SearchUtil } from '../../util/SearchUtil';
@@ -21,8 +24,6 @@ import { IRecommendation, Recommendation } from '../newlightbox/components';
 import { fetchRecommendations } from '../newlightbox/utils';
 import { FieldView, FieldViewProps } from '../nodes/FieldView';
 import './SearchBox.scss';
-import { Id } from '../../../fields/FieldSymbols';
-import { ClientUtils } from '../../../ClientUtils';
 
 const DAMPENING_FACTOR = 0.9;
 const MAX_ITERATIONS = 25;
@@ -520,3 +521,8 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() {
         );
     }
 }
+
+Docs.Prototypes.TemplateMap.set(DocumentType.SEARCH, {
+    layout: { view: SearchBox, dataField: 'data' },
+    options: { acl: '', _width: 400 },
+});
diff --git a/src/client/views/selectedDoc/SelectedDocView.tsx b/src/client/views/selectedDoc/SelectedDocView.tsx
index 7ad7b2927..6ed3b240d 100644
--- a/src/client/views/selectedDoc/SelectedDocView.tsx
+++ b/src/client/views/selectedDoc/SelectedDocView.tsx
@@ -8,7 +8,7 @@ import { Doc } from '../../../fields/Doc';
 import { StrCast } from '../../../fields/Types';
 import { DocumentManager } from '../../util/DocumentManager';
 import { SettingsManager } from '../../util/SettingsManager';
-import { FocusViewOptions } from '../nodes/FieldView';
+import { FocusViewOptions } from '../nodes/FocusViewOptions';
 
 export interface SelectedDocViewProps {
     selectedDocs: Doc[];
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index 9083d6ca3..5fd053eef 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -174,6 +174,49 @@ export function updateCachedAcls(doc: Doc) {
     return undefined;
 }
 
+export function ActiveInkPen(): Doc { return Doc.UserDoc(); } // prettier-ignore
+export function ActiveInkColor(): string { return StrCast(ActiveInkPen()?.activeInkColor, 'black'); } // prettier-ignore
+export function ActiveFillColor(): string { return StrCast(ActiveInkPen()?.activeFillColor, ''); } // prettier-ignore
+export function ActiveIsInkMask(): boolean { return BoolCast(ActiveInkPen()?.activeIsInkMask, false); } // prettier-ignore
+export function ActiveInkHideTextLabels(): boolean { return BoolCast(ActiveInkPen().activeInkHideTextLabels, false); } // prettier-ignore
+export function ActiveArrowStart(): string { return StrCast(ActiveInkPen()?.activeArrowStart, ''); } // prettier-ignore
+export function ActiveArrowEnd(): string { return StrCast(ActiveInkPen()?.activeArrowEnd, ''); } // prettier-ignore
+export function ActiveArrowScale(): number { return NumCast(ActiveInkPen()?.activeArrowScale, 1); } // prettier-ignore
+export function ActiveDash(): string { return StrCast(ActiveInkPen()?.activeDash, '0'); } // prettier-ignore
+export function ActiveInkWidth(): number { return Number(ActiveInkPen()?.activeInkWidth); } // prettier-ignore
+export function ActiveInkBezierApprox(): string { return StrCast(ActiveInkPen()?.activeInkBezier); } // prettier-ignore
+
+export function SetActiveInkWidth(width: string): void {
+    !isNaN(parseInt(width)) && ActiveInkPen() && (ActiveInkPen().activeInkWidth = width);
+}
+export function SetActiveBezierApprox(bezier: string): void {
+    ActiveInkPen() && (ActiveInkPen().activeInkBezier = isNaN(parseInt(bezier)) ? '' : bezier);
+}
+export function SetActiveInkColor(value: string) {
+    ActiveInkPen() && (ActiveInkPen().activeInkColor = value);
+}
+export function SetActiveIsInkMask(value: boolean) {
+    ActiveInkPen() && (ActiveInkPen().activeIsInkMask = value);
+}
+export function SetActiveInkHideTextLabels(value: boolean) {
+    ActiveInkPen() && (ActiveInkPen().activeInkHideTextLabels = value);
+}
+export function SetActiveFillColor(value: string) {
+    ActiveInkPen() && (ActiveInkPen().activeFillColor = value);
+}
+export function SetActiveArrowStart(value: string) {
+    ActiveInkPen() && (ActiveInkPen().activeArrowStart = value);
+}
+export function SetActiveArrowEnd(value: string) {
+    ActiveInkPen() && (ActiveInkPen().activeArrowEnd = value);
+}
+export function SetActiveArrowScale(value: number) {
+    ActiveInkPen() && (ActiveInkPen().activeArrowScale = value);
+}
+export function SetActiveDash(dash: string): void {
+    !isNaN(parseInt(dash)) && ActiveInkPen() && (ActiveInkPen().activeDash = dash);
+}
+
 @scriptingGlobal
 @Deserializable('Doc', updateCachedAcls, ['id'])
 export class Doc extends RefField {
@@ -420,6 +463,16 @@ export class Doc extends RefField {
 
 // eslint-disable-next-line no-redeclare
 export namespace Doc {
+    // eslint-disable-next-line import/no-mutable-exports
+    export let SelectOnLoad: Doc | undefined;
+    export function SetSelectOnLoad(doc: Doc | undefined) {
+        SelectOnLoad = doc;
+    }
+    // eslint-disable-next-line import/no-mutable-exports
+    export let DocDragDataName: string = '';
+    export function SetDocDragDataName(name: string) {
+        DocDragDataName = name;
+    }
     export function SetContainer(doc: Doc, container: Doc) {
         if (container !== Doc.MyRecentlyClosed) {
             doc.embedContainer = container;
@@ -1605,6 +1658,29 @@ export namespace Doc {
     }
 }
 
+export function RTFIsFragment(html: string) {
+    return html.indexOf('data-pm-slice') !== -1;
+}
+export function GetHrefFromHTML(html: string): string {
+    const parser = new DOMParser();
+    const parsedHtml = parser.parseFromString(html, 'text/html');
+    if (parsedHtml.body.childNodes.length === 1 && parsedHtml.body.childNodes[0].childNodes.length === 1 && (parsedHtml.body.childNodes[0].childNodes[0] as any).href) {
+        return (parsedHtml.body.childNodes[0].childNodes[0] as any).href;
+    }
+    return '';
+}
+export function GetDocFromUrl(url: string) {
+    return url.startsWith(document.location.origin) ? new URL(url).pathname.split('doc/').lastElement() : ''; // docId
+}
+
+let activeAudioLinker: (f: () => Doc | undefined, broadcast?: boolean) => (Doc | undefined)[];
+export function SetActiveAudioLinker(func: (f: () => Doc | undefined, broadcast?: boolean) => (Doc | undefined)[]) {
+    activeAudioLinker = func;
+}
+export function CreateLinkToActiveAudio(func: () => Doc | undefined, broadcast?: boolean) {
+    return activeAudioLinker(func, broadcast);
+}
+
 export function IdToDoc(id: string) {
     return DocCast(DocServer.GetCachedRefField(id));
 }
diff --git a/src/fields/RichTextUtils.ts b/src/fields/RichTextUtils.ts
index a2cc47a43..5eb60a2f8 100644
--- a/src/fields/RichTextUtils.ts
+++ b/src/fields/RichTextUtils.ts
@@ -12,7 +12,8 @@ import { DocServer } from '../client/DocServer';
 import { Networking } from '../client/Network';
 import { GoogleApiClientUtils } from '../client/apis/google_docs/GoogleApiClientUtils';
 import { GooglePhotos } from '../client/apis/google_docs/GooglePhotosClientUtils';
-import { DocUtils, Docs } from '../client/documents/Documents';
+import { Docs } from '../client/documents/Documents';
+import { DocUtils } from '../client/documents/DocUtils';
 import { FormattedTextBox } from '../client/views/nodes/formattedText/FormattedTextBox';
 import { schema } from '../client/views/nodes/formattedText/schema_rts';
 import { Doc, Opt } from './Doc';
diff --git a/src/fields/ScriptField.ts b/src/fields/ScriptField.ts
index 8a3787768..5e5f5527f 100644
--- a/src/fields/ScriptField.ts
+++ b/src/fields/ScriptField.ts
@@ -277,6 +277,10 @@ export class ComputedField extends ScriptField {
     }
 }
 
+export function FollowLinkScript() {
+    return ScriptField.MakeScript('return followLink(this,altKey)', { altKey: 'boolean' });
+}
+
 ScriptingGlobals.add(
     // eslint-disable-next-line prefer-arrow-callback
     function setIndexVal(list: any[], index: number, value: any) {
diff --git a/src/mobile/MobileInterface.tsx b/src/mobile/MobileInterface.tsx
index b355e70a7..55a85ed1b 100644
--- a/src/mobile/MobileInterface.tsx
+++ b/src/mobile/MobileInterface.tsx
@@ -127,6 +127,7 @@ import './AudioUpload.scss';
 import { Uploader } from './ImageUpload';
 import './ImageUpload.scss';
 import './MobileInterface.scss';
+import { DashboardView } from '../client/views/DashboardView';
 
 library.add(
     ...[
@@ -583,7 +584,7 @@ export class MobileInterface extends React.Component {
         };
 
         const freeformDoc = Doc.GuestTarget || Docs.Create.FreeformDocument([], freeformOptions);
-        const dashboardDoc = Docs.Create.StandardCollectionDockingDocument([{ doc: freeformDoc, initialWidth: 600 }], { title: `Dashboard ${dashboardCount}` }, id, 'row');
+        const dashboardDoc = DashboardView.StandardCollectionDockingDocument([{ doc: freeformDoc, initialWidth: 600 }], { title: `Dashboard ${dashboardCount}` }, id, 'row');
 
         const toggleComic = ScriptField.MakeScript(`toggleComicMode()`);
         const cloneDashboard = ScriptField.MakeScript(`cloneDashboard()`);
-- 
cgit v1.2.3-70-g09d2


From dd08c20ec6df3fad6ecd6b16c787f10b0c23feb4 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Thu, 2 May 2024 00:39:31 -0400
Subject: lots more dependency cycle unwinding.

---
 src/client/documents/DocUtils.ts                   |  36 ++--
 src/client/documents/Documents.ts                  |   4 +-
 src/client/util/BranchingTrailManager.tsx          |   5 +-
 src/client/util/CalendarManager.tsx                |  20 +-
 src/client/util/CaptureManager.tsx                 |   9 +-
 src/client/util/CurrentUserUtils.ts                |  24 +--
 src/client/util/DictationManager.ts                |  44 ++++-
 src/client/util/DocumentManager.ts                 |  85 +++-----
 src/client/util/DragManager.ts                     |  13 --
 src/client/util/GroupManager.tsx                   |   2 +-
 src/client/util/HypothesisUtils.ts                 |  10 +-
 .../util/Import & Export/DirectoryImportBox.tsx    |   1 -
 src/client/util/LinkFollower.ts                    |  31 ++-
 src/client/util/LinkManager.ts                     |   9 +-
 src/client/util/PingManager.ts                     |   4 +-
 src/client/util/ReplayMovements.ts                 |   6 +-
 src/client/util/Scripting.ts                       |   9 +-
 src/client/util/SelectionManager.ts                |  29 +--
 src/client/util/SettingsManager.tsx                |  20 +-
 src/client/util/SharingManager.tsx                 |  12 +-
 src/client/util/SnappingManager.ts                 |  16 +-
 src/client/views/AntimodeMenu.tsx                  |   8 +-
 src/client/views/ComponentDecorations.tsx          |   4 +-
 src/client/views/DocComponent.tsx                  |   2 +
 src/client/views/DocViewUtils.ts                   |  21 ++
 src/client/views/DocumentButtonBar.tsx             |  29 +--
 src/client/views/DocumentDecorations.tsx           | 117 ++++++-----
 src/client/views/FieldsDropdown.tsx                |  22 +--
 src/client/views/FilterPanel.tsx                   |   8 +-
 src/client/views/GestureOverlay.tsx                |   6 +-
 src/client/views/GlobalKeyHandler.ts               |  78 +++++---
 src/client/views/InkControlPtHandles.tsx           |   6 +-
 src/client/views/InkStrokeProperties.ts            |   3 +-
 src/client/views/InkTranscription.tsx              |   1 -
 src/client/views/InkingStroke.tsx                  |   2 +-
 src/client/views/LightboxView.tsx                  |  36 ++--
 src/client/views/Main.tsx                          |  12 +-
 src/client/views/MainView.tsx                      |  85 ++++----
 src/client/views/MarqueeAnnotator.tsx              |   1 -
 src/client/views/OverlayView.tsx                   |   5 +-
 src/client/views/PropertiesButtons.tsx             |  29 ++-
 .../views/PropertiesDocBacklinksSelector.tsx       |   8 +-
 src/client/views/PropertiesDocContextSelector.tsx  |   3 +-
 src/client/views/PropertiesView.tsx                |  50 +++--
 src/client/views/ScriptBox.tsx                     |   8 +-
 src/client/views/ScriptingRepl.tsx                 |  16 +-
 src/client/views/SidebarAnnos.tsx                  |   8 +-
 src/client/views/StyleProp.ts                      |  24 +++
 src/client/views/StyleProvider.tsx                 | 105 ++++------
 .../views/collections/CollectionCarousel3DView.tsx |   5 +-
 .../views/collections/CollectionCarouselView.tsx   |   2 +-
 .../views/collections/CollectionDockingView.tsx    |  18 +-
 src/client/views/collections/CollectionMenu.tsx    |  32 ++-
 .../views/collections/CollectionNoteTakingView.tsx |   2 +-
 .../views/collections/CollectionPileView.tsx       |   4 +-
 .../collections/CollectionStackedTimeline.tsx      |  16 +-
 .../views/collections/CollectionStackingView.tsx   |   2 +-
 src/client/views/collections/CollectionSubView.tsx |  11 +-
 .../views/collections/CollectionTimeView.tsx       |   3 +-
 .../views/collections/CollectionTreeView.tsx       |  17 +-
 .../views/collections/CollectionTreeViewType.ts    |   5 +
 src/client/views/collections/TabDocView.tsx        | 216 +++++++++++----------
 src/client/views/collections/TreeView.tsx          |  15 +-
 .../CollectionFreeFormClusters.ts                  |  31 ++-
 .../CollectionFreeFormInfoUI.tsx                   |  10 +-
 .../CollectionFreeFormPannableContents.tsx         |   9 +-
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  88 ++++-----
 .../collections/collectionFreeForm/MarqueeView.tsx |  15 +-
 .../collectionLinear/CollectionLinearView.tsx      |  10 +-
 .../collectionMulticolumn/MulticolumnResizer.tsx   |   2 +-
 .../collectionMulticolumn/MultirowResizer.tsx      |   2 +-
 .../collectionSchema/CollectionSchemaView.tsx      |  28 +--
 .../collectionSchema/SchemaTableCell.tsx           |   8 +-
 src/client/views/global/globalScripts.ts           |  58 +++---
 src/client/views/linking/LinkMenuGroup.tsx         |   4 +-
 src/client/views/linking/LinkMenuItem.tsx          |  31 ++-
 .../views/newlightbox/ButtonMenu/ButtonMenu.tsx    |   3 +-
 src/client/views/newlightbox/NewLightboxView.tsx   |  27 ++-
 .../components/Recommendation/Recommendation.tsx   |  12 +-
 src/client/views/nodes/AudioBox.tsx                |  25 +--
 .../views/nodes/CollectionFreeFormDocumentView.tsx |  41 ++--
 src/client/views/nodes/ComparisonBox.tsx           |   2 +-
 src/client/views/nodes/DataVizBox/DataVizBox.tsx   |   3 +-
 .../nodes/DataVizBox/components/LineChart.tsx      |   6 +-
 .../views/nodes/DataVizBox/components/PieChart.tsx |   2 +-
 .../views/nodes/DataVizBox/components/TableBox.tsx |   9 +-
 src/client/views/nodes/DocumentContentsView.tsx    |  13 +-
 src/client/views/nodes/DocumentIcon.tsx            |  11 +-
 src/client/views/nodes/DocumentLinksButton.tsx     |  13 --
 src/client/views/nodes/DocumentView.tsx            | 203 +++++++++----------
 src/client/views/nodes/FontIconBox/FontIconBox.tsx |  38 ++--
 src/client/views/nodes/FunctionPlotBox.tsx         |   7 +-
 src/client/views/nodes/ImageBox.tsx                |   6 +-
 src/client/views/nodes/KeyValueBox.tsx             |   4 +-
 src/client/views/nodes/LabelBox.tsx                |   2 +-
 src/client/views/nodes/LinkAnchorBox.scss          |  34 ----
 src/client/views/nodes/LinkAnchorBox.tsx           | 118 -----------
 src/client/views/nodes/LinkBox.tsx                 |   5 +-
 src/client/views/nodes/LinkDescriptionPopup.tsx    |   3 +-
 src/client/views/nodes/LinkDocPreview.tsx          |  26 ++-
 src/client/views/nodes/LoadingBox.tsx              |  21 +-
 .../views/nodes/MapBox/DirectionsAnchorMenu.tsx    |   4 +-
 src/client/views/nodes/MapBox/MapAnchorMenu.tsx    |   4 +-
 src/client/views/nodes/MapBox/MapBox.tsx           |   6 +-
 src/client/views/nodes/MapBox/MapBox2.tsx          |   6 +-
 .../views/nodes/MapboxMapBox/MapboxContainer.tsx   |   6 +-
 src/client/views/nodes/PDFBox.tsx                  |   6 +-
 .../views/nodes/RecordingBox/RecordingBox.tsx      |  16 +-
 src/client/views/nodes/ScreenshotBox.tsx           |  11 +-
 src/client/views/nodes/VideoBox.tsx                |  28 ++-
 src/client/views/nodes/WebBox.tsx                  |   5 +-
 .../views/nodes/formattedText/DashFieldView.tsx    |   5 +-
 .../views/nodes/formattedText/FormattedTextBox.tsx |  45 ++---
 .../formattedText/ProsemirrorExampleTransfer.ts    |   4 +-
 .../views/nodes/formattedText/RichTextMenu.tsx     |  17 +-
 .../views/nodes/formattedText/SummaryView.tsx      |   5 +-
 .../views/nodes/generativeFill/GenerativeFill.tsx  |   8 +-
 src/client/views/nodes/trails/PresBox.tsx          |  58 +++---
 src/client/views/nodes/trails/PresElementBox.tsx   |  11 +-
 src/client/views/pdf/AnchorMenu.tsx                |   6 +-
 src/client/views/pdf/Annotation.tsx                |   6 +-
 src/client/views/pdf/PDFViewer.tsx                 |   6 +-
 src/client/views/search/SearchBox.tsx              |  31 ++-
 src/client/views/selectedDoc/SelectedDocView.tsx   |  14 +-
 src/client/views/topbar/TopBar.tsx                 |   8 +-
 src/fields/Doc.ts                                  |  19 +-
 src/fields/ScriptField.ts                          |   1 -
 src/fields/documentSchemas.ts                      |   1 -
 src/mobile/MobileInterface.tsx                     |   9 +-
 src/server/SharedMediaTypes.ts                     |   5 +
 130 files changed, 1254 insertions(+), 1461 deletions(-)
 create mode 100644 src/client/views/DocViewUtils.ts
 create mode 100644 src/client/views/StyleProp.ts
 create mode 100644 src/client/views/collections/CollectionTreeViewType.ts
 delete mode 100644 src/client/views/nodes/LinkAnchorBox.scss
 delete mode 100644 src/client/views/nodes/LinkAnchorBox.tsx

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

diff --git a/src/client/documents/DocUtils.ts b/src/client/documents/DocUtils.ts
index 0a47e0359..0c9fe0315 100644
--- a/src/client/documents/DocUtils.ts
+++ b/src/client/documents/DocUtils.ts
@@ -9,7 +9,7 @@ import { ClientUtils } from '../../ClientUtils';
 import * as JSZipUtils from '../../JSZipUtils';
 import { decycle } from '../../decycler/decycler';
 import { DateField } from '../../fields/DateField';
-import { Doc, DocListCast, Field, FieldType, LinkedTo, Opt, SetActiveAudioLinker, StrListCast } from '../../fields/Doc';
+import { Doc, DocListCast, Field, FieldResult, FieldType, LinkedTo, Opt, StrListCast } from '../../fields/Doc';
 import { DocData } from '../../fields/DocSymbols';
 import { Id } from '../../fields/FieldSymbols';
 import { InkDataFieldName, InkField } from '../../fields/InkField';
@@ -29,9 +29,7 @@ import { SerializationHelper } from '../util/SerializationHelper';
 import { UndoManager, undoable } from '../util/UndoManager';
 import { ContextMenu } from '../views/ContextMenu';
 import { ContextMenuProps } from '../views/ContextMenuItem';
-import { FieldViewProps } from '../views/nodes/FieldView';
 import { LinkDescriptionPopup } from '../views/nodes/LinkDescriptionPopup';
-import { LoadingBox } from '../views/nodes/LoadingBox';
 import { OpenWhere } from '../views/nodes/OpenWhere';
 import { TaskCompletionBox } from '../views/nodes/TaskCompletedBox';
 import { DocumentType } from './DocumentTypes';
@@ -50,7 +48,7 @@ export namespace DocUtils {
         }
         if (key === LinkedTo) {
             // links are not a field value, so handled here.  value is an expression of form ([field=]idToDoc("..."))
-            const allLinks = LinkManager.Instance.getAllRelatedLinks(doc);
+            const allLinks = Doc.Links(doc);
             const matchLink = (val: string, anchor: Doc) => {
                 const linkedToExp = (val ?? '').split('=');
                 if (linkedToExp.length === 1) return Field.toScriptString(anchor) === val;
@@ -131,7 +129,7 @@ export namespace DocUtils {
                       if (!unsets.length && !exists.length && !xs.length && !checks.length && !matches.length) return true;
                       const failsNotEqualFacets = !xs.length ? false : xs.some(value => matchFieldValue(d, facetKey, value));
                       const satisfiesCheckFacets = !checks.length ? true : checks.some(value => matchFieldValue(d, facetKey, value));
-                      const satisfiesExistsFacets = !exists.length ? true : facetKey !== LinkedTo ? d[facetKey] !== undefined : LinkManager.Instance.getAllRelatedLinks(d).length;
+                      const satisfiesExistsFacets = !exists.length ? true : facetKey !== LinkedTo ? d[facetKey] !== undefined : Doc.Links(d).length;
                       const satisfiesUnsetsFacets = !unsets.length ? true : d[facetKey] === undefined;
                       const satisfiesMatchFacets = !matches.length
                           ? true
@@ -170,19 +168,7 @@ export namespace DocUtils {
         return rangeFilteredDocs;
     }
 
-    export const ActiveRecordings: { props: FieldViewProps; getAnchor: (addAsAnnotation: boolean) => Doc }[] = [];
-
-    export function MakeLinkToActiveAudio(getSourceDoc: () => Doc | undefined, broadcastEvent = true) {
-        broadcastEvent && runInAction(() => { Doc.RecordingEvent += 1; }); // prettier-ignore
-        return DocUtils.ActiveRecordings.map(audio => {
-            const sourceDoc = getSourceDoc();
-            return sourceDoc && DocUtils.MakeLink(sourceDoc, audio.getAnchor(true) || audio.props.Document, { link_displayLine: false, link_relationship: 'recording annotation:linked recording', link_description: 'recording timeline' });
-        });
-    }
-
-    SetActiveAudioLinker(MakeLinkToActiveAudio);
-
-    export function MakeLink(source: Doc, target: Doc, linkSettings: { link_relationship?: string; link_description?: string; link_displayLine?: boolean }, id?: string, showPopup?: number[]) {
+    export function MakeLink(source: Doc, target: Doc, linkSettings: { link_relationship?: string; link_description?: string }, id?: string, showPopup?: number[]) {
         if (!linkSettings.link_relationship) linkSettings.link_relationship = target.type === DocumentType.RTF ? 'Commentary:Comments On' : 'link';
         if (target.doc === Doc.UserDoc()) return undefined;
 
@@ -232,7 +218,6 @@ export namespace DocUtils {
                     title: ComputedField.MakeFunction('generateLinkTitle(this)') as any,
                     link_anchor_1_useSmallAnchor: source.useSmallAnchor ? true : undefined,
                     link_anchor_2_useSmallAnchor: target.useSmallAnchor ? true : undefined,
-                    link_displayLine: linkSettings.link_displayLine,
                     link_relationship: linkSettings.link_relationship,
                     link_description: linkSettings.link_description,
                     x: ComputedField.MakeFunction(`((this.${a}?.x||0)+(this.${b}?.x||0))/2`) as any,
@@ -256,7 +241,6 @@ export namespace DocUtils {
                 const script = scripts[key];
                 if (ScriptCast(doc[key])?.script.originalScript !== scripts[key] && script) {
                     (key.startsWith('_') ? doc : Doc.GetProto(doc))[key] = ScriptField.MakeScript(script, {
-                        self: Doc.name,
                         this: Doc.name,
                         dragData: Doc.DocDragDataName,
                         value: 'any',
@@ -579,7 +563,7 @@ export namespace DocUtils {
     export function LeavePushpin(doc: Doc, annotationField: string) {
         if (doc.followLinkToggle) return undefined;
         const context = Cast(doc.embedContainer, Doc, null) ?? Cast(doc.annotationOn, Doc, null);
-        const hasContextAnchor = LinkManager.Links(doc).some(l => (l.link_anchor_2 === doc && Cast(l.link_anchor_1, Doc, null)?.annotationOn === context) || (l.link_anchor_1 === doc && Cast(l.link_anchor_2, Doc, null)?.annotationOn === context));
+        const hasContextAnchor = Doc.Links(doc).some(l => (l.link_anchor_2 === doc && Cast(l.link_anchor_1, Doc, null)?.annotationOn === context) || (l.link_anchor_1 === doc && Cast(l.link_anchor_2, Doc, null)?.annotationOn === context));
         if (context && !hasContextAnchor && (context.type === DocumentType.VID || context.type === DocumentType.WEB || context.type === DocumentType.PDF || context.type === DocumentType.IMG)) {
             const pushpin = Docs.Create.FontIconDocument({
                 title: '',
@@ -673,7 +657,7 @@ export namespace DocUtils {
                 proto.data_duration = result.duration;
             }
             if (overwriteDoc) {
-                LoadingBox.removeCurrentlyLoading(overwriteDoc);
+                Doc.removeCurrentlyLoading(overwriteDoc);
             }
             generatedDocuments.push(doc);
         }
@@ -717,7 +701,7 @@ export namespace DocUtils {
                 if (overwriteDoc) {
                     overwriteDoc.isLoading = false;
                     overwriteDoc.loadingError = (result as any).message;
-                    LoadingBox.removeCurrentlyLoading(overwriteDoc);
+                    Doc.removeCurrentlyLoading(overwriteDoc);
                 }
             } else newFilename && processFileupload(generatedDocuments, newFilename, mimetype ?? '', result, options, overwriteDoc);
         });
@@ -755,7 +739,7 @@ export namespace DocUtils {
             if ((result as any).message) {
                 if (overwriteDoc) {
                     overwriteDoc.loadingError = (result as any).message;
-                    LoadingBox.removeCurrentlyLoading(overwriteDoc);
+                    Doc.removeCurrentlyLoading(overwriteDoc);
                 }
             } else newFilename && mimetype && processFileupload(generatedDocuments, newFilename, mimetype, result, options, overwriteDoc);
         });
@@ -868,6 +852,10 @@ export function FollowLinkScript() {
     return ScriptField.MakeScript('return followLink(this,altKey)', { altKey: 'boolean' });
 }
 
+export function IsFollowLinkScript(field: FieldResult<FieldType>) {
+    return ScriptCast(field)?.script.originalScript.includes('return followLink(');
+}
+
 ScriptingGlobals.add('Docs', Docs);
 // eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function copyDragFactory(dragFactory: Doc, asDelegate?: boolean) {
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index a3fea1ce4..1c18c0d84 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -19,7 +19,6 @@ import { SharingPermissions } from '../../fields/util';
 import { PointData } from '../../pen-gestures/GestureTypes';
 import { DocServer } from '../DocServer';
 import { dropActionType } from '../util/DropActionTypes';
-import { LinkManager } from '../util/LinkManager';
 import { CollectionViewType, DocumentType } from './DocumentTypes';
 
 class EmptyBox {
@@ -394,7 +393,6 @@ export class DocumentOptions {
     link?: string;
     link_description?: string; // added for links
     link_relationship?: string; // type of relatinoship a link represents
-    link_displayLine?: BOOLt = new BoolInfo('whether a link line should be dipslayed between the two link anchors');
     link_displayArrow?: BOOLt = new BoolInfo("whether to display link's directional arrowhead");
     link_anchor_1?: DOCt = new DocInfo('start anchor of a link');
     link_anchor_2?: DOCt = new DocInfo('end anchor of a link');
@@ -810,7 +808,7 @@ export namespace Docs {
                 'link'
             );
 
-            LinkManager.Instance.addLink(linkDoc);
+            Doc.AddLink(linkDoc);
 
             return linkDoc;
         }
diff --git a/src/client/util/BranchingTrailManager.tsx b/src/client/util/BranchingTrailManager.tsx
index 28c00644f..119d103c5 100644
--- a/src/client/util/BranchingTrailManager.tsx
+++ b/src/client/util/BranchingTrailManager.tsx
@@ -6,8 +6,8 @@ import * as React from 'react';
 import { Doc } from '../../fields/Doc';
 import { Id } from '../../fields/FieldSymbols';
 import { OverlayView } from '../views/OverlayView';
+import { DocumentView } from '../views/nodes/DocumentView';
 import { PresBox } from '../views/nodes/trails';
-import { DocumentManager } from './DocumentManager';
 
 @observer
 export class BranchingTrailManager extends React.Component {
@@ -46,7 +46,6 @@ export class BranchingTrailManager extends React.Component {
         // hi.overlayY = 100;
 
         // Doc.AddToMyOverlay(hi);
-        console.log(DocumentManager._overlayViews);
     };
 
     @action setSlideHistoryStack = action((newArr: String[]) => {
@@ -103,7 +102,7 @@ export class BranchingTrailManager extends React.Component {
         this.setSlideHistoryStack(newStack);
 
         removed.forEach(info => this.containsSet.delete(info.toString()));
-        DocumentManager.Instance.showDocument(targetDoc, { willZoomCentered: true });
+        DocumentView.showDocument(targetDoc, { willZoomCentered: true });
         if (this.slideHistoryStack.length === 0) {
             Doc.UserDoc().isBranchingMode = false;
         }
diff --git a/src/client/util/CalendarManager.tsx b/src/client/util/CalendarManager.tsx
index 8e3936d34..77cf80151 100644
--- a/src/client/util/CalendarManager.tsx
+++ b/src/client/util/CalendarManager.tsx
@@ -13,15 +13,12 @@ import { Doc, DocListCast } from '../../fields/Doc';
 import { DocData } from '../../fields/DocSymbols';
 import { StrCast } from '../../fields/Types';
 import { Docs } from '../documents/Documents';
-import { DictationOverlay } from '../views/DictationOverlay';
 import { MainViewModal } from '../views/MainViewModal';
 import { ObservableReactComponent } from '../views/ObservableReactComponent';
 import { DocumentView } from '../views/nodes/DocumentView';
 import { TaskCompletionBox } from '../views/nodes/TaskCompletedBox';
 import './CalendarManager.scss';
-import { DocumentManager } from './DocumentManager';
-import { SelectionManager } from './SelectionManager';
-import { SettingsManager } from './SettingsManager';
+import { SnappingManager } from './SnappingManager';
 // import 'react-date-range/dist/styles.css';
 // import 'react-date-range/dist/theme/default.css';
 
@@ -85,11 +82,9 @@ export class CalendarManager extends ObservableReactComponent<{}> {
     };
 
     public open = (target?: DocumentView, targetDoc?: Doc) => {
-        console.log('hi');
         runInAction(() => {
             this.targetDoc = targetDoc || target?.Document;
             this.targetDocView = target;
-            DictationOverlay.Instance.hasActiveModal = true;
             this.isOpen = this.targetDoc !== undefined;
         });
     };
@@ -99,7 +94,6 @@ export class CalendarManager extends ObservableReactComponent<{}> {
         TaskCompletionBox.taskCompleted = false;
         setTimeout(
             action(() => {
-                DictationOverlay.Instance.hasActiveModal = false;
                 this.targetDoc = undefined;
             }),
             500
@@ -137,7 +131,7 @@ export class CalendarManager extends ObservableReactComponent<{}> {
 
     // TODO: Make undoable
     private addToCalendar = () => {
-        const docs = SelectionManager.Views.length < 2 ? [this.targetDoc] : SelectionManager.Views.map(docView => docView.Document);
+        const docs = DocumentView.Selected().length < 2 ? [this.targetDoc] : DocumentView.Selected().map(docView => docView.Document);
         const targetDoc = this.layoutDocAcls ? docs[0] : docs[0]?.[DocData]; // doc to add to calendar
 
         console.log(targetDoc);
@@ -190,14 +184,14 @@ export class CalendarManager extends ObservableReactComponent<{}> {
 
     private focusOn = (contents: string) => {
         const title = this.targetDoc ? StrCast(this.targetDoc.title) : '';
-        const docs = SelectionManager.Views.length > 1 ? SelectionManager.Views.map(docView => docView.Document) : [this.targetDoc];
+        const docs = DocumentView.Selected().length > 1 ? DocumentView.Selected().map(docView => docView.Document) : [this.targetDoc];
         return (
             <span
                 className="focus-span"
                 title={title}
                 onClick={() => {
                     if (this.targetDoc && this.targetDocView && docs.length === 1) {
-                        DocumentManager.Instance.showDocument(this.targetDoc, { willZoomCentered: true });
+                        DocumentView.showDocument(this.targetDoc, { willZoomCentered: true });
                     }
                 }}
                 onPointerEnter={action(() => {
@@ -251,17 +245,17 @@ export class CalendarManager extends ObservableReactComponent<{}> {
 
     @computed
     get calendarInterface() {
-        const docs = SelectionManager.Views.length < 2 ? [this.targetDoc] : SelectionManager.Views.map(docView => docView.Document);
+        const docs = DocumentView.Selected().length < 2 ? [this.targetDoc] : DocumentView.Selected().map(docView => docView.Document);
         const targetDoc = this.layoutDocAcls ? docs[0] : docs[0]?.[DocData];
 
         return (
             <div
                 className="calendar-interface"
                 style={{
-                    background: SettingsManager.userBackgroundColor,
+                    background: SnappingManager.userBackgroundColor,
                     color: StrCast(Doc.UserDoc().userColor),
                 }}>
-                <p className="selected-doc-title" style={{ color: SettingsManager.userColor }}>
+                <p className="selected-doc-title" style={{ color: SnappingManager.userColor }}>
                     <b>{this.focusOn(docs.length < 2 ? StrCast(targetDoc?.title, 'this document') : '-multiple-')}</b>
                 </p>
                 <div className="creation-type-container">
diff --git a/src/client/util/CaptureManager.tsx b/src/client/util/CaptureManager.tsx
index a03c80f2a..4fd934774 100644
--- a/src/client/util/CaptureManager.tsx
+++ b/src/client/util/CaptureManager.tsx
@@ -10,8 +10,7 @@ import { DocCast, StrCast } from '../../fields/Types';
 import { LightboxView } from '../views/LightboxView';
 import { MainViewModal } from '../views/MainViewModal';
 import './CaptureManager.scss';
-import { LinkManager } from './LinkManager';
-import { SelectionManager } from './SelectionManager';
+import { DocumentView } from '../views/nodes/DocumentView';
 
 @observer
 export class CaptureManager extends React.Component<{}> {
@@ -56,7 +55,7 @@ export class CaptureManager extends React.Component<{}> {
         const doc = this._document;
         const order: JSX.Element[] = [];
         if (doc) {
-            LinkManager.Links(doc).forEach((l, i) =>
+            Doc.Links(doc).forEach((l, i) =>
                 order.push(
                     <div className="list-item">
                         <div className="number">{i}</div>
@@ -88,8 +87,8 @@ export class CaptureManager extends React.Component<{}> {
                     <div
                         className="cancel"
                         onClick={() => {
-                            const selected = SelectionManager.Views.slice();
-                            SelectionManager.DeselectAll();
+                            const selected = DocumentView.Selected();
+                            DocumentView.DeselectAll();
                             selected.map(dv => dv.props.removeDocument?.(dv.Document));
                             this.close();
                         }}>
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index 15c8fec5a..ffc1304bf 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -1,4 +1,4 @@
-import { observable, reaction, runInAction } from "mobx";
+import { reaction, runInAction } from "mobx";
 import * as rp from 'request-promise';
 import { ClientUtils, OmitKeys } from "../../ClientUtils";
 import { Doc, DocListCast, DocListCastAsync, Opt } from "../../fields/Doc";
@@ -19,7 +19,8 @@ import { CollectionViewType, DocumentType } from "../documents/DocumentTypes";
 import { Docs, DocumentOptions, FInfo, FInfoFieldType } from "../documents/Documents";
 import { DashboardView } from "../views/DashboardView";
 import { OverlayView } from "../views/OverlayView";
-import { CollectionTreeView, TreeViewType } from "../views/collections/CollectionTreeView";
+import { CollectionTreeView } from "../views/collections/CollectionTreeView";
+import { TreeViewType } from "../views/collections/CollectionTreeViewType";
 import { Colors } from "../views/global/globalEnums";
 import { mediaState } from "../views/nodes/AudioBox";
 import { ButtonType, FontIconBox } from "../views/nodes/FontIconBox/FontIconBox";
@@ -30,8 +31,9 @@ import { ImportElementBox } from "../views/nodes/importBox/ImportElementBox";
 import { DragManager } from "./DragManager";
 import { dropActionType } from "./DropActionTypes";
 import { MakeTemplate } from "./DropConverter";
-import { LinkManager, UPDATE_SERVER_CACHE } from "./LinkManager";
+import { UPDATE_SERVER_CACHE } from "./LinkManager";
 import { ScriptingGlobals } from "./ScriptingGlobals";
+import { SelectionManager } from "./SelectionManager";
 import { ColorScheme } from "./SettingsManager";
 import { SnappingManager } from "./SnappingManager";
 import { UndoManager } from "./UndoManager";
@@ -937,14 +939,13 @@ pie title Minerals in my tap water
         DocUtils.AssignDocField(doc, "globalScriptDatabase", () =>  Docs.Prototypes.MainScriptDocument(), {});
         DocUtils.AssignDocField(doc, "myHeaderBar", (opts) => Docs.Create.MulticolumnDocument([], opts), { title: "My Header Bar", isSystem: true, _chromeHidden:true, layout_maxShown: 10, childLayoutFitWidth:false, childDocumentsActive:false, dropAction: dropActionType.move}); // drop down panel at top of dashboard for stashing documents
         
-        Doc.AddDocToList(Doc.MyFilesystem, undefined, Doc.MyDashboards)
-        Doc.AddDocToList(Doc.MyFilesystem, undefined, Doc.MySharedDocs)
-        Doc.AddDocToList(Doc.MyFilesystem, undefined, Doc.MyRecentlyClosed)
-
-        Doc.GetProto(DocCast(Doc.UserDoc().emptyWebpage)).data = new WebField("https://www.wikipedia.org")
+        SelectionManager.DeselectAll(); // this forces SelectionManager implementation to copy over to DocumentView's API.  This also triggers the LinkManager to be created
+        
+        Doc.AddDocToList(Doc.MyFilesystem, undefined, Doc.MyDashboards);
+        Doc.AddDocToList(Doc.MyFilesystem, undefined, Doc.MySharedDocs);
+        Doc.AddDocToList(Doc.MyFilesystem, undefined, Doc.MyRecentlyClosed);
 
-        // eslint-disable-next-line no-new
-        new LinkManager();
+        Doc.GetProto(DocCast(Doc.UserDoc().emptyWebpage)).data = new WebField("https://www.wikipedia.org");
 
         DocServer.CacheNeedsUpdate() && setTimeout(UPDATE_SERVER_CACHE, 2500);
         setInterval(UPDATE_SERVER_CACHE, 120000);
@@ -969,12 +970,11 @@ pie title Minerals in my tap water
         });
     }
 
-    @observable public static ServerVersion: string = ';'
     public static async loadCurrentUser() {
         return rp.get(ClientUtils.prepend("/getCurrentUser")).then(async response => {
             if (response) {
                 const result: { version: string, userDocumentId: string, sharingDocumentId: string, linkDatabaseId: string,  email: string, cacheDocumentIds: string, resolvedPorts: string } = JSON.parse(response);
-                runInAction(() => { CurrentUserUtils.ServerVersion = result.version; });
+                runInAction(() => { SnappingManager.SetServerVersion(result.version); });
                 ClientUtils.SetCurrentUserEmail(result.email);
                 resolvedPorts = result.resolvedPorts as any;
                 DocServer.init(window.location.protocol, window.location.hostname, resolvedPorts?.socket, result.email);
diff --git a/src/client/util/DictationManager.ts b/src/client/util/DictationManager.ts
index 97ee628e2..bc9fe813f 100644
--- a/src/client/util/DictationManager.ts
+++ b/src/client/util/DictationManager.ts
@@ -10,12 +10,13 @@ import { RichTextField } from '../../fields/RichTextField';
 import { listSpec } from '../../fields/Schema';
 import { Cast, CastCtor } from '../../fields/Types';
 import { AudioField, ImageField } from '../../fields/URLField';
+import { AudioAnnoState } from '../../server/SharedMediaTypes';
+import { Networking } from '../Network';
 import { DocumentType } from '../documents/DocumentTypes';
 import { Docs } from '../documents/Documents';
 import { DictationOverlay } from '../views/DictationOverlay';
 import { DocumentView } from '../views/nodes/DocumentView';
 import { OpenWhere } from '../views/nodes/OpenWhere';
-import { SelectionManager } from './SelectionManager';
 import { UndoManager } from './UndoManager';
 
 /**
@@ -244,7 +245,7 @@ export namespace DictationManager {
         export const execute = async (phrase: string) =>
             UndoManager.RunInBatch(async () => {
                 console.log('PHRASE: ' + phrase);
-                const targets = SelectionManager.Views;
+                const targets = DocumentView.Selected();
                 if (!targets || !targets.length) {
                     return undefined;
                 }
@@ -395,4 +396,43 @@ export namespace DictationManager {
             },
         ];
     }
+    export function recordAudioAnnotation(dataDoc: Doc, field: string, onRecording?: (stop: () => void) => void, onEnd?: () => void) {
+        let gumStream: any;
+        let recorder: any;
+        navigator.mediaDevices.getUserMedia({ audio: true }).then(stream => {
+            let audioTextAnnos = Cast(dataDoc[field + '_audioAnnotations_text'], listSpec('string'), null);
+            if (audioTextAnnos) audioTextAnnos.push('');
+            else audioTextAnnos = dataDoc[field + '_audioAnnotations_text'] = new List<string>(['']);
+            DictationManager.Controls.listen({
+                interimHandler: value => { audioTextAnnos[audioTextAnnos.length - 1] = value; }, // prettier-ignore
+                continuous: { indefinite: false },
+            }).then(results => {
+                if (results && [DictationManager.Controls.Infringed].includes(results)) {
+                    DictationManager.Controls.stop();
+                }
+                onEnd?.();
+            });
+
+            gumStream = stream;
+            recorder = new MediaRecorder(stream);
+            recorder.ondataavailable = async (e: any) => {
+                const [{ result }] = await Networking.UploadFilesToServer({ file: e.data });
+                if (!(result instanceof Error)) {
+                    const audioField = new AudioField(result.accessPaths.agnostic.client);
+                    const audioAnnos = Cast(dataDoc[field + '_audioAnnotations'], listSpec(AudioField), null);
+                    if (audioAnnos) audioAnnos.push(audioField);
+                    else dataDoc[field + '_audioAnnotations'] = new List([audioField]);
+                }
+            };
+            recorder.start();
+            const stopFunc = () => {
+                recorder.stop();
+                DictationManager.Controls.stop(/* false */);
+                dataDoc.audioAnnoState = AudioAnnoState.stopped;
+                gumStream.getAudioTracks()[0].stop();
+            };
+            if (onRecording) onRecording(stopFunc);
+            else setTimeout(stopFunc, 5000);
+        });
+    }
 }
diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts
index 8f0c2b0bf..5bcac7330 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -1,23 +1,17 @@
 import { Howl } from 'howler';
 import { action, computed, makeObservable, observable, ObservableSet, observe } from 'mobx';
 import { Doc, Opt } from '../../fields/Doc';
-import { AclAdmin, AclEdit, Animation, DocData } from '../../fields/DocSymbols';
+import { Animation, DocData } from '../../fields/DocSymbols';
 import { Id } from '../../fields/FieldSymbols';
 import { listSpec } from '../../fields/Schema';
 import { Cast, DocCast, NumCast, StrCast } from '../../fields/Types';
 import { AudioField } from '../../fields/URLField';
-import { GetEffectiveAcl } from '../../fields/util';
 import { CollectionViewType } from '../documents/DocumentTypes';
-import { TabDocView } from '../views/collections/TabDocView';
 import { LightboxView } from '../views/LightboxView';
 import { DocumentView, DocumentViewInternal } from '../views/nodes/DocumentView';
 import { FocusViewOptions } from '../views/nodes/FocusViewOptions';
-import { KeyValueBox } from '../views/nodes/KeyValueBox';
-import { LinkAnchorBox } from '../views/nodes/LinkAnchorBox';
 import { OpenWhere } from '../views/nodes/OpenWhere';
 import { PresBox } from '../views/nodes/trails';
-import { ScriptingGlobals } from './ScriptingGlobals';
-import { SelectionManager } from './SelectionManager';
 
 type childIterator = { viewSpec: Opt<Doc>; childDocView: Opt<DocumentView>; focused: boolean; contextPath: Doc[] };
 export class DocumentManager {
@@ -29,10 +23,9 @@ export class DocumentManager {
     }
 
     // global holds all of the nodes (regardless of which collection they're in)
-    @observable _documentViews = new Set<DocumentView>();
-    @observable.shallow public CurrentlyLoading: Doc[] = [];
+    @observable private _documentViews = new Set<DocumentView>();
     @computed public get DocumentViews() {
-        return Array.from(this._documentViews).filter(view => !(view.ComponentView instanceof KeyValueBox) && (!LightboxView.LightboxDoc || LightboxView.Contains(view)));
+        return Array.from(this._documentViews).filter(view => (!view.ComponentView?.dontRegisterView?.() && !LightboxView.LightboxDoc) || LightboxView.Contains(view));
     }
     public AddDocumentView(dv: DocumentView) {
         this._documentViews.add(dv);
@@ -44,7 +37,19 @@ export class DocumentManager {
     // private constructor so no other class can create a nodemanager
     private constructor() {
         makeObservable(this);
-        observe(this.CurrentlyLoading, change => {
+
+        DocumentView.allViews = () => this.DocumentViews;
+        DocumentView.addView = this.AddView;
+        DocumentView.removeView = this.RemoveView;
+        DocumentView.showDocument = this.showDocument;
+        DocumentView.showDocumentView = this.showDocumentView;
+        DocumentView.linkCommonAncestor = DocumentManager.LinkCommonAncestor;
+        DocumentView.addViewRenderedCb = this.AddViewRenderedCb;
+        DocumentView.getFirstDocumentView = this.getFirstDocumentView;
+        DocumentView.getDocumentView = this.getDocumentView;
+        DocumentView.getContextPath = DocumentManager.GetContextPath;
+        DocumentView.getLightboxDocumentView = this.getLightboxDocumentView;
+        observe(Doc.CurrentlyLoading, change => {
             // watch CurrentlyLoading-- when something is loaded, it's removed from the list and we have to update its icon if it were iconified since LoadingBox icons are different than the media they become
             switch (change.type as any) {
                 case 'update':
@@ -92,17 +97,12 @@ export class DocumentManager {
 
     @action
     public AddView = (view: DocumentView) => {
-        if (!view._props.LayoutTemplateString?.includes(KeyValueBox.name) &&
-            !view._props.LayoutTemplateString?.includes(LinkAnchorBox.name)) {      
-            this.AddDocumentView(view);
-            this.callAddViewFuncs(view);
-        } // prettier-ignore
+        this.AddDocumentView(view);
+        this.callAddViewFuncs(view);
     };
     public RemoveView = action((view: DocumentView) => {
-        if (!view._props.LayoutTemplateString?.includes(KeyValueBox.name) && !view._props.LayoutTemplateString?.includes(LinkAnchorBox.name)) {
-            this.DeleteDocumentView(view);
-        }
-        SelectionManager.DeselectView(view);
+        this.DeleteDocumentView(view);
+        DocumentView.DeselectView(view);
     });
 
     // gets all views
@@ -203,6 +203,11 @@ export class DocumentManager {
     }
     static _overlayViews = new ObservableSet<DocumentView>();
 
+    /**
+     * Find the nearest common ancestor collection that contains a link's source and target
+     * @param linkDoc
+     * @returns common ancestor DocumentView
+     */
     public static LinkCommonAncestor(linkDoc: Doc) {
         const getAnchor = (which: number) => {
             const anch = DocCast(linkDoc['link_anchor_' + which]);
@@ -245,11 +250,8 @@ export class DocumentManager {
         Doc.RemoveDocFromList(Doc.MyRecentlyClosed, undefined, targetDoc);
         const docContextPath = DocumentManager.GetContextPath(targetDoc, true);
         if (docContextPath.some(doc => doc.hidden)) options.toggleTarget = false;
-        const tabView = Array.from(TabDocView._allTabs).find(view => view._document === docContextPath[0]);
-        if (!tabView?._activated && tabView?._document) {
-            options.toggleTarget = false;
-            TabDocView.Activate(tabView?._document);
-        }
+        if (DocumentView.activateTabView(docContextPath[0])) options.toggleTarget = false;
+
         const rootContextView =
             docContextPath.length &&
             (await new Promise<DocumentView>(res => {
@@ -260,7 +262,7 @@ export class DocumentManager {
                     return;
                 }
                 options.didMove = true;
-                (!LightboxView.LightboxDoc && docContextPath.some(doc => TabDocView.Activate(doc))) || DocumentViewInternal.addDocTabFunc(docContextPath[0], options.openLocation ?? OpenWhere.addRight);
+                (!LightboxView.LightboxDoc && docContextPath.some(doc => DocumentView.activateTabView(doc))) || DocumentViewInternal.addDocTabFunc(docContextPath[0], options.openLocation ?? OpenWhere.addRight);
                 this.AddViewRenderedCb(docContextPath[0], dv => res(dv));
             }));
         if (options.openLocation === OpenWhere.lightbox) {
@@ -355,33 +357,4 @@ export class DocumentManager {
         }
     }
 }
-// eslint-disable-next-line default-param-last
-export function DocFocusOrOpen(docIn: Doc, optionsIn: FocusViewOptions = { willZoomCentered: true, zoomScale: 0, openLocation: OpenWhere.toggleRight }, containingDoc?: Doc) {
-    let doc = docIn;
-    const options = optionsIn;
-    const func = () => {
-        const cv = DocumentManager.Instance.getDocumentView(containingDoc);
-        const dv = DocumentManager.Instance.getDocumentView(doc, cv);
-        if (dv && (!containingDoc || dv.containerViewPath?.().lastElement()?.Document === containingDoc)) {
-            DocumentManager.Instance.showDocumentView(dv, options).then(() => dv && Doc.linkFollowHighlight(dv.Document));
-        } else {
-            const container = DocCast(containingDoc ?? doc.embedContainer ?? Doc.BestEmbedding(doc));
-            const showDoc = !Doc.IsSystem(container) && !cv ? container : doc;
-            options.toggleTarget = undefined;
-            DocumentManager.Instance.showDocument(showDoc, options, () => DocumentManager.Instance.showDocument(doc, { ...options, openLocation: undefined })).then(() => {
-                const cvFound = DocumentManager.Instance.getDocumentView(containingDoc);
-                const dvFound = DocumentManager.Instance.getDocumentView(doc, cvFound);
-                dvFound && Doc.linkFollowHighlight(dvFound.Document);
-            });
-        }
-    };
-    if (Doc.IsDataProto(doc) && Doc.GetEmbeddings(doc).some(embed => embed.hidden && [AclAdmin, AclEdit].includes(GetEffectiveAcl(embed)))) {
-        doc = Doc.GetEmbeddings(doc).find(embed => embed.hidden && [AclAdmin, AclEdit].includes(GetEffectiveAcl(embed)))!;
-    }
-    if (doc.hidden) {
-        doc.hidden = false;
-        options.toggleTarget = false;
-        setTimeout(func);
-    } else func();
-}
-ScriptingGlobals.add(DocFocusOrOpen);
+setTimeout(() => DocumentManager.Instance);
diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts
index 34f54be2e..fda505420 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -27,8 +27,6 @@ import { ScriptCast } from '../../fields/Types';
 import { Docs } from '../documents/Documents';
 import { DocumentView } from '../views/nodes/DocumentView';
 import { dropActionType } from './DropActionTypes';
-import { ScriptingGlobals } from './ScriptingGlobals';
-import { SelectionManager } from './SelectionManager';
 import { SnappingManager } from './SnappingManager';
 import { UndoManager } from './UndoManager';
 
@@ -648,14 +646,3 @@ export namespace DragManager {
         document.addEventListener('pointerup', upHandler, true);
     }
 }
-
-// eslint-disable-next-line prefer-arrow-callback
-ScriptingGlobals.add(function toggleRaiseOnDrag(readOnly?: boolean) {
-    if (readOnly) {
-        return SelectionManager.Views.some(dv => dv.Document.keepZWhenDragged);
-    }
-    SelectionManager.Views.forEach(dv => {
-        dv.Document.keepZWhenDragged = !dv.Document.keepZWhenDragged;
-    });
-    return undefined;
-});
diff --git a/src/client/util/GroupManager.tsx b/src/client/util/GroupManager.tsx
index e1a503ac9..5701a22c0 100644
--- a/src/client/util/GroupManager.tsx
+++ b/src/client/util/GroupManager.tsx
@@ -77,7 +77,7 @@ export class GroupManager extends ObservableReactComponent<{}> {
      */
     @action
     open = () => {
-        // SelectionManager.DeselectAll();
+        // DocumentView.DeselectAll();
         this.isOpen = true;
         this.populateUsers();
     };
diff --git a/src/client/util/HypothesisUtils.ts b/src/client/util/HypothesisUtils.ts
index dd18b2533..5a6522dc8 100644
--- a/src/client/util/HypothesisUtils.ts
+++ b/src/client/util/HypothesisUtils.ts
@@ -6,8 +6,6 @@ import { WebField } from '../../fields/URLField';
 import { Docs } from '../documents/Documents';
 import { DocumentLinksButton } from '../views/nodes/DocumentLinksButton';
 import { DocumentView } from '../views/nodes/DocumentView';
-import { DocumentManager } from './DocumentManager';
-import { SelectionManager } from './SelectionManager';
 
 export namespace Hypothesis {
     /**
@@ -25,7 +23,7 @@ export namespace Hypothesis {
      * Search for a WebDocument whose url field matches the given uri, return undefined if not found
      */
     export const findWebDoc = async (uri: string) => {
-        const currentDoc = SelectionManager.Docs.lastElement();
+        const currentDoc = DocumentView.Selected().lastElement()?.Document;
         if (currentDoc && Cast(currentDoc.data, WebField)?.url.href === uri) return currentDoc; // always check first whether the currently selected doc is the annotation's source, only use Search otherwise
 
         const results: Doc[] = [];
@@ -39,7 +37,7 @@ export namespace Hypothesis {
         //     })
         // );
 
-        const onScreenResults = results.filter(doc => DocumentManager.Instance.getFirstDocumentView(doc));
+        const onScreenResults = results.filter(doc => DocumentView.getFirstDocumentView(doc));
         return onScreenResults.length ? onScreenResults[0] : results.length ? results[0] : undefined; // prioritize results that are currently on the screen
     };
 
@@ -65,7 +63,7 @@ export namespace Hypothesis {
                 DocumentLinksButton.AnnotationId = annotationId;
                 DocumentLinksButton.AnnotationUri = annotationUri;
             });
-            const endLinkView = DocumentManager.Instance.getFirstDocumentView(sourceDoc);
+            const endLinkView = DocumentView.getFirstDocumentView(sourceDoc);
             const rect = document.body.getBoundingClientRect();
             const x = rect.x + rect.width / 2;
             const y = 250;
@@ -182,7 +180,7 @@ export namespace Hypothesis {
                     bubbles: true,
                 })
             );
-            const targetView: Opt<DocumentView> = DocumentManager.Instance.getFirstDocumentView(target);
+            const targetView: Opt<DocumentView> = DocumentView.getFirstDocumentView(target);
             const position = targetView?.screenToViewTransform().inverse().transformPoint(0, 0);
             targetView && position && simulateMouseClick(targetView.ContentDiv!, position[0], position[1], position[0], position[1], false);
         }, 300);
diff --git a/src/client/util/Import & Export/DirectoryImportBox.tsx b/src/client/util/Import & Export/DirectoryImportBox.tsx
index 398ba3c04..baa0786b7 100644
--- a/src/client/util/Import & Export/DirectoryImportBox.tsx	
+++ b/src/client/util/Import & Export/DirectoryImportBox.tsx	
@@ -17,7 +17,6 @@
 // import { DocumentType } from '../../documents/DocumentTypes';
 // import { Networking } from '../../Network';
 // import { FieldView, FieldViewProps } from '../../views/nodes/FieldView';
-// import { DocumentManager } from '../DocumentManager';
 // import './DirectoryImportBox.scss';
 // import ImportMetadataEntry, { keyPlaceholder, valuePlaceholder } from './ImportMetadataEntry';
 // import * as React from 'react';
diff --git a/src/client/util/LinkFollower.ts b/src/client/util/LinkFollower.ts
index a6f6122ab..9a0edcfec 100644
--- a/src/client/util/LinkFollower.ts
+++ b/src/client/util/LinkFollower.ts
@@ -1,14 +1,12 @@
 import { action, runInAction } from 'mobx';
-import { Doc, DocListCast, FieldResult, FieldType, Opt } from '../../fields/Doc';
-import { BoolCast, Cast, DocCast, NumCast, ScriptCast, StrCast } from '../../fields/Types';
+import { Doc, DocListCast, Opt } from '../../fields/Doc';
+import { BoolCast, Cast, DocCast, NumCast, StrCast } from '../../fields/Types';
 import { DocumentType } from '../documents/DocumentTypes';
+import { DocumentView } from '../views/nodes/DocumentView';
 import { FocusViewOptions } from '../views/nodes/FocusViewOptions';
 import { OpenWhere } from '../views/nodes/OpenWhere';
 import { PresBox } from '../views/nodes/trails';
-import { DocumentManager } from './DocumentManager';
-import { LinkManager } from './LinkManager';
 import { ScriptingGlobals } from './ScriptingGlobals';
-import { SelectionManager } from './SelectionManager';
 import { SnappingManager } from './SnappingManager';
 import { UndoManager } from './UndoManager';
 /*
@@ -24,6 +22,9 @@ import { UndoManager } from './UndoManager';
  * - user defined kvps
  */
 export class LinkFollower {
+    public static Init() {
+        DocumentView.FollowLink = LinkFollower.FollowLink;
+    }
     // follows a link - if the target is on screen, it highlights/pans to it.
     // if the target isn't onscreen, then it will open up the target in the lightbox, or in place
     // depending on the followLinkLocation property of the source (or the link itself as a fallback);
@@ -42,9 +43,9 @@ export class LinkFollower {
     };
 
     public static traverseLink(link: Opt<Doc>, sourceDoc: Doc, finished?: () => void, traverseBacklink?: boolean) {
-        const getView = (doc: Doc) => DocumentManager.Instance.getFirstDocumentView(DocCast(doc.layout_unrendered ? doc.annotationOn : doc));
+        const getView = (doc: Doc) => DocumentView.getFirstDocumentView(DocCast(doc.layout_unrendered ? doc.annotationOn : doc));
         const isAnchor = (source: Doc, anchor: Doc) => Doc.AreProtosEqual(anchor, source) || Doc.AreProtosEqual(anchor.annotationOn as Doc, source);
-        const linkDocs = link ? [link] : LinkManager.Links(sourceDoc);
+        const linkDocs = link ? [link] : Doc.Links(sourceDoc);
         const fwdLinks = linkDocs.filter(l => isAnchor(sourceDoc, l.link_anchor_1 as Doc)); // link docs where 'sourceDoc' is link_anchor_1
         const backLinks = linkDocs.filter(l => isAnchor(sourceDoc, l.link_anchor_2 as Doc)); // link docs where 'sourceDoc' is link_anchor_2
         const fwdLinkWithoutTargetView = fwdLinks.find(l => !getView(DocCast(l.link_anchor_2)));
@@ -68,7 +69,7 @@ export class LinkFollower {
                         ? linkDoc.link_anchor_2
                         : linkDoc.link_anchor_1
             ) as Doc;
-            const srcAnchor: Doc = LinkManager.getOppositeAnchor(linkDoc, target) ?? sourceDoc;
+            const srcAnchor: Doc = Doc.getOppositeAnchor(linkDoc, target) ?? sourceDoc;
             if (target) {
                 const doFollow = (canToggle?: boolean) => {
                     const toggleTarget = canToggle && BoolCast(sourceDoc.followLinkToggle);
@@ -87,14 +88,14 @@ export class LinkFollower {
                         zoomTextSelections: BoolCast(srcAnchor.followLinkZoomText),
                     };
                     if (target.type === DocumentType.PRES) {
-                        const containerDocContext = DocumentManager.GetContextPath(sourceDoc, true); // gather all views that affect layout of sourceDoc so we can revert them after playing the rail
-                        SelectionManager.DeselectAll();
-                        if (!DocumentManager.Instance.AddViewRenderedCb(target, dv => containerDocContext.length && dv.ComponentView?.playTrail?.(containerDocContext))) {
+                        const containerDocContext = DocumentView.getContextPath(sourceDoc, true); // gather all views that affect layout of sourceDoc so we can revert them after playing the rail
+                        DocumentView.DeselectAll();
+                        if (!DocumentView.addViewRenderedCb(target, dv => containerDocContext.length && dv.ComponentView?.playTrail?.(containerDocContext))) {
                             PresBox.OpenPresMinimized(target, [0, 0]);
                         }
                         finished?.();
                     } else {
-                        DocumentManager.Instance.showDocument(target, options, allFinished);
+                        DocumentView.showDocument(target, options, allFinished);
                     }
                 };
                 let movedTarget = false;
@@ -133,10 +134,6 @@ export class LinkFollower {
 
 // eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function followLink(doc: Doc, altKey: boolean) {
-    SelectionManager.DeselectAll();
+    DocumentView.DeselectAll();
     return LinkFollower.FollowLink(undefined, doc, altKey) ? undefined : { select: true };
 });
-
-export function IsFollowLinkScript(field: FieldResult<FieldType>) {
-    return ScriptCast(field)?.script.originalScript.includes('return followLink(');
-}
diff --git a/src/client/util/LinkManager.ts b/src/client/util/LinkManager.ts
index dbd13f978..56d5dce4e 100644
--- a/src/client/util/LinkManager.ts
+++ b/src/client/util/LinkManager.ts
@@ -30,8 +30,8 @@ export class LinkManager {
     @observable.shallow userLinkDBs: Doc[] = [];
     @observable public currentLink: Opt<Doc> = undefined;
     @observable public currentLinkAnchor: Opt<Doc> = undefined;
-    public static get Instance() {
-        return LinkManager._instance;
+    public static get Instance(): LinkManager {
+        return Doc.UserDoc() ? LinkManager._instance ?? new LinkManager() : (undefined as any as LinkManager);
     }
 
     public static Links(doc: Doc | undefined) {
@@ -47,10 +47,13 @@ export class LinkManager {
         this.userLinkDBs.push(linkDb);
     };
     public static AutoKeywords = 'keywords:Usages';
-    constructor() {
+    private constructor() {
         makeObservable(this);
         LinkManager._instance = this;
         Doc.AddLink = this.addLink;
+        Doc.DeleteLink = this.deleteLink;
+        Doc.Links = LinkManager.Links;
+        Doc.getOppositeAnchor = LinkManager.getOppositeAnchor;
         this.createlink_relationshipLists();
         // since this is an action, not a reaction, we get only one shot to add this link to the Anchor docs
         // Thus make sure all promised values are resolved from link -> link.proto -> link.link_anchor_[1,2] -> link.link_anchor_[1,2].proto
diff --git a/src/client/util/PingManager.ts b/src/client/util/PingManager.ts
index e5e69c5ac..255e9cee0 100644
--- a/src/client/util/PingManager.ts
+++ b/src/client/util/PingManager.ts
@@ -1,6 +1,6 @@
 import { action, makeObservable, observable, runInAction } from 'mobx';
 import { Networking } from '../Network';
-import { CurrentUserUtils } from './CurrentUserUtils';
+import { SnappingManager } from './SnappingManager';
 
 export class PingManager {
     // create static instance and getter for global use
@@ -32,7 +32,7 @@ export class PingManager {
         try {
             const res = await Networking.PostToServer('/ping', { date: new Date() });
             runInAction(() => {
-                CurrentUserUtils.ServerVersion = res.message;
+                SnappingManager.SetServerVersion(res.message);
             });
             !this.IsBeating && this.setIsBeating(true);
         } catch {
diff --git a/src/client/util/ReplayMovements.ts b/src/client/util/ReplayMovements.ts
index dcc6aabab..530fcf211 100644
--- a/src/client/util/ReplayMovements.ts
+++ b/src/client/util/ReplayMovements.ts
@@ -4,8 +4,8 @@ import { CollectionDockingView } from '../views/collections/CollectionDockingVie
 import { CollectionFreeFormView } from '../views/collections/collectionFreeForm';
 import { OpenWhereMod } from '../views/nodes/OpenWhere';
 import { VideoBox } from '../views/nodes/VideoBox';
-import { DocumentManager } from './DocumentManager';
 import { Movement, Presentation } from './TrackMovements';
+import { DocumentView } from '../views/nodes/DocumentView';
 
 export class ReplayMovements {
     private timers: NodeJS.Timeout[] | null;
@@ -106,7 +106,7 @@ export class ReplayMovements {
 
     // returns undefined if the docView isn't open on the screen
     getCollectionFFView = (doc: Doc) => {
-        const isInView = DocumentManager.Instance.getDocumentView(doc);
+        const isInView = DocumentView.getDocumentView(doc);
         return isInView?.ComponentView as CollectionFreeFormView;
     };
 
@@ -118,7 +118,7 @@ export class ReplayMovements {
         }
         // console.log('openTab', docId, doc);
         CollectionDockingView.AddSplit(doc, OpenWhereMod.right);
-        const docView = DocumentManager.Instance.getDocumentView(doc);
+        const docView = DocumentView.getDocumentView(doc);
         // BUG - this returns undefined if the doc is already open
         return docView?.ComponentView as CollectionFreeFormView;
     };
diff --git a/src/client/util/Scripting.ts b/src/client/util/Scripting.ts
index 8df579e75..6948469cc 100644
--- a/src/client/util/Scripting.ts
+++ b/src/client/util/Scripting.ts
@@ -5,8 +5,8 @@
 // import * as typescriptlib from '!!raw-loader!../../../node_modules/typescript/lib/lib.d.ts'
 // import * as typescriptes5 from '!!raw-loader!../../../node_modules/typescript/lib/lib.es5.d.ts'
 // eslint-disable-next-line node/no-unpublished-import
-import * as ts from 'typescript';
 import * as typescriptlib from '!!raw-loader!./type_decls.d';
+import * as ts from 'typescript';
 import { Doc, FieldType } from '../../fields/Doc';
 import { RefField } from '../../fields/RefField';
 import { ScriptField } from '../../fields/ScriptField';
@@ -195,7 +195,6 @@ export function CompileScript(script: string, options: ScriptOptions = {}): Comp
     if (found) return found as CompiledScript;
     const { requiredType = '', addReturn = false, params = {}, capturedVariables = {}, typecheck = true } = options;
     if (options.params && !options.params.this) options.params.this = Doc.name;
-    if (options.params && !options.params.self) options.params.self = Doc.name;
     if (options.globals) {
         ScriptingGlobals.setScriptingGlobals(options.globals);
     }
@@ -266,9 +265,3 @@ export function CompileScript(script: string, options: ScriptOptions = {}): Comp
     !signature.includes('XXX') && ScriptField._scriptFieldCache.set(script + ':' + signature, result as CompiledScript);
     return result;
 }
-
-ScriptingGlobals.add(CompileScript);
-// eslint-disable-next-line prefer-arrow-callback
-ScriptingGlobals.add(function runScript(doc: Doc, script: ScriptField) {
-    return script?.script.run({ this: doc }).result;
-});
diff --git a/src/client/util/SelectionManager.ts b/src/client/util/SelectionManager.ts
index 7e8f42de9..0b942116c 100644
--- a/src/client/util/SelectionManager.ts
+++ b/src/client/util/SelectionManager.ts
@@ -1,6 +1,5 @@
 import { action, makeObservable, observable, runInAction } from 'mobx';
 import { Doc, Opt } from '../../fields/Doc';
-import { DocViews } from '../../fields/DocSymbols';
 import { List } from '../../fields/List';
 import { listSpec } from '../../fields/Schema';
 import { Cast, DocCast } from '../../fields/Types';
@@ -24,6 +23,13 @@ export class SelectionManager {
     private constructor() {
         SelectionManager._manager = this;
         makeObservable(this);
+        DocumentView.DeselectAll = SelectionManager.DeselectAll;
+        DocumentView.DeselectView = SelectionManager.DeselectView;
+        DocumentView.SelectView = SelectionManager.SelectView;
+        DocumentView.SelectedDocs = SelectionManager.Docs;
+        DocumentView.Selected = SelectionManager.Views;
+        DocumentView.SelectSchemaDoc = SelectionManager.SelectSchemaViewDoc;
+        DocumentView.SelectedSchemaDoc = () => this.SelectedSchemaDocument;
     }
 
     @action
@@ -54,8 +60,10 @@ export class SelectionManager {
     public static DeselectAll = (except?: Doc): void => {
         const found = this.Instance.SelectedViews.find(dv => dv.Document === except);
         runInAction(() => {
-            LinkManager.Instance.currentLink = undefined;
-            LinkManager.Instance.currentLinkAnchor = undefined;
+            if (LinkManager.Instance) {
+                LinkManager.Instance.currentLink = undefined;
+                LinkManager.Instance.currentLinkAnchor = undefined;
+            }
             this.Instance.SelectedSchemaDocument = undefined;
         });
         this.Instance.SelectedViews.forEach(dv => {
@@ -68,19 +76,18 @@ export class SelectionManager {
         if (found) this.SelectView(found, false);
     };
 
-    public static IsSelected = (doc?: Doc) => Array.from(doc?.[DocViews] ?? []).some(dv => dv?.IsSelected);
-    public static get Views() { return this.Instance.SelectedViews; } // prettier-ignore
-    public static get SelectedSchemaDoc() { return this.Instance.SelectedSchemaDocument; } // prettier-ignore
-    public static get Docs() { return this.Instance.SelectedViews.map(dv => dv.Document).filter(doc => doc?._type_collection !== CollectionViewType.Docking); } // prettier-ignore
+    public static Views() { return SelectionManager.Instance.SelectedViews; } // prettier-ignore
+    public static get SelectedSchemaDoc() { return SelectionManager.Instance.SelectedSchemaDocument; } // prettier-ignore
+    public static Docs() { return SelectionManager.Instance.SelectedViews.map(dv => dv.Document).filter(doc => doc?._type_collection !== CollectionViewType.Docking); } // prettier-ignore
 }
 
 // eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function SelectedDocType(type: string, expertMode: boolean, checkContext?: boolean) {
     if (Doc.noviceMode && expertMode) return false;
     if (type === 'tab') {
-        return SelectionManager.Views.lastElement()?._props.renderDepth === 0;
+        return DocumentView.Selected().lastElement()?._props.renderDepth === 0;
     }
-    const selected = (sel => (checkContext ? DocCast(sel?.embedContainer) : sel))(SelectionManager.SelectedSchemaDoc ?? SelectionManager.Docs.lastElement());
+    const selected = (sel => (checkContext ? DocCast(sel?.embedContainer) : sel))(DocumentView.SelectedSchemaDoc() ?? SelectionManager.Docs().lastElement());
     return selected?.type === type || selected?.type_collection === type || !type;
 });
 // eslint-disable-next-line prefer-arrow-callback
@@ -109,8 +116,6 @@ ScriptingGlobals.add(function redo() {
 });
 // eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function selectedDocs(container: Doc, excludeCollections: boolean, prevValue: any) {
-    const docs = SelectionManager.Views.map(dv => dv.Document).filter(
-        d => !Doc.AreProtosEqual(d, container) && !d.annotationOn && d.type !== DocumentType.KVP && (!excludeCollections || d.type !== DocumentType.COL || !Cast(d.data, listSpec(Doc), null))
-    );
+    const docs = SelectionManager.Docs().filter(d => !Doc.AreProtosEqual(d, container) && !d.annotationOn && d.type !== DocumentType.KVP && (!excludeCollections || d.type !== DocumentType.COL || !Cast(d.data, listSpec(Doc), null)));
     return docs.length ? new List(docs) : prevValue;
 });
diff --git a/src/client/util/SettingsManager.tsx b/src/client/util/SettingsManager.tsx
index 8cb97fe50..d3c10f9f4 100644
--- a/src/client/util/SettingsManager.tsx
+++ b/src/client/util/SettingsManager.tsx
@@ -8,7 +8,7 @@ import * as React from 'react';
 import { BsGoogle } from 'react-icons/bs';
 import { FaFillDrip, FaPalette } from 'react-icons/fa';
 import { ClientUtils, addStyleSheet, addStyleSheetRule } from '../../ClientUtils';
-import { Doc, Opt } from '../../fields/Doc';
+import { Doc } from '../../fields/Doc';
 import { DashVersion } from '../../fields/DocSymbols';
 import { BoolCast, Cast, NumCast, StrCast } from '../../fields/Types';
 import { DocServer } from '../DocServer';
@@ -17,7 +17,7 @@ import { GoogleAuthenticationManager } from '../apis/GoogleAuthenticationManager
 import { MainViewModal } from '../views/MainViewModal';
 import { GroupManager } from './GroupManager';
 import './SettingsManager.scss';
-import { SnappingManager } from './SnappingManager';
+import { SnappingManager, freeformScrollMode } from './SnappingManager';
 import { undoable } from './UndoManager';
 
 export enum ColorScheme {
@@ -28,11 +28,6 @@ export enum ColorScheme {
     Cupcake = 'Cupcake',
 }
 
-export enum freeformScrollMode {
-    Pan = 'pan',
-    Zoom = 'zoom',
-}
-
 @observer
 export class SettingsManager extends React.Component<{}> {
     // eslint-disable-next-line no-use-before-define
@@ -44,17 +39,15 @@ export class SettingsManager extends React.Component<{}> {
     @observable private _curr_password = '';
     @observable private _new_password = '';
     @observable private _new_confirm = '';
-    @observable private _lastPressedSidebarBtn: Opt<Doc> = undefined; // bcz: this is a hack to handle highlighting buttons in the leftpanel menu .. need to find a cleaner approach
     @observable private _activeTab = 'Accounts';
     @observable private _isOpen = false;
 
-    @observable public propertiesWidth: number = 0;
-
     private googleAuthorize = action(() => GoogleAuthenticationManager.Instance.fetchOrGenerateAccessToken(true));
 
     public closeMgr = action(() => {
         this._isOpen = false;
     });
+    // eslint-disable-next-line react/no-unused-class-component-methods
     public openMgr = action(() => {
         this._isOpen = true;
     });
@@ -154,9 +147,6 @@ export class SettingsManager extends React.Component<{}> {
         SnappingManager.SettingsStyle = SettingsManager._settingsStyle;
     }
 
-    public get LastPressedBtn() { return this._lastPressedSidebarBtn; } // prettier-ignore
-    public set LastPressedBtn(state:Doc|undefined) { this._lastPressedSidebarBtn = state; } // prettier-ignore
-
     @computed public static get userColor() {
         return StrCast(Doc.UserDoc().userColor);
     }
@@ -287,7 +277,7 @@ export class SettingsManager extends React.Component<{}> {
                     size={Size.XSMALL}
                     color={SettingsManager.userColor}
                 />
-                <Toggle
+                {/* <Toggle
                     formLabel="Show Link Lines"
                     formLabelPlacement="right"
                     toggleType={ToggleType.SWITCH}
@@ -297,7 +287,7 @@ export class SettingsManager extends React.Component<{}> {
                     toggleStatus={BoolCast(Doc.UserDoc().showLinkLines)}
                     size={Size.XSMALL}
                     color={SettingsManager.userColor}
-                />
+                /> */}
                 <Group formLabel="Title Height">
                     <NumberDropdown
                         number={NumCast(Doc.UserDoc().headerHeight, 30)}
diff --git a/src/client/util/SharingManager.tsx b/src/client/util/SharingManager.tsx
index b0140b73c..31a99896f 100644
--- a/src/client/util/SharingManager.tsx
+++ b/src/client/util/SharingManager.tsx
@@ -21,11 +21,9 @@ import { DocServer } from '../DocServer';
 import { MainViewModal } from '../views/MainViewModal';
 import { DocumentView } from '../views/nodes/DocumentView';
 import { TaskCompletionBox } from '../views/nodes/TaskCompletedBox';
-import { DocumentManager } from './DocumentManager';
 import { GroupManager, UserOptions } from './GroupManager';
 import { GroupMemberView } from './GroupMemberView';
 import { SearchUtil } from './SearchUtil';
-import { SelectionManager } from './SelectionManager';
 import './SharingManager.scss';
 import { SnappingManager } from './SnappingManager';
 import { undoable } from './UndoManager';
@@ -152,7 +150,7 @@ export class SharingManager extends React.Component<{}> {
         const users = this.individualSort === 'ascending' ? this.users.slice().sort(this.sortUsers) : this.individualSort === 'descending' ? this.users.slice().sort(this.sortUsers).reverse() : this.users;
         const groups = this.groupSort === 'ascending' ? groupList.slice().sort(this.sortGroups) : this.groupSort === 'descending' ? groupList.slice().sort(this.sortGroups).reverse() : groupList;
 
-        let docs = SelectionManager.Views.length < 2 ? [this.targetDoc] : SelectionManager.Views.map(docView => docView.Document);
+        let docs = DocumentView.Selected().length < 2 ? [this.targetDoc] : DocumentView.Selected().map(docView => docView.Document);
 
         if (this.myDocAcls) {
             const newDocs: Doc[] = [];
@@ -442,7 +440,7 @@ export class SharingManager extends React.Component<{}> {
         const { user, sharingDoc } = recipient;
         const target = targetDoc || this.targetDoc!;
         const acl = `acl_${normalizeEmail(user.email)}`;
-        const docs = SelectionManager.Views.length < 2 ? [target] : SelectionManager.Views.map(docView => docView.Document);
+        const docs = DocumentView.Selected().length < 2 ? [target] : DocumentView.Selected().map(docView => docView.Document);
         docs.map(doc => (this.layoutDocAcls || doc.dockingConfig ? doc : Doc.GetProto(doc))).forEach(doc => {
             distributeAcls(acl, permission as SharingPermissions, doc, undefined, this.upgradeNested ? true : undefined);
             if (permission !== SharingPermissions.None) {
@@ -460,7 +458,7 @@ export class SharingManager extends React.Component<{}> {
         const target = targetDoc || this.targetDoc!;
         const acl = `acl_${normalizeEmail(StrCast(group.title))}`;
 
-        const docs = SelectionManager.Views.length < 2 ? [target] : SelectionManager.Views.map(docView => docView.Document);
+        const docs = DocumentView.Selected().length < 2 ? [target] : DocumentView.Selected().map(docView => docView.Document);
         docs.map(doc => (this.layoutDocAcls || doc.dockingConfig ? doc : Doc.GetProto(doc))).forEach(doc => {
             distributeAcls(acl, permission as SharingPermissions, doc, undefined, this.upgradeNested ? true : undefined);
 
@@ -643,14 +641,14 @@ export class SharingManager extends React.Component<{}> {
 
     private focusOn = (contents: string) => {
         const title = this.targetDoc ? StrCast(this.targetDoc.title) : '';
-        const docs = SelectionManager.Views.length > 1 ? SelectionManager.Views.map(docView => docView.props.Document) : [this.targetDoc];
+        const docs = DocumentView.Selected().length > 1 ? DocumentView.Selected().map(docView => docView.props.Document) : [this.targetDoc];
         return (
             <span
                 className="focus-span"
                 title={title}
                 onClick={() => {
                     if (this.targetDoc && this.targetDocView && docs.length === 1) {
-                        DocumentManager.Instance.showDocument(this.targetDoc, { willZoomCentered: true });
+                        DocumentView.showDocument(this.targetDoc, { willZoomCentered: true });
                     }
                 }}
                 onPointerEnter={action(() => {
diff --git a/src/client/util/SnappingManager.ts b/src/client/util/SnappingManager.ts
index 5cd6ecfe1..6789c2ab8 100644
--- a/src/client/util/SnappingManager.ts
+++ b/src/client/util/SnappingManager.ts
@@ -1,5 +1,9 @@
 import { observable, action, runInAction, makeObservable } from 'mobx';
 
+export enum freeformScrollMode {
+    Pan = 'pan',
+    Zoom = 'zoom',
+}
 export class SnappingManager {
     // eslint-disable-next-line no-use-before-define
     private static _manager: SnappingManager;
@@ -19,6 +23,9 @@ export class SnappingManager {
     @observable _vertSnapLines: number[] = [];
     @observable _exploreMode = false;
     @observable _userPanned = false;
+    @observable _serverVersion: string = '';
+    @observable _lastBtnId: string = '';
+    @observable _propertyWid: number = 0;
 
     private constructor() {
         SnappingManager._manager = this;
@@ -45,6 +52,10 @@ export class SnappingManager {
     public static get CanEmbed()       { return this.Instance._canEmbed; } // prettier-ignore
     public static get ExploreMode()    { return this.Instance._exploreMode; } // prettier-ignore
     public static get UserPanned()     { return this.Instance._userPanned; } // prettier-ignore
+    public static get ServerVersion()  { return this.Instance._serverVersion; } // prettier-ignore
+    public static get LastPressedBtn() { return this.Instance._lastBtnId; } // prettier-ignore
+    public static get PropertiesWidth(){ return this.Instance._propertyWid; } // prettier-ignore
+
     public static SetShiftKey       = (down: boolean) => runInAction(() => {this.Instance._shiftKey = down}); // prettier-ignore
     public static SetCtrlKey        = (down: boolean) => runInAction(() => {this.Instance._ctrlKey = down}); // prettier-ignore
     public static SetMetaKey        = (down: boolean) => runInAction(() => {this.Instance._metaKey = down}); // prettier-ignore
@@ -54,7 +65,10 @@ export class SnappingManager {
     public static SetIsResizing     = (docid?:string) => runInAction(() => {this.Instance._isResizing = docid}); // prettier-ignore
     public static SetCanEmbed       = (embed:boolean) => runInAction(() => {this.Instance._canEmbed = embed}); // prettier-ignore
     public static SetExploreMode    = (state:boolean) => runInAction(() => {this.Instance._exploreMode = state}); // prettier-ignore
-    public static TriggerUserPanned = () => runInAction(() => {this.Instance._userPanned = !this.Instance._userPanned}); // prettier-ignore
+    public static TriggerUserPanned = ()              => runInAction(() => {this.Instance._userPanned = !this.Instance._userPanned}); // prettier-ignore
+    public static SetServerVersion  = (version:string) =>runInAction(() => {this.Instance._serverVersion = version}); // prettier-ignore
+    public static SetLastPressedBtn = (id:string)      =>runInAction(() => {this.Instance._lastBtnId = id}); // prettier-ignore
+    public static SetPropertiesWidth= (wid:number)     =>runInAction(() => {this.Instance._propertyWid = wid}); // prettier-ignore
 
     public static userColor: string | undefined;
     public static userVariantColor: string | undefined;
diff --git a/src/client/views/AntimodeMenu.tsx b/src/client/views/AntimodeMenu.tsx
index b1eb730fa..303672d90 100644
--- a/src/client/views/AntimodeMenu.tsx
+++ b/src/client/views/AntimodeMenu.tsx
@@ -1,6 +1,6 @@
 import { action, makeObservable, observable, runInAction } from 'mobx';
 import * as React from 'react';
-import { SettingsManager } from '../util/SettingsManager';
+import { SnappingManager } from '../util/SnappingManager';
 import './AntimodeMenu.scss';
 import { ObservableReactComponent } from './ObservableReactComponent';
 
@@ -157,7 +157,7 @@ export abstract class AntimodeMenu<T extends AntimodeMenuProps> extends Observab
                     left: this._left,
                     top: this._top,
                     opacity: this._opacity,
-                    background: SettingsManager.userBackgroundColor,
+                    background: SnappingManager.userBackgroundColor,
                     transitionProperty: this._transitionProperty,
                     transitionDuration: this._transitionDuration,
                     transitionDelay: this._transitionDelay,
@@ -183,7 +183,7 @@ export abstract class AntimodeMenu<T extends AntimodeMenuProps> extends Observab
                     height: 'inherit',
                     width: 200,
                     opacity: this._opacity,
-                    background: SettingsManager.userBackgroundColor,
+                    background: SnappingManager.userBackgroundColor,
                     transitionProperty: this._transitionProperty,
                     transitionDuration: this._transitionDuration,
                     transitionDelay: this._transitionDelay,
@@ -206,7 +206,7 @@ export abstract class AntimodeMenu<T extends AntimodeMenuProps> extends Observab
                     left: this._left,
                     top: this._top,
                     opacity: this._opacity,
-                    background: SettingsManager.userBackgroundColor,
+                    background: SnappingManager.userBackgroundColor,
                     transitionProperty: this._transitionProperty,
                     transitionDuration: this._transitionDuration,
                     transitionDelay: this._transitionDelay,
diff --git a/src/client/views/ComponentDecorations.tsx b/src/client/views/ComponentDecorations.tsx
index 64b8a8446..929b549e0 100644
--- a/src/client/views/ComponentDecorations.tsx
+++ b/src/client/views/ComponentDecorations.tsx
@@ -1,7 +1,7 @@
 import { observer } from 'mobx-react';
 import * as React from 'react';
-import { SelectionManager } from '../util/SelectionManager';
 import './ComponentDecorations.scss';
+import { DocumentView } from './nodes/DocumentView';
 
 @observer
 export class ComponentDecorations extends React.Component<{ boundsTop: number; boundsLeft: number }, { value: string }> {
@@ -9,7 +9,7 @@ export class ComponentDecorations extends React.Component<{ boundsTop: number; b
     static Instance: ComponentDecorations;
 
     render() {
-        const seldoc = SelectionManager.Views.lastElement();
+        const seldoc = DocumentView.Selected().lastElement();
         return seldoc?.ComponentView?.componentUI?.(this.props.boundsLeft, this.props.boundsTop) ?? null;
     }
 }
diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx
index a0e3d2ddd..97ff346e4 100644
--- a/src/client/views/DocComponent.tsx
+++ b/src/client/views/DocComponent.tsx
@@ -60,6 +60,8 @@ export interface ViewBoxInterface {
     ptFromScreen?: (pt: { X: number; Y: number }) => { X: number; Y: number };
     snapPt?: (pt: { X: number; Y: number }, excludeSegs?: number[]) => { nearestPt: { X: number; Y: number }; distance: number };
     search?: (str: string, bwd?: boolean, clear?: boolean) => boolean;
+    dontRegisterView?: () => boolean; // KeyValueBox's don't want to register their views
+    isUnstyledView?: () => boolean; // SchemaView and KeyValue are untyled -- not tiles, no opacity
 }
 /**
  * DocComponent returns a React base class used by Doc views with accessors for unpacking the Document,layoutDoc, and dataDoc's
diff --git a/src/client/views/DocViewUtils.ts b/src/client/views/DocViewUtils.ts
new file mode 100644
index 000000000..1f5f29c7e
--- /dev/null
+++ b/src/client/views/DocViewUtils.ts
@@ -0,0 +1,21 @@
+/* eslint-disable prefer-destructuring */
+/* eslint-disable default-param-last */
+/* eslint-disable no-use-before-define */
+import { runInAction } from 'mobx';
+import { Doc, SetActiveAudioLinker } from '../../fields/Doc';
+import { DocUtils } from '../documents/DocUtils';
+import { FieldViewProps } from './nodes/FieldView';
+
+export namespace DocViewUtils {
+    export const ActiveRecordings: { props: FieldViewProps; getAnchor: (addAsAnnotation: boolean) => Doc }[] = [];
+
+    export function MakeLinkToActiveAudio(getSourceDoc: () => Doc | undefined, broadcastEvent = true) {
+        broadcastEvent && runInAction(() => { Doc.RecordingEvent += 1; }); // prettier-ignore
+        return ActiveRecordings.map(audio => {
+            const sourceDoc = getSourceDoc();
+            return sourceDoc && DocUtils.MakeLink(sourceDoc, audio.getAnchor(true) || audio.props.Document, { link_relationship: 'recording annotation:linked recording', link_description: 'recording timeline' });
+        });
+    }
+
+    SetActiveAudioLinker(MakeLinkToActiveAudio);
+}
diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx
index dd744f272..487868169 100644
--- a/src/client/views/DocumentButtonBar.tsx
+++ b/src/client/views/DocumentButtonBar.tsx
@@ -14,23 +14,21 @@ import { returnFalse, returnTrue, setupMoveUpEvents, simulateMouseClick } from '
 import { emptyFunction } from '../../Utils';
 import { Doc } from '../../fields/Doc';
 import { Cast, DocCast } from '../../fields/Types';
-import { DocUtils } from '../documents/DocUtils';
+import { DocUtils, IsFollowLinkScript } from '../documents/DocUtils';
 import { CalendarManager } from '../util/CalendarManager';
+import { DictationManager } from '../util/DictationManager';
 import { DragManager } from '../util/DragManager';
 import { dropActionType } from '../util/DropActionTypes';
-import { IsFollowLinkScript } from '../util/LinkFollower';
-import { SelectionManager } from '../util/SelectionManager';
 import { SharingManager } from '../util/SharingManager';
 import { UndoManager, undoBatch } from '../util/UndoManager';
 import './DocumentButtonBar.scss';
 import { ObservableReactComponent } from './ObservableReactComponent';
 import { PinProps } from './PinFuncs';
 import { TemplateMenu } from './TemplateMenu';
-import { TabDocView } from './collections/TabDocView';
 import { Colors } from './global/globalEnums';
 import { LinkPopup } from './linking/LinkPopup';
 import { DocumentLinksButton } from './nodes/DocumentLinksButton';
-import { DocumentView, DocumentViewInternal } from './nodes/DocumentView';
+import { DocumentView } from './nodes/DocumentView';
 import { OpenWhere } from './nodes/OpenWhere';
 import { DashFieldView } from './nodes/formattedText/DashFieldView';
 
@@ -206,7 +204,7 @@ export class DocumentButtonBar extends ObservableReactComponent<{ views: () => (
                                     .views()
                                     .filter(v => v)
                                     .map(dv => dv!.Document);
-                                TabDocView.PinDoc(docs, {
+                                DocumentView.PinDoc(docs, {
                                     pinAudioPlay: true,
                                     pinDocLayout: pinLayoutView,
                                     pinData: { dataview: pinContentView },
@@ -221,7 +219,7 @@ export class DocumentButtonBar extends ObservableReactComponent<{ views: () => (
             );
         };
         return !targetDoc ? null : (
-            <Tooltip title={<div className="dash-tooltip">{`Pin Document ${SelectionManager.Views.length > 1 ? 'multiple documents' : ''} to Trail`}</div>}>
+            <Tooltip title={<div className="dash-tooltip">{`Pin Document ${DocumentView.Selected().length > 1 ? 'multiple documents' : ''} to Trail`}</div>}>
                 <div
                     className="documentButtonBar-icon documentButtonBar-pin"
                     onClick={e => {
@@ -229,7 +227,7 @@ export class DocumentButtonBar extends ObservableReactComponent<{ views: () => (
                             .views()
                             .filter(v => v)
                             .map(dv => dv!.Document);
-                        TabDocView.PinDoc(docs, { pinAudioPlay: true, pinDocLayout: e.shiftKey, pinData: { dataview: e.altKey }, activeFrame: Cast(docs.lastElement()?.activeFrame, 'number', null) });
+                        DocumentView.PinDoc(docs, { pinAudioPlay: true, pinDocLayout: e.shiftKey, pinData: { dataview: e.altKey }, activeFrame: Cast(docs.lastElement()?.activeFrame, 'number', null) });
                         e.stopPropagation();
                     }}>
                     <div className="documentButtonBar-pinTypes">
@@ -299,7 +297,7 @@ export class DocumentButtonBar extends ObservableReactComponent<{ views: () => (
                         this._props.views().map(
                             view =>
                                 view &&
-                                DocumentViewInternal.recordAudioAnnotation(
+                                DictationManager.recordAudioAnnotation(
                                     view.dataDoc,
                                     view.LayoutFieldKey,
                                     stopFunc => {
@@ -381,7 +379,7 @@ export class DocumentButtonBar extends ObservableReactComponent<{ views: () => (
     }
 
     openContextMenu = (e: PointerEvent) => {
-        let child = SelectionManager.Views[0].ContentDiv!.children[0];
+        let child = DocumentView.Selected()[0].ContentDiv!.children[0];
         while (child.children.length) {
             const next = Array.from(child.children).find(c => c.className?.toString().includes('SVGAnimatedString') || typeof c.className === 'string');
             if (next?.className?.toString().includes(DocumentView.ROOT_DIV)) break;
@@ -442,14 +440,7 @@ export class DocumentButtonBar extends ObservableReactComponent<{ views: () => (
                 </div>
                 {this._showLinkPopup ? (
                     <div style={{ position: 'absolute', zIndex: 1000 }}>
-                        <LinkPopup
-                            key="popup"
-                            linkCreated={link => {
-                                link.link_displayLine = !IsFollowLinkScript(this._props.views().lastElement()?.Document.onClick);
-                            }}
-                            linkCreateAnchor={() => this._props.views().lastElement()?.ComponentView?.getAnchor?.(true)}
-                            linkFrom={() => this._props.views().lastElement()?.Document}
-                        />
+                        <LinkPopup key="popup" linkCreated={emptyFunction} linkCreateAnchor={() => this._props.views().lastElement()?.ComponentView?.getAnchor?.(true)} linkFrom={() => this._props.views().lastElement()?.Document} />
                     </div>
                 ) : (
                     <div className="documentButtonBar-button">{this.linkButton}</div>
@@ -457,7 +448,7 @@ export class DocumentButtonBar extends ObservableReactComponent<{ views: () => (
 
                 {DocumentLinksButton.StartLink && DocumentLinksButton.StartLink !== doc ? <div className="documentButtonBar-button">{this.endLinkButton} </div> : null}
                 <div className="documentButtonBar-button">{this.templateButton}</div>
-                {!SelectionManager.Views?.some(v => v.allLinks.length) ? null : <div className="documentButtonBar-button">{this.followLinkButton}</div>}
+                {!DocumentView.Selected().some(v => v.allLinks.length) ? null : <div className="documentButtonBar-button">{this.followLinkButton}</div>}
                 <div className="documentButtonBar-button">{this.pinButton}</div>
                 <div className="documentButtonBar-button">{this.recordButton}</div>
                 <div className="documentButtonBar-button">{this.calendarButton}</div>
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index ef493fb69..432b02782 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -17,9 +17,7 @@ import { BoolCast, Cast, DocCast, NumCast, StrCast } from '../../fields/Types';
 import { GetEffectiveAcl } from '../../fields/util';
 import { DocumentType } from '../documents/DocumentTypes';
 import { Docs } from '../documents/Documents';
-import { DocumentManager } from '../util/DocumentManager';
 import { DragManager } from '../util/DragManager';
-import { SelectionManager } from '../util/SelectionManager';
 import { SettingsManager } from '../util/SettingsManager';
 import { SnappingManager } from '../util/SnappingManager';
 import { UndoManager } from '../util/UndoManager';
@@ -81,14 +79,14 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
                 let inputting = false;
                 if (this._titleControlString.startsWith('$')) {
                     const titleFieldKey = this._titleControlString.substring(1);
-                    if (SelectionManager.Views[0]?.Document[titleFieldKey] !== this._accumulatedTitle) {
+                    if (DocumentView.Selected()[0]?.Document[titleFieldKey] !== this._accumulatedTitle) {
                         inputting = true;
                     }
                 }
                 const center = {x: (this.Bounds.x+this.Bounds.r)/2, y: (this.Bounds.y+this.Bounds.b)/2};
                 const {x,y} = Utils.rotPt(e.clientX - center.x, 
                                           e.clientY - center.y, 
-                                          NumCast(SelectionManager.Views.lastElement()?.screenToViewTransform().Rotate));
+                                          NumCast(DocumentView.Selected().lastElement()?.screenToViewTransform().Rotate));
                 (this._showNothing = !inputting && !DocumentButtonBar.Instance?._tooltipOpen && !(this.Bounds.x !== Number.MAX_VALUE && //
                     (this.Bounds.x > center.x+x || this.Bounds.r < center.x+x || 
                      this.Bounds.y > center.y+y || this.Bounds.b < center.y+y )));
@@ -110,7 +108,7 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
     @computed get Bounds() {
         return (SnappingManager.IsLinkFollowing || SnappingManager.ExploreMode) ? 
             { x: 0, y: 0, r: 0, b: 0 }
-            : SelectionManager.Views
+            : DocumentView.Selected()
                 .filter(dv => dv._props.renderDepth > 0)
                 .map(dv => dv.getBounds)
                 .reduce((bounds, rect) => !rect ? bounds
@@ -127,7 +125,7 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
             this._titleControlString = this._accumulatedTitle;
         } else if (this._titleControlString.startsWith('$')) {
             if (this._accumulatedTitle.startsWith('-->#')) {
-                SelectionManager.Docs.forEach(doc => {
+                DocumentView.SelectedDocs().forEach(doc => {
                     doc[DocData].onViewMounted = ScriptField.MakeScript(`updateTagsCollection(this)`);
                 });
             }
@@ -135,11 +133,11 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
             UndoManager.RunInBatch(
                 () =>
                     titleFieldKey &&
-                    SelectionManager.Views.forEach(d => {
+                    DocumentView.Selected().forEach(dv => {
                         if (titleFieldKey === 'title') {
-                            d.dataDoc.title_custom = !this._accumulatedTitle.startsWith('-');
+                            dv.dataDoc.title_custom = !this._accumulatedTitle.startsWith('-');
                         }
-                        KeyValueBox.SetField(d.Document, titleFieldKey, this._accumulatedTitle);
+                        KeyValueBox.SetField(dv.Document, titleFieldKey, this._accumulatedTitle);
                     }),
                 'edit title'
             );
@@ -154,7 +152,7 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
     };
 
     onContainerDown = (e: React.PointerEvent) => {
-        const effectiveLayoutAcl = GetEffectiveAcl(SelectionManager.Views[0].Document);
+        const effectiveLayoutAcl = GetEffectiveAcl(DocumentView.Selected()[0].Document);
         if (effectiveLayoutAcl === AclAdmin || effectiveLayoutAcl === AclEdit || effectiveLayoutAcl === AclAugment) {
             setupMoveUpEvents(this, e, moveEv => this.onBackgroundMove(true, moveEv), emptyFunction, emptyFunction);
             e.stopPropagation();
@@ -162,7 +160,7 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
     };
 
     onTitleDown = (e: React.PointerEvent) => {
-        const effectiveLayoutAcl = GetEffectiveAcl(SelectionManager.Views[0].Document);
+        const effectiveLayoutAcl = GetEffectiveAcl(DocumentView.SelectedDocs()[0]);
         if (effectiveLayoutAcl === AclAdmin || effectiveLayoutAcl === AclEdit || effectiveLayoutAcl === AclAugment) {
             setupMoveUpEvents(
                 this,
@@ -170,7 +168,7 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
                 moveEv => this.onBackgroundMove(true, moveEv),
                 emptyFunction,
                 action(() => {
-                    const selected = SelectionManager.Views.length === 1 ? SelectionManager.Docs[0] : undefined;
+                    const selected = DocumentView.SelectedDocs().length === 1 ? DocumentView.SelectedDocs()[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);
@@ -186,19 +184,16 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
     };
     @action
     onBackgroundMove = (dragTitle: boolean, e: PointerEvent): boolean => {
-        const dragDocView = SelectionManager.Views[0];
+        const dragDocView = DocumentView.Selected()[0];
         const effectiveLayoutAcl = GetEffectiveAcl(dragDocView.Document);
         if (effectiveLayoutAcl !== AclAdmin && effectiveLayoutAcl !== AclEdit && effectiveLayoutAcl !== AclAugment) {
             return false;
         }
         const containers = new Set<Doc | undefined>();
-        SelectionManager.Views.forEach(v => containers.add(DocCast(v.Document.embedContainer)));
+        DocumentView.Selected().forEach(v => containers.add(DocCast(v.Document.embedContainer)));
         if (containers.size > 1) return false;
         const { left, top } = dragDocView.getBounds || { left: 0, top: 0 };
-        const dragData = new DragManager.DocumentDragData(
-            SelectionManager.Views.map(dv => dv.Document),
-            dragDocView._props.dropAction
-        );
+        const dragData = new DragManager.DocumentDragData(DocumentView.SelectedDocs(), dragDocView._props.dropAction);
         dragData.offset = dragDocView.screenToContentsTransform().transformDirection(e.x - left, e.y - top);
         dragData.moveDocument = dragDocView._props.moveDocument;
         dragData.removeDocument = dragDocView._props.removeDocument;
@@ -206,7 +201,7 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
         dragData.canEmbed = dragTitle;
         this._hidden = true;
         DragManager.StartDocumentDrag(
-            SelectionManager.Views.map(dv => dv.ContentDiv!),
+            DocumentView.Selected().map(dv => dv.ContentDiv!),
             dragData,
             e.x,
             e.y,
@@ -223,7 +218,7 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
     _deleteAfterIconify = false;
     _iconifyBatch: UndoManager.Batch | undefined;
     onCloseClick = (forceDeleteOrIconify: boolean | undefined) => {
-        const views = SelectionManager.Views.filter(v => v && v._props.renderDepth > 0);
+        const views = DocumentView.Selected().filter(v => v && v._props.renderDepth > 0);
         if (forceDeleteOrIconify === false && this._iconifyBatch) return;
         this._deleteAfterIconify = !!(forceDeleteOrIconify || this._iconifyBatch);
         let iconifyingCount = views.length;
@@ -239,7 +234,7 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
                             iconView._props.removeDocument?.(iconView.Document);
                         }
                     });
-                    views.forEach(SelectionManager.DeselectView);
+                    views.forEach(DocumentView.DeselectView);
                 }
                 this._iconifyBatch?.end();
                 this._iconifyBatch = undefined;
@@ -258,53 +253,49 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
     };
 
     onMaximizeDown = (e: React.PointerEvent) => {
-        setupMoveUpEvents(this, e, () => DragManager.StartWindowDrag?.(e, [SelectionManager.Views.lastElement().Document]) ?? false, emptyFunction, this.onMaximizeClick, false, false);
+        setupMoveUpEvents(this, e, () => DragManager.StartWindowDrag?.(e, [DocumentView.SelectedDocs().lastElement()]) ?? false, emptyFunction, this.onMaximizeClick, false, false);
         e.stopPropagation();
     };
     onMaximizeClick = (e: any): void => {
-        const selectedDocs = SelectionManager.Views;
+        const selectedDocs = DocumentView.SelectedDocs();
         if (selectedDocs.length) {
             if (e.ctrlKey) {
                 // open an embedding in a new tab with Ctrl Key
-                CollectionDockingView.AddSplit(Doc.BestEmbedding(selectedDocs[0].Document), OpenWhereMod.right);
+                CollectionDockingView.AddSplit(Doc.BestEmbedding(selectedDocs[0]), OpenWhereMod.right);
             } else if (e.shiftKey) {
                 // open centered in a new workspace with Shift Key
-                const embedding = Doc.MakeEmbedding(selectedDocs[0].Document);
+                const embedding = Doc.MakeEmbedding(selectedDocs[0]);
                 embedding.embedContainer = undefined;
                 embedding.x = -NumCast(embedding._width) / 2;
                 embedding.y = -NumCast(embedding._height) / 2;
                 CollectionDockingView.AddSplit(Docs.Create.FreeformDocument([embedding], { title: 'Tab for ' + embedding.title }), OpenWhereMod.right);
             } else if (e.altKey) {
                 // open same document in new tab
-                CollectionDockingView.ToggleSplit(selectedDocs[0].Document, OpenWhereMod.right);
+                CollectionDockingView.ToggleSplit(selectedDocs[0], OpenWhereMod.right);
             } else {
-                let openDoc = selectedDocs[0].Document;
+                let openDoc = selectedDocs[0];
                 if (openDoc.layout_fieldKey === 'layout_icon') {
                     openDoc = Doc.GetEmbeddings(openDoc).find(embedding => !embedding.embedContainer) ?? Doc.MakeEmbedding(openDoc);
                     Doc.deiconifyView(openDoc);
                 }
-                LightboxView.Instance.SetLightboxDoc(
-                    openDoc,
-                    undefined,
-                    selectedDocs.slice(1).map(view => view.Document)
-                );
+                LightboxView.Instance.SetLightboxDoc(openDoc, undefined, selectedDocs.slice(1));
             }
         }
-        SelectionManager.DeselectAll();
+        DocumentView.DeselectAll();
     };
 
     onIconifyClick = (): void => {
-        SelectionManager.Views.forEach(dv => dv?.iconify());
-        SelectionManager.DeselectAll();
+        DocumentView.Selected().forEach(dv => dv?.iconify());
+        DocumentView.DeselectAll();
     };
 
-    onSelectContainerDocClick = () => SelectionManager.Views?.[0]?.containerViewPath?.().lastElement()?.select(false);
+    onSelectContainerDocClick = () => DocumentView.Selected()?.[0]?.containerViewPath?.().lastElement()?.select(false);
     /**
      * sets up events when user clicks on the border radius editor
      */
     @action
     onRadiusDown = (e: React.PointerEvent): void => {
-        SnappingManager.SetIsResizing(SelectionManager.Docs.lastElement()?.[Id]);
+        SnappingManager.SetIsResizing(DocumentView.SelectedDocs().lastElement()?.[Id]);
         this._isRounding = true;
         this._resizeUndo = UndoManager.StartBatch('DocDecs set radius');
         setupMoveUpEvents(
@@ -314,7 +305,7 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
                 const [x, y] = [this.Bounds.x + 3, this.Bounds.y + 3];
                 const maxDist = Math.min((this.Bounds.r - this.Bounds.x) / 2, (this.Bounds.b - this.Bounds.y) / 2);
                 const dist = moveEv.clientX < x && moveEv.clientY < y ? 0 : Math.sqrt((moveEv.clientX - x) * (moveEv.clientX - x) + (moveEv.clientY - y) * (moveEv.clientY - y));
-                SelectionManager.Docs.forEach(doc => {
+                DocumentView.SelectedDocs().forEach(doc => {
                     const docMax = Math.min(NumCast(doc.width) / 2, NumCast(doc.height) / 2);
                     const radius = Math.min(1, dist / maxDist) * docMax; // set radius based on ratio of drag distance to half diagonal distance of bounding box
                     doc._layout_borderRounding = `${radius}px`;
@@ -339,7 +330,7 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
             e,
             returnFalse, // don't care about move or up event,
             emptyFunction, // just care about whether we get a click event
-            () => UndoManager.RunInBatch(() => SelectionManager.Docs.forEach(doc => Doc.toggleLockedPosition(doc)), 'toggleBackground')
+            () => UndoManager.RunInBatch(() => DocumentView.Selected().forEach(dv => Doc.toggleLockedPosition(dv.Document)), 'toggleBackground')
         );
         e.stopPropagation();
     };
@@ -356,7 +347,7 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
     @action
     onRotateCenterDown = (e: React.PointerEvent): void => {
         this._isRotating = true;
-        const seldocview = SelectionManager.Views[0];
+        const seldocview = DocumentView.Selected()[0];
         setupMoveUpEvents(
             this,
             e,
@@ -374,11 +365,11 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
         this._isRotating = true;
         const rcScreen = { X: this.rotCenter[0], Y: this.rotCenter[1] };
         const rotateUndo = UndoManager.StartBatch('drag rotation');
-        const selectedInk = SelectionManager.Views.filter(i => i.ComponentView instanceof InkingStroke);
+        const selectedInk = DocumentView.Selected().filter(i => i.ComponentView instanceof InkingStroke);
         const centerPoint = this.rotCenter.slice();
         const infos = new Map<Doc, { unrotatedDocPos: { x: number; y: number }; startRotCtr: { x: number; y: number }; accumRot: number }>();
-        const seldocview = SelectionManager.Views[0];
-        SelectionManager.Views.forEach(dv => {
+        const seldocview = DocumentView.Selected()[0];
+        DocumentView.Selected().forEach(dv => {
             const accumRot = (NumCast(dv.Document._rotation) / 180) * Math.PI;
             const localRotCtr = dv.screenToViewTransform().transformPoint(rcScreen.X, rcScreen.Y);
             const localRotCtrOffset = [localRotCtr[0] - NumCast(dv.Document.width) / 2, localRotCtr[1] - NumCast(dv.Document.height) / 2];
@@ -387,7 +378,7 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
             infos.set(dv.Document, { unrotatedDocPos, startRotCtr, accumRot });
         });
         const infoRot = (angle: number, isAbs = false) => {
-            SelectionManager.Views.forEach(
+            DocumentView.Selected().forEach(
                 action(dv => {
                     const { unrotatedDocPos, startRotCtr, accumRot } = infos.get(dv.Document)!;
                     const endRotCtr = Utils.rotPt(startRotCtr.x, startRotCtr.y, isAbs ? angle : accumRot + angle);
@@ -437,7 +428,7 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
 
     @action
     onPointerDown = (e: React.PointerEvent): void => {
-        SnappingManager.SetIsResizing(SelectionManager.Docs.lastElement()?.[Id]); // turns off pointer events on things like youtube videos and web pages so that dragging doesn't get "stuck" when cursor moves over them
+        SnappingManager.SetIsResizing(DocumentView.Selected().lastElement()?.Document[Id]); // turns off pointer events on things like youtube videos and web pages so that dragging doesn't get "stuck" when cursor moves over them
         setupMoveUpEvents(this, e, this.onPointerMove, this.onPointerUp, emptyFunction);
         e.stopPropagation();
         const id = (this._resizeHdlId = e.currentTarget.className);
@@ -449,7 +440,7 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
         };
         this._resizeUndo = UndoManager.StartBatch('drag resizing');
         this._snapPt = { x: e.pageX, y: e.pageY };
-        SelectionManager.Views.forEach(docView => CollectionFreeFormView.from(docView)?.dragStarting(false, false));
+        DocumentView.Selected().forEach(docView => CollectionFreeFormView.from(docView)?.dragStarting(false, false));
     };
 
     projectDragToAspect = (e: PointerEvent, docView: DocumentView, fixedAspect: number) => {
@@ -467,7 +458,7 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
         return project([e.clientX + this._offset.x, e.clientY + this._offset.y], tl, [tl[0] + fixedAspect, tl[1] + 1]);
     };
     onPointerMove = (e: PointerEvent): boolean => {
-        const first = SelectionManager.Views[0];
+        const first = DocumentView.Selected()[0];
         const effectiveAcl = GetEffectiveAcl(first.Document);
         if (!(effectiveAcl === AclAdmin || effectiveAcl === AclEdit || effectiveAcl === AclAugment)) return false;
         if (!first) return false;
@@ -484,10 +475,10 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
         !this._interactionLock && runInAction(async () => {   // resize selected docs if we're not in the middle of a resize (ie, throttle input events to frame rate)
             this._interactionLock = true;
             this._snapPt = thisPt;
-            e.ctrlKey && (SelectionManager.Views.forEach(docView => !Doc.NativeHeight(docView.Document) && docView.toggleNativeDimensions()));
-            const hasFixedAspect = SelectionManager.Docs.some(this.hasFixedAspect);
+            e.ctrlKey && (DocumentView.Selected().forEach(docView => !Doc.NativeHeight(docView.Document) && docView.toggleNativeDimensions()));
+            const hasFixedAspect = DocumentView.Selected().map(dv => dv.Document).some(this.hasFixedAspect);
             const scaleAspect = {x:scale.x === 1 && hasFixedAspect ? scale.y : scale.x, y: scale.x !== 1 && hasFixedAspect ? scale.x : scale.y};
-            SelectionManager.Views.forEach(docView => 
+            DocumentView.Selected().forEach(docView => 
                 this.resizeView(docView, refPt, scaleAspect, { dragHdl, ctrlKey:e.ctrlKey })); // prettier-ignore
             await new Promise<any>(res => { setTimeout(() => { res(this._interactionLock = undefined)})});
         }); // prettier-ignore
@@ -526,7 +517,7 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
         const doc = docView.Document;
         if (doc.isGroup) {
             DocListCast(doc.data)
-                .map(member => DocumentManager.Instance.getDocumentView(member, docView)!)
+                .map(member => DocumentView.getDocumentView(member, docView)!)
                 .forEach(member => this.resizeView(member, refPt, scale, opts));
             doc.xPadding = NumCast(doc.xPadding) * scale.x;
             doc.yPadding = NumCast(doc.yPadding) * scale.y;
@@ -607,7 +598,7 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
         this._resizeUndo?.end();
 
         // detect layout_autoHeight gesture and apply
-        SelectionManager.Views.forEach(view => {
+        DocumentView.Selected().forEach(view => {
             NumCast(view.Document._height) < 20 && (view.layoutDoc._layout_autoHeight = true);
         });
         // need to change points for resize, or else rotation/control points will fail.
@@ -626,18 +617,18 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
 
     @computed
     get selectionTitle(): string {
-        if (SelectionManager.Views.length === 1) {
-            const selected = SelectionManager.Views[0];
+        if (DocumentView.Selected().length === 1) {
+            const selected = DocumentView.Selected()[0];
             if (this._titleControlString.startsWith('$')) {
                 return Field.toJavascriptString(selected.Document[this._titleControlString.substring(1)] as FieldType) || '-unset-';
             }
             return this._accumulatedTitle;
         }
-        return SelectionManager.Views.length > 1 ? '-multiple-' : '-unset-';
+        return DocumentView.Selected().length > 1 ? '-multiple-' : '-unset-';
     }
 
     @computed get rotCenter() {
-        const lastView = SelectionManager.Views.lastElement();
+        const lastView = DocumentView.Selected().lastElement();
         if (lastView) {
             const invXf = lastView.screenToContentsTransform().inverse();
             const seldoc = lastView.layoutDoc;
@@ -649,7 +640,7 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
 
     render() {
         const { b, r, x, y } = this.Bounds;
-        const seldocview = SelectionManager.Views.lastElement();
+        const seldocview = DocumentView.Selected().lastElement();
         if (SnappingManager.IsDragging || r - x < 1 || x === Number.MAX_VALUE || !seldocview || this._hidden || isNaN(r) || isNaN(b) || isNaN(x) || isNaN(y)) {
             setTimeout(
                 action(() => {
@@ -678,7 +669,7 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
             hideDecorations ||
             seldocview._props.hideOpenButton ||
             seldocview.Document.layout_hideOpenButton ||
-            SelectionManager.Views.some(docView => docView.Document._dragOnlyWithinContainer || docView.Document.isGroup || docView.Document.layout_hideOpenButton) ||
+            DocumentView.Selected().some(docView => docView.Document._dragOnlyWithinContainer || docView.Document.isGroup || docView.Document.layout_hideOpenButton) ||
             this._isRounding ||
             this._isRotating;
         const hideDeleteButton =
@@ -688,7 +679,7 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
             this._isRotating ||
             seldocview._props.hideDeleteButton ||
             seldocview.Document.hideDeleteButton ||
-            SelectionManager.Views.some(docView => {
+            DocumentView.Selected().some(docView => {
                 const collectionAcl = docView.containerViewPath?.()?.lastElement() ? GetEffectiveAcl(docView.containerViewPath?.().lastElement().dataDoc) : AclEdit;
                 return collectionAcl !== AclAdmin && collectionAcl !== AclEdit && GetEffectiveAcl(docView.Document) !== AclAdmin;
             });
@@ -703,7 +694,7 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
         const bounds = this.ClippedBounds;
         const useLock = bounds.r - bounds.x > 135;
         const useRotation = !hideResizers && seldocview.Document.type !== DocumentType.EQUATION && CollectionFreeFormDocumentView.from(seldocview); // when do we want an object to not rotate?
-        const rotation = SelectionManager.Views.length === 1 ? seldocview.screenToContentsTransform().inverse().RotateDeg : 0;
+        const rotation = DocumentView.Selected().length === 1 ? seldocview.screenToContentsTransform().inverse().RotateDeg : 0;
 
         // Radius constants
         const useRounding = seldocview.ComponentView instanceof ImageBox || seldocview.ComponentView instanceof FormattedTextBox || seldocview.ComponentView instanceof CollectionFreeFormView;
@@ -773,7 +764,7 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
         );
         const centery = hideTitle ? 0 : this._titleHeight;
         const transformOrigin = `${50}% calc(50% + ${centery / 2}px)`;
-        const freeformDoc = SelectionManager.Views.some(v => CollectionFreeFormDocumentView.from(v));
+        const freeformDoc = DocumentView.Selected().some(v => CollectionFreeFormDocumentView.from(v));
         return (
             <div className="documentDecorations" style={{ display: this._showNothing && !freeformDoc ? 'none' : undefined }}>
                 <div
@@ -786,7 +777,7 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
                         transformOrigin,
                         background: SnappingManager.ShiftKey ? undefined : 'yellow',
                         pointerEvents: SnappingManager.ShiftKey || SnappingManager.IsResizing ? 'none' : 'all',
-                        display: SelectionManager.Views.length <= 1 || hideDecorations ? 'none' : undefined,
+                        display: DocumentView.Selected().length <= 1 || hideDecorations ? 'none' : undefined,
                         transform: `rotate(${rotation}deg)`,
                     }}
                     onPointerDown={this.onBackgroundDown}
@@ -843,7 +834,7 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
                                     style={{
                                         transform: `translate(${-this._resizeBorderWidth / 2 + 10}px, ${this._resizeBorderWidth + bounds.b - bounds.y + this._titleHeight}px) `,
                                     }}>
-                                    <DocumentButtonBar views={() => SelectionManager.Views} />
+                                    <DocumentButtonBar views={() => DocumentView.Selected()} />
                                 </div>
                             )}
                         </div>
diff --git a/src/client/views/FieldsDropdown.tsx b/src/client/views/FieldsDropdown.tsx
index 3cb7848c2..0ea0ebd83 100644
--- a/src/client/views/FieldsDropdown.tsx
+++ b/src/client/views/FieldsDropdown.tsx
@@ -13,7 +13,7 @@ import Select from 'react-select';
 import { Doc } from '../../fields/Doc';
 import { DocOptions, FInfo } from '../documents/Documents';
 import { SearchUtil } from '../util/SearchUtil';
-import { SettingsManager } from '../util/SettingsManager';
+import { SnappingManager } from '../util/SnappingManager';
 import './FilterPanel.scss';
 import { ObservableReactComponent } from './ObservableReactComponent';
 
@@ -68,36 +68,36 @@ export class FieldsDropdown extends ObservableReactComponent<fieldsDropdownProps
                         ...baseStyles,
                         minHeight: '5px',
                         maxHeight: '30px',
-                        color: SettingsManager.userColor,
-                        backgroundColor: SettingsManager.userBackgroundColor,
+                        color: SnappingManager.userColor,
+                        backgroundColor: SnappingManager.userBackgroundColor,
                         padding: 0,
                         margin: 0,
                     }),
                     singleValue: (baseStyles /* , state */) => ({
                         ...baseStyles,
-                        color: SettingsManager.userColor,
-                        background: SettingsManager.userBackgroundColor,
+                        color: SnappingManager.userColor,
+                        background: SnappingManager.userBackgroundColor,
                     }),
                     placeholder: (baseStyles /* , state */) => ({
                         ...baseStyles,
-                        color: SettingsManager.userColor,
-                        background: SettingsManager.userBackgroundColor,
+                        color: SnappingManager.userColor,
+                        background: SnappingManager.userBackgroundColor,
                     }),
                     input: (baseStyles /* , state */) => ({
                         ...baseStyles,
                         padding: 0,
                         margin: 0,
-                        color: SettingsManager.userColor,
+                        color: SnappingManager.userColor,
                         background: 'transparent',
                     }),
                     option: (baseStyles, state) => ({
                         ...baseStyles,
-                        color: SettingsManager.userColor,
-                        background: !state.isFocused ? SettingsManager.userBackgroundColor : SettingsManager.userVariantColor,
+                        color: SnappingManager.userColor,
+                        background: !state.isFocused ? SnappingManager.userBackgroundColor : SnappingManager.userVariantColor,
                     }),
                     menuList: (baseStyles /* , state */) => ({
                         ...baseStyles,
-                        backgroundColor: SettingsManager.userBackgroundColor,
+                        backgroundColor: SnappingManager.userBackgroundColor,
                     }),
                 }}
                 placeholder={typeof this._props.placeholder === 'string' ? this._props.placeholder : this._props.placeholder?.()}
diff --git a/src/client/views/FilterPanel.tsx b/src/client/views/FilterPanel.tsx
index ed9fc8dfb..c97edd7f0 100644
--- a/src/client/views/FilterPanel.tsx
+++ b/src/client/views/FilterPanel.tsx
@@ -11,12 +11,12 @@ import { Doc, DocListCast, Field, FieldType, LinkedTo, StrListCast } from '../..
 import { Id } from '../../fields/FieldSymbols';
 import { List } from '../../fields/List';
 import { RichTextField } from '../../fields/RichTextField';
-import { DocumentManager } from '../util/DocumentManager';
 import { SearchUtil } from '../util/SearchUtil';
-import { SettingsManager } from '../util/SettingsManager';
+import { SnappingManager } from '../util/SnappingManager';
 import { undoable } from '../util/UndoManager';
 import { FieldsDropdown } from './FieldsDropdown';
 import './FilterPanel.scss';
+import { DocumentView } from './nodes/DocumentView';
 import { Handle, Tick, TooltipRail, Track } from './nodes/SliderBox-components';
 import { ObservableReactComponent } from './ObservableReactComponent';
 
@@ -40,7 +40,7 @@ export class FilterPanel extends ObservableReactComponent<filterProps> {
         return this._props.Document;
     }
     @computed get targetDocChildKey() {
-        const targetView = DocumentManager.Instance.getFirstDocumentView(this.Document);
+        const targetView = DocumentView.getFirstDocumentView(this.Document);
         return targetView?.ComponentView?.annotationKey ?? targetView?.ComponentView?.fieldKey ?? 'data';
     }
     @computed get targetDocChildren() {
@@ -299,7 +299,7 @@ export class FilterPanel extends ObservableReactComponent<filterProps> {
                                 .find(filter => filter.split(Doc.FilterSep)[0] === facetHeader)
                                 ?.split(Doc.FilterSep)[1]
                         }
-                        style={{ color: SettingsManager.userColor, background: SettingsManager.userBackgroundColor }}
+                        style={{ color: SnappingManager.userColor, background: SnappingManager.userBackgroundColor }}
                         onBlur={undoable(e => Doc.setDocFilter(this.Document, facetHeader, e.currentTarget.value, !e.currentTarget.value ? 'remove' : 'match'), 'set text filter')}
                         onKeyDown={e => e.key === 'Enter' && undoable(() => Doc.setDocFilter(this.Document, facetHeader, e.currentTarget.value, !e.currentTarget.value ? 'remove' : 'match'), 'set text filter')()}
                     />
diff --git a/src/client/views/GestureOverlay.tsx b/src/client/views/GestureOverlay.tsx
index 0d1573ea3..7246f0ba0 100644
--- a/src/client/views/GestureOverlay.tsx
+++ b/src/client/views/GestureOverlay.tsx
@@ -65,8 +65,6 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
     @observable private _clipboardDoc?: JSX.Element = undefined;
     @observable private _possibilities: JSX.Element[] = [];
 
-    public static DownDocView: DocumentView | undefined;
-
     @computed private get height(): number {
         return 2 * Math.max(this._pointerY && this._thumbY ? this._thumbY - this._pointerY : 100, 100);
     }
@@ -136,7 +134,7 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
     }
     @action
     onPointerUp = () => {
-        GestureOverlay.DownDocView = undefined;
+        DocumentView.DownDocView = undefined;
         if (this._points.length > 1) {
             const B = this.svgBounds;
             const points = this._points.map(p => ({ X: p.X - B.left, Y: p.Y - B.top }));
@@ -388,7 +386,7 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
     }
 
     get elements() {
-        const selView = GestureOverlay.DownDocView;
+        const selView = DocumentView.DownDocView;
         const width = Number(ActiveInkWidth()) * NumCast(selView?.Document._freeform_scale, 1); // * (selView?.screenToViewTransform().Scale || 1);
         const rect = this._overlayRef.current?.getBoundingClientRect();
         const B = { left: -20000, right: 20000, top: -20000, bottom: 20000, width: 40000, height: 40000 }; // this.getBounds(this._points, true);
diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts
index 0f444094a..c73da35bc 100644
--- a/src/client/views/GlobalKeyHandler.ts
+++ b/src/client/views/GlobalKeyHandler.ts
@@ -8,7 +8,6 @@ import { Cast, PromiseValue } from '../../fields/Types';
 import { GoogleAuthenticationManager } from '../apis/GoogleAuthenticationManager';
 import { DragManager } from '../util/DragManager';
 import { GroupManager } from '../util/GroupManager';
-import { SelectionManager } from '../util/SelectionManager';
 import { SettingsManager } from '../util/SettingsManager';
 import { SharingManager } from '../util/SharingManager';
 import { SnappingManager } from '../util/SnappingManager';
@@ -23,6 +22,7 @@ import { CollectionStackedTimeline } from './collections/CollectionStackedTimeli
 import { CollectionFreeFormView } from './collections/collectionFreeForm';
 import { CollectionFreeFormDocumentView } from './nodes/CollectionFreeFormDocumentView';
 import { DocumentLinksButton } from './nodes/DocumentLinksButton';
+import { DocumentView } from './nodes/DocumentView';
 import { OpenWhereMod } from './nodes/OpenWhere';
 import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox';
 import { AnchorMenu } from './pdf/AnchorMenu';
@@ -34,10 +34,12 @@ type KeyControlInfo = {
 };
 type KeyHandler = (keycode: string, e: KeyboardEvent) => KeyControlInfo;
 export class KeyManager {
-    public static Instance = new KeyManager();
+    // eslint-disable-next-line no-use-before-define
+    public static Instance: KeyManager;
     private router = new Map<string, KeyHandler>();
 
     constructor() {
+        KeyManager.Instance = this;
         const isMac = navigator.platform.toLowerCase().indexOf('mac') >= 0;
 
         // SHIFT CONTROL ALT META
@@ -46,6 +48,16 @@ export class KeyManager {
         this.router.set(isMac ? '0100' : '0010', this.alt);
         this.router.set(isMac ? '1001' : '1100', this.ctrl_shift);
         this.router.set('1000', this.shift);
+
+        window.removeEventListener('keydown', KeyManager.Instance.handleModifiers, true);
+        window.addEventListener('keydown', KeyManager.Instance.handleModifiers, true);
+        window.removeEventListener('keyup', KeyManager.Instance.unhandleModifiers);
+        window.addEventListener('keyup', KeyManager.Instance.unhandleModifiers);
+        window.removeEventListener('keydown', KeyManager.Instance.handle);
+        window.addEventListener('keydown', KeyManager.Instance.handle);
+        window.removeEventListener('keyup', KeyManager.Instance.unhandle);
+        window.addEventListener('keyup', KeyManager.Instance.unhandle);
+        window.addEventListener('paste', KeyManager.Instance.paste as any);
     }
 
     public unhandle = action((/* e: KeyboardEvent */) => {
@@ -89,8 +101,8 @@ export class KeyManager {
     private handleGreedy = action((/* keyname: string */) => {});
 
     nudge = (x: number, y: number, label: string) => {
-        const nudgeable = SelectionManager.Views.some(dv => CollectionFreeFormDocumentView.from(dv)?.nudge);
-        nudgeable && UndoManager.RunInBatch(() => SelectionManager.Views.map(dv => CollectionFreeFormDocumentView.from(dv)?.nudge(x, y)), label);
+        const nudgeable = DocumentView.Selected().some(dv => CollectionFreeFormDocumentView.from(dv)?.nudge);
+        nudgeable && UndoManager.RunInBatch(() => DocumentView.Selected().map(dv => CollectionFreeFormDocumentView.from(dv)?.nudge(x, y)), label);
         return { stopPropagation: nudgeable, preventDefault: nudgeable };
     };
 
@@ -98,16 +110,16 @@ export class KeyManager {
         switch (keyname) {
             case 'u':
                 if (document.activeElement?.tagName !== 'INPUT' && document.activeElement?.tagName !== 'TEXTAREA') {
-                    const ungroupings = SelectionManager.Views;
+                    const ungroupings = DocumentView.Selected();
                     undoable(() => () => ungroupings.forEach(dv => { dv.layoutDoc.group = undefined; }), 'ungroup');
-                    SelectionManager.DeselectAll();
+                    DocumentView.DeselectAll();
                 }
                 break;
             case 'g':
                 if (document.activeElement?.tagName !== 'INPUT' && document.activeElement?.tagName !== 'TEXTAREA') {
-                    const selected = SelectionManager.Views;
+                    const selected = DocumentView.Selected();
                     const cv = selected.reduce((col, dv) => (!col || CollectionFreeFormView.from(dv) === col ? CollectionFreeFormView.from(dv) : undefined), undefined as undefined | CollectionFreeFormView);
-                    cv && undoable(() => cv._marqueeViewRef.current?.collection(e, true, SelectionManager.Docs), 'grouping');
+                    cv && undoable(() => cv._marqueeViewRef.current?.collection(e, true, DocumentView.SelectedDocs()), 'grouping');
                 }
                 break;
             case ' ':
@@ -133,7 +145,7 @@ export class KeyManager {
                         doDeselect = !ContextMenu.Instance.closeMenu();
                     }
                     if (doDeselect) {
-                        SelectionManager.DeselectAll();
+                        DocumentView.DeselectAll();
                         LightboxView.Instance.SetLightboxDoc(undefined);
                     }
                     // DictationManager.Controls.stop();
@@ -153,7 +165,7 @@ export class KeyManager {
                 if (document.activeElement?.tagName !== 'INPUT' && document.activeElement?.tagName !== 'TEXTAREA') {
                     if (LightboxView.LightboxDoc) {
                         LightboxView.Instance.SetLightboxDoc(undefined);
-                        SelectionManager.DeselectAll();
+                        DocumentView.DeselectAll();
                     } else if (!window.getSelection()?.toString()) DocumentDecorations.Instance.onCloseClick(true);
                     return { stopPropagation: true, preventDefault: true };
                 }
@@ -179,15 +191,15 @@ export class KeyManager {
             case 'arrowdown':  return this.nudge(0, 10, 'nudge down');
             case 'u' : 
                 if (document.activeElement?.tagName !== 'INPUT' && document.activeElement?.tagName !== 'TEXTAREA') {
-                    UndoManager.RunInBatch(() => SelectionManager.Docs.forEach(doc => {doc.group = undefined}), 'unggroup');
-                    SelectionManager.DeselectAll();
+                    UndoManager.RunInBatch(() => DocumentView.Selected().map(dv => dv.Document).forEach(doc => {doc.group = undefined}), 'unggroup');
+                    DocumentView.DeselectAll();
                 }
                 break;
             case 'g':
                 if (document.activeElement?.tagName !== 'INPUT' && document.activeElement?.tagName !== 'TEXTAREA') {
                     const randomGroup = random(0, 1000);
-                    UndoManager.RunInBatch(() => SelectionManager.Docs.forEach(doc => {doc.group = randomGroup}), 'group');
-                    SelectionManager.DeselectAll();
+                    UndoManager.RunInBatch(() => DocumentView.Selected().forEach(dv => {dv.Document.group = randomGroup}), 'group');
+                    DocumentView.DeselectAll();
                 }
                 break;
             default: 
@@ -206,7 +218,7 @@ export class KeyManager {
         switch (keyname) {
             case 'ƒ':
             case 'f':
-                UndoManager.RunInBatch(() => CollectionFreeFormDocumentView.from(SelectionManager.Views?.[0])?.float(), 'float');
+                UndoManager.RunInBatch(() => CollectionFreeFormDocumentView.from(DocumentView.Selected()?.[0])?.float(), 'float');
                 break;
             default:
         }
@@ -259,8 +271,8 @@ export class KeyManager {
                 }
                 break;
             case 'f':
-                if (SelectionManager.Views.length === 1 && SelectionManager.Views[0].ComponentView?.search) {
-                    SelectionManager.Views[0].ComponentView?.search?.('', false, false);
+                if (DocumentView.Selected().length === 1 && DocumentView.Selected()[0].ComponentView?.search) {
+                    DocumentView.Selected()[0].ComponentView?.search?.('', false, false);
                 } else {
                     const searchBtn = DocListCast(Doc.MyLeftSidebarMenu.data).find(d => d.target === Doc.MySearcher);
                     if (searchBtn) {
@@ -279,14 +291,14 @@ export class KeyManager {
                 break;
             case 'y':
                 if (Doc.ActivePage !== 'home') {
-                    SelectionManager.DeselectAll();
+                    DocumentView.DeselectAll();
                     UndoManager.Redo();
                 }
                 stopPropagation = false;
                 break;
             case 'z':
                 if (Doc.ActivePage !== 'home') {
-                    SelectionManager.DeselectAll();
+                    DocumentView.DeselectAll();
                     UndoManager.Undo();
                 }
                 stopPropagation = false;
@@ -302,11 +314,17 @@ export class KeyManager {
                 preventDefault = false;
                 break;
             case 'x':
-                if (SelectionManager.Views.length) {
+                if (DocumentView.Selected().length) {
                     const bds = DocumentDecorations.Instance.Bounds;
-                    const pt = SelectionManager.Views[0].screenToViewTransform().transformPoint(bds.x + (bds.r - bds.x) / 2, bds.y + (bds.b - bds.y) / 2);
-                    const text = `__DashDocId(${pt?.[0] || 0},${pt?.[1] || 0}):` + SelectionManager.Views.map(dv => dv.Document[Id]).join(':');
-                    SelectionManager.Views.length && navigator.clipboard.writeText(text);
+                    const pt = DocumentView.Selected()[0] //
+                        .screenToViewTransform()
+                        .transformPoint(bds.x + (bds.r - bds.x) / 2, bds.y + (bds.b - bds.y) / 2);
+                    const text = 
+                        `__DashDocId(${pt?.[0] || 0},${pt?.[1] || 0}):` + //
+                        DocumentView.Selected() 
+                            .map(dv => dv.Document[Id])
+                            .join(':'); // prettier-ignore
+                    DocumentView.Selected().length && navigator.clipboard.writeText(text);
                     DocumentDecorations.Instance.onCloseClick(true);
                     stopPropagation = false;
                     preventDefault = false;
@@ -315,9 +333,15 @@ export class KeyManager {
             case 'c':
                 if ((document.activeElement as any)?.type !== 'text' && !AnchorMenu.Instance.Active && DocumentDecorations.Instance.Bounds.r - DocumentDecorations.Instance.Bounds.x > 2) {
                     const bds = DocumentDecorations.Instance.Bounds;
-                    const pt = SelectionManager.Views[0].screenToViewTransform().transformPoint(bds.x + (bds.r - bds.x) / 2, bds.y + (bds.b - bds.y) / 2);
-                    const text = `__DashCloneId(${pt?.[0] || 0},${pt?.[1] || 0}):` + SelectionManager.Views.map(dv => dv.Document[Id]).join(':');
-                    SelectionManager.Views.length && navigator.clipboard.writeText(text);
+                    const pt = DocumentView.Selected()[0]
+                        .screenToViewTransform()
+                        .transformPoint(bds.x + (bds.r - bds.x) / 2, bds.y + (bds.b - bds.y) / 2); // prettier-ignore
+                    const text =
+                        `__DashCloneId(${pt?.[0] || 0},${pt?.[1] || 0}):` +
+                        DocumentView.SelectedDocs()
+                            .map(doc => doc[Id])
+                            .join(':'); // prettier-ignore
+                    DocumentView.Selected().length && navigator.clipboard.writeText(text);
                     stopPropagation = false;
                 }
                 preventDefault = false;
@@ -336,7 +360,7 @@ export class KeyManager {
         if (!plain) return;
         const clone = plain.startsWith('__DashCloneId(');
         const docids = plain.split(':'); // hack!  docids[0] is the top left of the selection rectangle
-        const addDocument = SelectionManager.Views.lastElement()?.ComponentView?.addDocument;
+        const addDocument = DocumentView.Selected().lastElement()?.ComponentView?.addDocument;
         if (addDocument && (plain.startsWith('__DashDocId(') || clone)) {
             Doc.Paste(docids.slice(1), clone, addDocument);
         }
diff --git a/src/client/views/InkControlPtHandles.tsx b/src/client/views/InkControlPtHandles.tsx
index b4fe44733..a49923ffb 100644
--- a/src/client/views/InkControlPtHandles.tsx
+++ b/src/client/views/InkControlPtHandles.tsx
@@ -7,7 +7,6 @@ import { List } from '../../fields/List';
 import { listSpec } from '../../fields/Schema';
 import { Cast } from '../../fields/Types';
 import { returnFalse, setupMoveUpEvents } from '../../ClientUtils';
-import { SelectionManager } from '../util/SelectionManager';
 import { UndoManager } from '../util/UndoManager';
 import { Colors } from './global/globalEnums';
 import { InkingStroke } from './InkingStroke';
@@ -15,6 +14,7 @@ import { InkStrokeProperties } from './InkStrokeProperties';
 import { SnappingManager } from '../util/SnappingManager';
 import { ObservableReactComponent } from './ObservableReactComponent';
 import { PointData } from '../../pen-gestures/GestureTypes';
+import { DocumentView } from './nodes/DocumentView';
 
 export interface InkControlProps {
     inkDoc: Doc;
@@ -224,8 +224,8 @@ export class InkEndPtHandles extends ObservableReactComponent<InkEndProps> {
                 const v1n = { X: v1.X / v1len, Y: v1.Y / v1len };
                 const v2n = { X: v2.X / v2len, Y: v2.Y / v2len };
                 const angle = Math.acos(v1n.X * v2n.X + v1n.Y * v2n.Y) * Math.sign(v1.X * v2.Y - v2.X * v1.Y);
-                InkStrokeProperties.Instance.stretchInk(SelectionManager.Views, scaling, p2, v1n, moveEv.shiftKey);
-                InkStrokeProperties.Instance.rotateInk(SelectionManager.Views, angle, pt2()); // bcz: call pt2() func here because pt2 will have changed from previous stretchInk call
+                InkStrokeProperties.Instance.stretchInk(DocumentView.Selected(), scaling, p2, v1n, moveEv.shiftKey);
+                InkStrokeProperties.Instance.rotateInk(DocumentView.Selected(), angle, pt2()); // bcz: call pt2() func here because pt2 will have changed from previous stretchInk call
                 return false;
             }),
             action(() => {
diff --git a/src/client/views/InkStrokeProperties.ts b/src/client/views/InkStrokeProperties.ts
index 109a9cad4..3920ecc2a 100644
--- a/src/client/views/InkStrokeProperties.ts
+++ b/src/client/views/InkStrokeProperties.ts
@@ -9,7 +9,6 @@ import { Cast, NumCast } from '../../fields/Types';
 import { PointData } from '../../pen-gestures/GestureTypes';
 import { Point } from '../../pen-gestures/ndollar';
 import { DocumentType } from '../documents/DocumentTypes';
-import { DocumentManager } from '../util/DocumentManager';
 import { undoBatch } from '../util/UndoManager';
 import { FitOneCurve } from '../util/bezierFit';
 import { InkingStroke } from './InkingStroke';
@@ -386,7 +385,7 @@ export class InkStrokeProperties {
         containingCollection?.childDocs
             .filter(doc => doc.type === DocumentType.INK)
             .forEach(doc => {
-                const testInkView = DocumentManager.Instance.getDocumentView(doc, containingDocView);
+                const testInkView = DocumentView.getDocumentView(doc, containingDocView);
                 const snapped = testInkView?.ComponentView?.snapPt?.(screenDragPt, doc === inkView.Document ? this.excludeSelfSnapSegs(ink, controlIndex) : []);
                 if (snapped && snapped.distance < snapData.distance) {
                     const snappedInkPt = doc === inkView.Document ? snapped.nearestPt : inkView.ComponentView?.ptFromScreen?.(testInkView?.ComponentView?.ptToScreen?.(snapped.nearestPt) ?? { X: 0, Y: 0 }); // convert from snapped ink coordinate system to dragged ink coordinate system by converting to/from screen space
diff --git a/src/client/views/InkTranscription.tsx b/src/client/views/InkTranscription.tsx
index 7e2b334af..1ed8de1be 100644
--- a/src/client/views/InkTranscription.tsx
+++ b/src/client/views/InkTranscription.tsx
@@ -6,7 +6,6 @@
 // import { Cast, DateCast, NumCast } from '../../fields/Types';
 // import { aggregateBounds } from '../../Utils';
 // import { DocumentType } from '../documents/DocumentTypes';
-// import { DocumentManager } from '../util/DocumentManager';
 // import { CollectionFreeFormView } from './collections/collectionFreeForm';
 // import { InkingStroke } from './InkingStroke';
 // import './InkTranscription.scss';
diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx
index 884512539..62fc73c78 100644
--- a/src/client/views/InkingStroke.tsx
+++ b/src/client/views/InkingStroke.tsx
@@ -44,7 +44,7 @@ import { InkTangentHandles } from './InkTangentHandles';
 import { FieldView, FieldViewProps } from './nodes/FieldView';
 import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox';
 import { PinDocView, PinProps } from './PinFuncs';
-import { StyleProp } from './StyleProvider';
+import { StyleProp } from './StyleProp';
 
 const { INK_MASK_SIZE } = require('./global/globalCssVariables.module.scss'); // prettier-ignore
 
diff --git a/src/client/views/LightboxView.tsx b/src/client/views/LightboxView.tsx
index 020525ef8..12d899388 100644
--- a/src/client/views/LightboxView.tsx
+++ b/src/client/views/LightboxView.tsx
@@ -13,10 +13,6 @@ import { CreateLinkToActiveAudio, Doc, DocListCast, FieldResult, Opt } from '../
 import { Id } from '../../fields/FieldSymbols';
 import { InkTool } from '../../fields/InkField';
 import { Cast, NumCast, toList } from '../../fields/Types';
-import { DocumentManager } from '../util/DocumentManager';
-import { LinkManager } from '../util/LinkManager';
-import { SelectionManager } from '../util/SelectionManager';
-import { SettingsManager } from '../util/SettingsManager';
 import { SnappingManager } from '../util/SnappingManager';
 import { Transform } from '../util/Transform';
 import { GestureOverlay } from './GestureOverlay';
@@ -24,8 +20,6 @@ import './LightboxView.scss';
 import { ObservableReactComponent } from './ObservableReactComponent';
 import { DefaultStyleProvider, wavyBorderPath } from './StyleProvider';
 import { CollectionDockingView } from './collections/CollectionDockingView';
-import { CollectionStackedTimeline } from './collections/CollectionStackedTimeline';
-import { TabDocView } from './collections/TabDocView';
 import { DocumentView } from './nodes/DocumentView';
 import { OpenWhere, OpenWhereMod } from './nodes/OpenWhere';
 
@@ -90,7 +84,7 @@ export class LightboxView extends ObservableReactComponent<LightboxViewProps> {
                 });
             const l = CreateLinkToActiveAudio(() => doc).lastElement();
             l && (Cast(l.link_anchor_2, Doc, null).backgroundColor = 'lightgreen');
-            CollectionStackedTimeline.CurrentlyPlaying?.forEach(dv => dv.ComponentView?.Pause?.());
+            DocumentView.CurrentlyPlaying?.forEach(dv => dv.ComponentView?.Pause?.());
             this._history.push({ doc, target });
         } else {
             this._future = [];
@@ -98,14 +92,14 @@ export class LightboxView extends ObservableReactComponent<LightboxViewProps> {
             Doc.ActiveTool = InkTool.None;
             SnappingManager.SetExploreMode(false);
         }
-        SelectionManager.DeselectAll();
+        DocumentView.DeselectAll();
         if (future) {
             this._future.push(
                 ...(this._doc ? [this._doc] : []),
                 ...future
                     .slice()
                     .sort((a, b) => NumCast(b._timecodeToShow) - NumCast(a._timecodeToShow))
-                    .sort((a, b) => LinkManager.Links(a).length - LinkManager.Links(b).length)
+                    .sort((a, b) => Doc.Links(a).length - Doc.Links(b).length)
             );
         }
         this._doc = doc;
@@ -134,11 +128,11 @@ export class LightboxView extends ObservableReactComponent<LightboxViewProps> {
         const lightDoc = this._doc;
         if (!lightDoc) return;
         const target = (this._docTarget = this._future.pop());
-        const targetDocView = target && DocumentManager.Instance.getLightboxDocumentView(target);
+        const targetDocView = target && DocumentView.getLightboxDocumentView(target);
         if (targetDocView && target) {
             const l = CreateLinkToActiveAudio(() => targetDocView.ComponentView?.getAnchor?.(true) || target).lastElement();
             l && (Cast(l.link_anchor_2, Doc, null).backgroundColor = 'lightgreen');
-            DocumentManager.Instance.showDocument(target, { willZoomCentered: true, zoomScale: 0.9 });
+            DocumentView.showDocument(target, { willZoomCentered: true, zoomScale: 0.9 });
             if (this._history.lastElement().target !== target) this._history.push({ doc: lightDoc, target });
         } else if (!target && this._path.length) {
             savedKeys.forEach(key => {
@@ -157,10 +151,10 @@ export class LightboxView extends ObservableReactComponent<LightboxViewProps> {
             return;
         }
         const { doc, target } = this._history.lastElement();
-        const docView = DocumentManager.Instance.getLightboxDocumentView(target || doc);
+        const docView = DocumentView.getLightboxDocumentView(target || doc);
         if (docView) {
             this._docTarget = target;
-            target && DocumentManager.Instance.showDocument(target, { willZoomCentered: true, zoomScale: 0.9 });
+            target && DocumentView.showDocument(target, { willZoomCentered: true, zoomScale: 0.9 });
         } else {
             this.SetLightboxDoc(doc, target);
         }
@@ -178,8 +172,8 @@ export class LightboxView extends ObservableReactComponent<LightboxViewProps> {
         if (this._docTarget) {
             const fieldKey = Doc.LayoutFieldKey(this._docTarget);
             const contents = [...DocListCast(this._docTarget[fieldKey]), ...DocListCast(this._docTarget[fieldKey + '_annotations'])];
-            const links = LinkManager.Links(this._docTarget)
-                .map(link => LinkManager.getOppositeAnchor(link, this._docTarget!)!)
+            const links = Doc.Links(this._docTarget)
+                .map(link => Doc.getOppositeAnchor(link, this._docTarget!)!)
                 .filter(doc => doc);
             this.SetLightboxDoc(this._docTarget, undefined, contents.length ? contents : links);
         }
@@ -220,7 +214,7 @@ export class LightboxView extends ObservableReactComponent<LightboxViewProps> {
             <div
                 className="lightboxView-navBtn"
                 title={color}
-                style={{ top, color: SettingsManager.userColor, background: undefined }}
+                style={{ top, color: SnappingManager.userColor, background: undefined }}
                 onClick={e => {
                     e.stopPropagation();
                     click();
@@ -237,8 +231,8 @@ export class LightboxView extends ObservableReactComponent<LightboxViewProps> {
             <div className={classname}>
                 <Toggle
                     tooltip={tooltip}
-                    color={SettingsManager.userColor}
-                    background={toggleBackground ? SettingsManager.userVariantColor : SettingsManager.userBackgroundColor}
+                    color={SnappingManager.userColor}
+                    background={toggleBackground ? SnappingManager.userVariantColor : SnappingManager.userBackgroundColor}
                     toggleType={ToggleType.BUTTON}
                     type={Type.TERT}
                     icon={<FontAwesomeIcon icon={toggleBackground ? icon : (icon2 as IconProp) || icon} size="sm" />}
@@ -252,7 +246,7 @@ export class LightboxView extends ObservableReactComponent<LightboxViewProps> {
         return !this._doc ? null : (
             <div
                 className="lightboxView-frame"
-                style={{ background: SettingsManager.userBackgroundColor }}
+                style={{ background: SnappingManager.userBackgroundColor }}
                 onPointerDown={e => {
                     downx = e.clientX;
                     downy = e.clientY;
@@ -266,7 +260,7 @@ export class LightboxView extends ObservableReactComponent<LightboxViewProps> {
                         width: this.lightboxWidth(),
                         height: this.lightboxHeight(),
                         clipPath: `path('${Doc.UserDoc().renderStyle === 'comic' ? wavyBorderPath(this.lightboxWidth(), this.lightboxHeight()) : undefined}')`,
-                        background: SettingsManager.userBackgroundColor,
+                        background: SnappingManager.userBackgroundColor,
                     }}>
                     <GestureOverlay isActive>
                         <DocumentView
@@ -292,7 +286,7 @@ export class LightboxView extends ObservableReactComponent<LightboxViewProps> {
                             removeDocument={undefined}
                             whenChildContentsActiveChanged={emptyFunction}
                             addDocTab={this.AddDocTab}
-                            pinToPres={TabDocView.PinDoc}
+                            pinToPres={DocumentView.PinDoc}
                             focus={emptyFunction}
                         />
                     </GestureOverlay>
diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx
index 01f3b032e..b6dfed687 100644
--- a/src/client/views/Main.tsx
+++ b/src/client/views/Main.tsx
@@ -8,14 +8,17 @@ import * as React from 'react';
 import * as ReactDOM from 'react-dom/client';
 import { AssignAllExtensions } from '../../extensions/Extensions';
 import { FieldLoader } from '../../fields/FieldLoader';
+import { BranchingTrailManager } from '../util/BranchingTrailManager';
 import { CurrentUserUtils } from '../util/CurrentUserUtils';
 import { PingManager } from '../util/PingManager';
 import { ReplayMovements } from '../util/ReplayMovements';
 import { TrackMovements } from '../util/TrackMovements';
+import { KeyManager } from './GlobalKeyHandler';
+import { MainView } from './MainView';
 import { CollectionView } from './collections/CollectionView';
+import { CollectionFreeFormInfoUI } from './collections/collectionFreeForm/CollectionFreeFormInfoUI';
 import './global/globalScripts';
-import { MainView } from './MainView';
-import { BranchingTrailManager } from '../util/BranchingTrailManager';
+import { LinkFollower } from '../util/LinkFollower';
 
 dotenv.config();
 
@@ -58,6 +61,11 @@ FieldLoader.ServerLoadStatus = { requested: 0, retrieved: 0, message: 'cache' };
         new ReplayMovements();
         new BranchingTrailManager({});
         new PingManager();
+        new KeyManager();
+
+        // iniitialize plugin apis
+        CollectionFreeFormInfoUI.Init();
+        LinkFollower.Init();
         root.render(<MainView />);
     }, 0);
 })();
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index aec553baa..2c7d3d32a 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -12,7 +12,8 @@ import '../../../node_modules/browndash-components/dist/styles/global.min.css';
 import { ClientUtils, lightOrDark, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue, returnZero, setupMoveUpEvents } from '../../ClientUtils';
 import { emptyFunction } from '../../Utils';
 import { Doc, DocListCast, GetDocFromUrl, Opt } from '../../fields/Doc';
-import { DocData } from '../../fields/DocSymbols';
+import { DocData, DocViews } from '../../fields/DocSymbols';
+import { Id } from '../../fields/FieldSymbols';
 import { DocCast, StrCast, toList } from '../../fields/Types';
 import { DocServer } from '../DocServer';
 import { GoogleAuthenticationManager } from '../apis/GoogleAuthenticationManager';
@@ -29,7 +30,6 @@ import { Hypothesis } from '../util/HypothesisUtils';
 import { UPDATE_SERVER_CACHE } from '../util/LinkManager';
 import { RTFMarkup } from '../util/RTFMarkup';
 import { ScriptingGlobals } from '../util/ScriptingGlobals';
-import { SelectionManager } from '../util/SelectionManager';
 import { ServerStats } from '../util/ServerStats';
 import { SettingsManager } from '../util/SettingsManager';
 import { SharingManager } from '../util/SharingManager';
@@ -42,7 +42,6 @@ import { DashboardView } from './DashboardView';
 import { DictationOverlay } from './DictationOverlay';
 import { DocumentDecorations } from './DocumentDecorations';
 import { GestureOverlay } from './GestureOverlay';
-import { KeyManager } from './GlobalKeyHandler';
 import { LightboxView } from './LightboxView';
 import './MainView.scss';
 import { ObservableReactComponent } from './ObservableReactComponent';
@@ -160,7 +159,7 @@ export class MainView extends ObservableReactComponent<{}> {
     leftMenuHeight = () => this._dashUIHeight;
     leftMenuFlyoutWidth = () => this._leftMenuFlyoutWidth;
     leftMenuFlyoutHeight = () => this._dashUIHeight;
-    propertiesWidth = () => Math.max(0, Math.min(this._dashUIWidth - 50, SettingsManager.Instance?.propertiesWidth || 0));
+    propertiesWidth = () => Math.max(0, Math.min(this._dashUIWidth - 50, SnappingManager.PropertiesWidth || 0));
     propertiesHeight = () => this._dashUIHeight;
     mainDocViewWidth = () => this._dashUIWidth - this.propertiesWidth() - this.leftMenuWidth() - this.leftMenuFlyoutWidth();
     mainDocViewHeight = () => this._dashUIHeight - this.headerBarDocHeight();
@@ -169,7 +168,7 @@ export class MainView extends ObservableReactComponent<{}> {
         // Utils.TraceConsoleLog();
         reaction(
             // when a multi-selection occurs, remove focus from all active elements to allow keyboad input to go only to global key manager to act upon selection
-            () => SelectionManager.Views.slice(),
+            () => DocumentView.Selected().slice(),
             views => views.length > 1 && (document.activeElement as any)?.blur !== undefined && (document.activeElement as any)!.blur()
         );
         const scriptTag = document.createElement('script');
@@ -231,37 +230,29 @@ export class MainView extends ObservableReactComponent<{}> {
         tag.src = 'https://www.youtube.com/iframe_api';
         const firstScriptTag = document.getElementsByTagName('script')[0];
         firstScriptTag.parentNode!.insertBefore(tag, firstScriptTag);
-        window.removeEventListener('keydown', KeyManager.Instance.handleModifiers, true);
-        window.addEventListener('keydown', KeyManager.Instance.handleModifiers, true);
-        window.removeEventListener('keyup', KeyManager.Instance.unhandleModifiers);
-        window.addEventListener('keyup', KeyManager.Instance.unhandleModifiers);
-        window.removeEventListener('keydown', KeyManager.Instance.handle);
-        window.addEventListener('keydown', KeyManager.Instance.handle);
-        window.removeEventListener('keyup', KeyManager.Instance.unhandle);
-        window.addEventListener('keyup', KeyManager.Instance.unhandle);
-        window.addEventListener('paste', KeyManager.Instance.paste as any);
         document.addEventListener('dash', (e: any) => {
             // event used by chrome plugin to tell Dash which document to focus on
             const id = GetDocFromUrl(e.detail);
-            DocServer.GetRefField(id).then(doc => (doc instanceof Doc ? DocumentManager.Instance.showDocument(doc, { willPan: false }) : null));
+            DocServer.GetRefField(id).then(doc => (doc instanceof Doc ? DocumentView.showDocument(doc, { willPan: false }) : null));
         });
         document.addEventListener('linkAnnotationToDash', Hypothesis.linkListener);
         this.initEventListeners();
     }
 
     componentWillUnMount() {
-        window.removeEventListener('keyup', KeyManager.Instance.unhandle);
-        window.removeEventListener('keydown', KeyManager.Instance.handle);
-        window.removeEventListener('pointerdown', this.globalPointerDown, true);
-        window.removeEventListener('pointermove', this.globalPointerMove, true);
-        window.removeEventListener('pointerup', this.globalPointerClick, true);
-        window.removeEventListener('paste', KeyManager.Instance.paste as any);
-        document.removeEventListener('linkAnnotationToDash', Hypothesis.linkListener);
+        // window.removeEventListener('keyup', KeyManager.Instance.unhandle);
+        // window.removeEventListener('keydown', KeyManager.Instance.handle);
+        // window.removeEventListener('pointerdown', this.globalPointerDown, true);
+        // window.removeEventListener('pointermove', this.globalPointerMove, true);
+        // window.removeEventListener('pointerup', this.globalPointerClick, true);
+        // window.removeEventListener('paste', KeyManager.Instance.paste as any);
+        // document.removeEventListener('linkAnnotationToDash', Hypothesis.linkListener);
     }
 
     constructor(props: any) {
         super(props);
         makeObservable(this);
+        CollectionDockingView.setTabJSXComponent(TabDocView);
         DocumentViewInternal.addDocTabFunc = MainView.addDocTabFunc_impl;
         MainView.Instance = this;
         DashboardView._urlState = HistoryUtil.parseUrl(window.location) || ({} as any);
@@ -592,7 +583,7 @@ export class MainView extends ObservableReactComponent<{}> {
                 if (!e.cancelBubble) {
                     const pathstr = (e as any)?.path?.map((p: any) => p.classList?.toString()).join();
                     if (pathstr?.includes('libraryFlyout')) {
-                        SelectionManager.DeselectAll();
+                        DocumentView.DeselectAll();
                     }
                 }
             },
@@ -719,14 +710,14 @@ export class MainView extends ObservableReactComponent<{}> {
             this,
             e,
             action(() => {
-                SettingsManager.Instance.propertiesWidth = Math.max(0, this._dashUIWidth - e.clientX);
-                return !SettingsManager.Instance.propertiesWidth;
+                SnappingManager.SetPropertiesWidth(Math.max(0, this._dashUIWidth - e.clientX));
+                return !SnappingManager.PropertiesWidth;
             }),
             action(() => {
-                SettingsManager.Instance.propertiesWidth < 5 && (SettingsManager.Instance.propertiesWidth = 0);
+                SnappingManager.PropertiesWidth < 5 && SnappingManager.SetPropertiesWidth(0);
             }),
             action(() => {
-                SettingsManager.Instance.propertiesWidth = this.propertiesWidth() < 15 ? Math.min(this._dashUIWidth - 50, 250) : 0;
+                SnappingManager.SetPropertiesWidth(this.propertiesWidth() < 15 ? Math.min(this._dashUIWidth - 50, 250) : 0);
             }),
             false
         );
@@ -776,7 +767,7 @@ export class MainView extends ObservableReactComponent<{}> {
                         Document={this._sidebarContent.proto || this._sidebarContent}
                         addDocument={undefined}
                         addDocTab={DocumentViewInternal.addDocTabFunc}
-                        pinToPres={TabDocView.PinDoc}
+                        pinToPres={DocumentView.PinDoc}
                         containerViewPath={returnEmptyDoclist}
                         styleProvider={this._sidebarContent.proto === Doc.MyDashboards || this._sidebarContent.proto === Doc.MyFilesystem || this._sidebarContent.proto === Doc.MyTrails ? DashboardStyleProvider : DefaultStyleProvider}
                         removeDocument={returnFalse}
@@ -799,7 +790,7 @@ export class MainView extends ObservableReactComponent<{}> {
 
     @computed get leftMenuPanel() {
         return (
-            <div key="menu" className="mainView-leftMenuPanel" style={{ background: SettingsManager.userBackgroundColor, display: LightboxView.LightboxDoc ? 'none' : undefined }}>
+            <div key="menu" className="mainView-leftMenuPanel" style={{ background: SnappingManager.userBackgroundColor, display: LightboxView.LightboxDoc ? 'none' : undefined }}>
                 <DocumentView
                     Document={Doc.MyLeftSidebarMenu}
                     addDocument={undefined}
@@ -852,9 +843,9 @@ export class MainView extends ObservableReactComponent<{}> {
                     {this.flyout}
                     <div
                         className="mainView-libraryHandle"
-                        style={{ background: SettingsManager.userBackgroundColor, left: leftMenuFlyoutWidth - 10 /* ~half width of handle */, display: !this._leftMenuFlyoutWidth ? 'none' : undefined }}
+                        style={{ background: SnappingManager.userBackgroundColor, left: leftMenuFlyoutWidth - 10 /* ~half width of handle */, display: !this._leftMenuFlyoutWidth ? 'none' : undefined }}
                         onPointerDown={this.onFlyoutPointerDown}>
-                        <FontAwesomeIcon icon="chevron-left" color={SettingsManager.userColor} style={{ opacity: '50%' }} size="sm" />
+                        <FontAwesomeIcon icon="chevron-left" color={SnappingManager.userColor} style={{ opacity: '50%' }} size="sm" />
                     </div>
                     <div className="mainView-innerContainer" style={{ width: `calc(100% - ${width}px)` }}>
                         {this.dockingContent}
@@ -864,11 +855,11 @@ export class MainView extends ObservableReactComponent<{}> {
                                 className={`mainView-propertiesDragger${this.propertiesWidth() < 10 ? '-minified' : ''}`}
                                 key="props"
                                 onPointerDown={this.onPropertiesPointerDown}
-                                style={{ background: SettingsManager.userVariantColor, right: this.propertiesWidth() - 1 }}>
-                                <FontAwesomeIcon icon={this.propertiesWidth() < 10 ? 'chevron-left' : 'chevron-right'} color={SettingsManager.userColor} size="sm" />
+                                style={{ background: SnappingManager.userVariantColor, right: this.propertiesWidth() - 1 }}>
+                                <FontAwesomeIcon icon={this.propertiesWidth() < 10 ? 'chevron-left' : 'chevron-right'} color={SnappingManager.userColor} size="sm" />
                             </div>
                         )}
-                        <div className="properties-container" style={{ width: this.propertiesWidth(), color: SettingsManager.userColor }}>
+                        <div className="properties-container" style={{ width: this.propertiesWidth(), color: SnappingManager.userColor }}>
                             <div style={{ display: this.propertiesWidth() < 10 ? 'none' : undefined }}>
                                 <PropertiesView styleProvider={DefaultStyleProvider} addDocTab={DocumentViewInternal.addDocTabFunc} width={this.propertiesWidth()} height={this.propertiesHeight()} />
                             </div>
@@ -911,11 +902,11 @@ export class MainView extends ObservableReactComponent<{}> {
         // setTimeout(action(() => (this._leftMenuFlyoutWidth += 0.5)));
 
         this._sidebarContent.proto = DocCast(button.target);
-        SettingsManager.Instance.LastPressedBtn = button;
+        SnappingManager.SetLastPressedBtn(button[Id]);
     });
 
     closeFlyout = action(() => {
-        SettingsManager.Instance.LastPressedBtn = undefined;
+        SnappingManager.SetLastPressedBtn('');
         this._panelContent = 'none';
         this._sidebarContent.proto = undefined;
         this._leftMenuFlyoutWidth = 0;
@@ -933,7 +924,7 @@ export class MainView extends ObservableReactComponent<{}> {
 
     @computed get docButtons() {
         return !Doc.MyDockedBtns ? null : (
-            <div className="mainView-docButtons" style={{ background: SettingsManager.userBackgroundColor, color: SettingsManager.userColor }} ref={this._docBtnRef}>
+            <div className="mainView-docButtons" style={{ background: SnappingManager.userBackgroundColor, color: SnappingManager.userColor }} ref={this._docBtnRef}>
                 <CollectionLinearView
                     Document={Doc.MyDockedBtns}
                     docViewPath={returnEmptyDocViewList}
@@ -964,8 +955,8 @@ export class MainView extends ObservableReactComponent<{}> {
         );
     }
     @computed get snapLines() {
-        const dragged = DragManager.docsBeingDragged.lastElement() ?? SelectionManager.Docs.lastElement();
-        const dragPar = dragged ? CollectionFreeFormView.from(DocumentManager.Instance.getDocumentView(dragged)) : undefined;
+        const dragged = DragManager.docsBeingDragged.lastElement() ?? DocumentView.SelectedDocs().lastElement();
+        const dragPar = dragged ? CollectionFreeFormView.from(Array.from(dragged[DocViews]).lastElement()) : undefined;
         return !dragPar?.layoutDoc.freeform_snapLines ? null : (
             <div className="mainView-snapLines">
                 <svg style={{ width: '100%', height: '100%' }}>
@@ -1010,14 +1001,22 @@ export class MainView extends ObservableReactComponent<{}> {
         );
     }
 
+    togglePropertiesFlyout = () => {
+        if (MainView.Instance.propertiesWidth() > 0) {
+            SnappingManager.SetPropertiesWidth(0);
+        } else {
+            SnappingManager.SetPropertiesWidth(300);
+        }
+    };
+
     lightboxMaxBorder = [200, 50];
     render() {
         return (
             <div
                 className="mainView-container"
                 style={{
-                    color: SettingsManager.userColor,
-                    background: SettingsManager.userBackgroundColor,
+                    color: SnappingManager.userColor,
+                    background: SnappingManager.userBackgroundColor,
                 }}
                 onScroll={() =>
                     (ele => {
@@ -1067,7 +1066,7 @@ export class MainView extends ObservableReactComponent<{}> {
                         case 'dashboard':
                         default: return (<>
                                         <div key="dashdiv" style={{ position: 'relative', display: this._hideUI || LightboxView.LightboxDoc ? 'none' : undefined, zIndex: 2001 }}>
-                                            <CollectionMenu panelWidth={this.topMenuWidth} panelHeight={this.topMenuHeight} toggleTopBar={this.toggleTopBar} topBarHeight={this.headerBarHeightFunc}/>
+                                            <CollectionMenu panelWidth={this.topMenuWidth} panelHeight={this.topMenuHeight} togglePropertiesFlyout={this.togglePropertiesFlyout} toggleTopBar={this.toggleTopBar} topBarHeight={this.headerBarHeightFunc}/>
                                         </div>
                                         {this.mainDashboardArea}
                                     </> );
@@ -1086,7 +1085,7 @@ export class MainView extends ObservableReactComponent<{}> {
                 {/* <InkTranscription /> */}
                 {this.snapLines}
                 <LightboxView key="lightbox" PanelWidth={this._windowWidth} PanelHeight={this._windowHeight} maxBorder={this.lightboxMaxBorder} />
-                <OverlayView />
+                {LightboxView.LightboxDoc ? null : <OverlayView />}
                 <GPTPopup key="gptpopup" />
                 <SchemaCSVPopUp key="schemacsvpopup" />
                 <GenerativeFill imageEditorOpen={ImageEditor.Open} imageEditorSource={ImageEditor.Source} imageRootDoc={ImageEditor.RootDoc} addDoc={ImageEditor.AddDoc} />
diff --git a/src/client/views/MarqueeAnnotator.tsx b/src/client/views/MarqueeAnnotator.tsx
index 0f13de9e5..a917a7d57 100644
--- a/src/client/views/MarqueeAnnotator.tsx
+++ b/src/client/views/MarqueeAnnotator.tsx
@@ -227,7 +227,6 @@ export class MarqueeAnnotator extends ObservableReactComponent<MarqueeAnnotatorP
                               const linkDocData = dragEx.linkDocument[DocData];
                               linkDocData.link_relationship = 'cropped image';
                               linkDocData.title = 'crop: ' + this.props.Document.title;
-                              linkDocData.link_displayLine = false;
                           }
                       },
                   });
diff --git a/src/client/views/OverlayView.tsx b/src/client/views/OverlayView.tsx
index e17da2680..a7907a565 100644
--- a/src/client/views/OverlayView.tsx
+++ b/src/client/views/OverlayView.tsx
@@ -13,7 +13,6 @@ import { DocumentType } from '../documents/DocumentTypes';
 import { DragManager } from '../util/DragManager';
 import { dropActionType } from '../util/DropActionTypes';
 import { Transform } from '../util/Transform';
-import { LightboxView } from './LightboxView';
 import { ObservableReactComponent } from './ObservableReactComponent';
 import './OverlayView.scss';
 import { DefaultStyleProvider } from './StyleProvider';
@@ -94,7 +93,7 @@ export class OverlayWindow extends ObservableReactComponent<OverlayWindowProps>
     };
 
     render() {
-        return LightboxView.LightboxDoc ? null : (
+        return (
             <div className="overlayWindow-outerDiv" style={{ transform: `translate(${this.x}px, ${this.y}px)`, width: this.width, height: this.height }}>
                 <div className="overlayWindow-titleBar" onPointerDown={this.onPointerDown}>
                     {this._props.overlayOptions.title || 'Untitled'}
@@ -181,7 +180,7 @@ export class OverlayView extends ObservableReactComponent<{}> {
     docScreenToLocalXf = computedFn((doc: Doc) => () => new Transform(-NumCast(doc.overlayX), -NumCast(doc.overlayY), 1));
 
     @computed get overlayDocs() {
-        return Doc.MyOverlayDocs.filter(d => !LightboxView.LightboxDoc || d.type === DocumentType.PRES).map(d => {
+        return Doc.MyOverlayDocs.map(d => {
             let [offsetx, offsety] = [0, 0];
             const dref = React.createRef<HTMLDivElement>();
             const onPointerMove = action((e: PointerEvent, down: number[]) => {
diff --git a/src/client/views/PropertiesButtons.tsx b/src/client/views/PropertiesButtons.tsx
index cbbf48c75..edf6df2b9 100644
--- a/src/client/views/PropertiesButtons.tsx
+++ b/src/client/views/PropertiesButtons.tsx
@@ -20,11 +20,8 @@ import { DocData } from '../../fields/DocSymbols';
 import { ScriptField } from '../../fields/ScriptField';
 import { BoolCast, ScriptCast } from '../../fields/Types';
 import { ImageField } from '../../fields/URLField';
-import { DocUtils } from '../documents/DocUtils';
+import { DocUtils, IsFollowLinkScript } from '../documents/DocUtils';
 import { CollectionViewType, DocumentType } from '../documents/DocumentTypes';
-import { IsFollowLinkScript } from '../util/LinkFollower';
-import { LinkManager } from '../util/LinkManager';
-import { SelectionManager } from '../util/SelectionManager';
 import { SettingsManager } from '../util/SettingsManager';
 import { undoBatch, undoable } from '../util/UndoManager';
 import { InkingStroke } from './InkingStroke';
@@ -40,13 +37,13 @@ export class PropertiesButtons extends React.Component<{}, {}> {
     @observable public static Instance: PropertiesButtons;
 
     @computed get selectedDoc() {
-        return SelectionManager.SelectedSchemaDoc || SelectionManager.Views.lastElement()?.Document;
+        return DocumentView.SelectedSchemaDoc() || DocumentView.Selected().lastElement()?.Document;
     }
     @computed get selectedLayoutDoc() {
-        return SelectionManager.SelectedSchemaDoc || SelectionManager.Views.lastElement()?.layoutDoc;
+        return DocumentView.SelectedSchemaDoc() || DocumentView.Selected().lastElement()?.layoutDoc;
     }
     @computed get selectedTabView() {
-        return !SelectionManager.SelectedSchemaDoc && SelectionManager.Views.lastElement()?.topMost;
+        return !DocumentView.SelectedSchemaDoc() && DocumentView.Selected().lastElement()?.topMost;
     }
 
     @computed get titleButton() {
@@ -164,7 +161,7 @@ export class PropertiesButtons extends React.Component<{}, {}> {
     //         on => `${on ? 'Set' : 'Remove'} lightbox flag`,
     //         on => 'window-restore',
     //         onClick => {
-    //             SelectionManager.Views.forEach(dv => {
+    //             DocumentView.Selected().forEach(dv => {
     //                 const containerDoc = dv.Document;
     //                 //containerDoc.followAllLinks =
     //                 // containerDoc.noShadow =
@@ -320,14 +317,14 @@ export class PropertiesButtons extends React.Component<{}, {}> {
     @undoBatch
     handlePerspectiveChange = (e: any) => {
         this.selectedDoc && (this.selectedDoc._type_collection = e.target.value);
-        SelectionManager.Views.forEach(docView => {
+        DocumentView.Selected().forEach(docView => {
             docView.layoutDoc._type_collection = e.target.value;
         });
     };
     @computed get onClickVal() {
         const linkButton = IsFollowLinkScript(this.selectedDoc.onClick);
         const followLoc = this.selectedDoc._followLinkLocation;
-        const linkedToLightboxView = () => LinkManager.Links(this.selectedDoc).some(link => LinkManager.getOppositeAnchor(link, this.selectedDoc)?._isLightbox);
+        const linkedToLightboxView = () => Doc.Links(this.selectedDoc).some(link => Doc.getOppositeAnchor(link, this.selectedDoc)?._isLightbox);
 
         if (followLoc === OpenWhere.lightbox && !linkedToLightboxView()) return 'linkInPlace';
         if (linkButton && followLoc === OpenWhere.addRight) return 'linkOnRight';
@@ -381,7 +378,7 @@ export class PropertiesButtons extends React.Component<{}, {}> {
     @undoBatch
     @action
     handleOptionChange = (onClick: string) => {
-        SelectionManager.Views.forEach(docView => {
+        DocumentView.Selected().forEach(docView => {
             const linkButton = IsFollowLinkScript(docView.Document.onClick);
             docView.noOnClick();
             switch (onClick) {
@@ -417,7 +414,7 @@ export class PropertiesButtons extends React.Component<{}, {}> {
             const click = () => this.handleOptionChange(value[0]);
             const linkButton = IsFollowLinkScript(this.selectedDoc.onClick);
             const followLoc = this.selectedDoc._followLinkLocation;
-            const linkedToLightboxView = () => LinkManager.Links(this.selectedDoc).some(link => LinkManager.getOppositeAnchor(link, this.selectedDoc)?._isLightbox);
+            const linkedToLightboxView = () => Doc.Links(this.selectedDoc).some(link => Doc.getOppositeAnchor(link, this.selectedDoc)?._isLightbox);
 
             let active = false;
             // prettier-ignore
@@ -451,7 +448,7 @@ export class PropertiesButtons extends React.Component<{}, {}> {
     }
     @undoBatch
     editOnClickScript = () => {
-        if (SelectionManager.Views.length) SelectionManager.Views.forEach(dv => DocUtils.makeCustomViewClicked(dv.Document, undefined, 'onClick'));
+        if (DocumentView.Selected().length) DocumentView.Selected().forEach(dv => DocUtils.makeCustomViewClicked(dv.Document, undefined, 'onClick'));
         else this.selectedDoc && DocUtils.makeCustomViewClicked(this.selectedDoc, undefined, 'onClick');
     };
 
@@ -472,8 +469,8 @@ export class PropertiesButtons extends React.Component<{}, {}> {
                 fillWidth
                 toggleType={ToggleType.BUTTON}
                 onClick={undoable(() => {
-                    if (SelectionManager.Views.length > 1) {
-                        SelectionManager.Views.forEach(dv => (onClick ?? onPropToggle)(dv, dv.Document, property));
+                    if (DocumentView.Selected().length > 1) {
+                        DocumentView.Selected().forEach(dv => (onClick ?? onPropToggle)(dv, dv.Document, property));
                     } else if (targetDoc) (onClick ?? onPropToggle)(undefined, targetDoc, property);
                 }, property)}
             />
@@ -482,7 +479,7 @@ export class PropertiesButtons extends React.Component<{}, {}> {
 
     render() {
         const layoutField = this.selectedDoc?.[Doc.LayoutFieldKey(this.selectedDoc)];
-        const isText = SelectionManager.Views.lastElement()?.ComponentView instanceof FormattedTextBox;
+        const isText = DocumentView.Selected().lastElement()?.ComponentView instanceof FormattedTextBox;
         const isInk = this.selectedDoc?.layout_isSvg;
         const isImage = layoutField instanceof ImageField;
         const isMap = this.selectedDoc?.type === DocumentType.MAP;
diff --git a/src/client/views/PropertiesDocBacklinksSelector.tsx b/src/client/views/PropertiesDocBacklinksSelector.tsx
index a806995f2..edb55f341 100644
--- a/src/client/views/PropertiesDocBacklinksSelector.tsx
+++ b/src/client/views/PropertiesDocBacklinksSelector.tsx
@@ -7,12 +7,12 @@ import { Doc } from '../../fields/Doc';
 import { Cast } from '../../fields/Types';
 import { DocumentType } from '../documents/DocumentTypes';
 import { LinkManager } from '../util/LinkManager';
-import { SelectionManager } from '../util/SelectionManager';
 import { SettingsManager } from '../util/SettingsManager';
 import './PropertiesDocBacklinksSelector.scss';
 import { CollectionDockingView } from './collections/CollectionDockingView';
 import { LinkMenu } from './linking/LinkMenu';
 import { OpenWhere } from './nodes/OpenWhere';
+import { DocumentView } from './nodes/DocumentView';
 
 type PropertiesDocBacklinksSelectorProps = {
     Document: Doc;
@@ -25,7 +25,7 @@ type PropertiesDocBacklinksSelectorProps = {
 export class PropertiesDocBacklinksSelector extends React.Component<PropertiesDocBacklinksSelectorProps> {
     getOnClick = action((link: Doc) => {
         const linkAnchor = this.props.Document;
-        const other = LinkManager.getOppositeAnchor(link, linkAnchor);
+        const other = Doc.getOppositeAnchor(link, linkAnchor);
         const otherdoc = !other ? undefined : other.annotationOn && other.type !== DocumentType.RTF ? Cast(other.annotationOn, Doc, null) : other;
         LinkManager.Instance.currentLink = link;
         if (otherdoc) {
@@ -36,10 +36,10 @@ export class PropertiesDocBacklinksSelector extends React.Component<PropertiesDo
     });
 
     render() {
-        return !SelectionManager.Views.length ? null : (
+        return !DocumentView.Selected().length ? null : (
             <div className="propertiesDocBacklinksSelector" style={{ color: SettingsManager.userColor, backgroundColor: SettingsManager.userBackgroundColor }}>
                 {this.props.hideTitle ? null : <p key="contexts">Contexts:</p>}
-                <LinkMenu docView={SelectionManager.Views.lastElement()} clearLinkEditor={undefined} itemHandler={this.getOnClick} style={{ left: 0, top: 0 }} />
+                <LinkMenu docView={DocumentView.Selected().lastElement()} clearLinkEditor={undefined} itemHandler={this.getOnClick} style={{ left: 0, top: 0 }} />
             </div>
         );
     }
diff --git a/src/client/views/PropertiesDocContextSelector.tsx b/src/client/views/PropertiesDocContextSelector.tsx
index 722b1f90a..7465c727a 100644
--- a/src/client/views/PropertiesDocContextSelector.tsx
+++ b/src/client/views/PropertiesDocContextSelector.tsx
@@ -7,11 +7,10 @@ import * as React from 'react';
 import { Doc } from '../../fields/Doc';
 import { Id } from '../../fields/FieldSymbols';
 import { Cast, StrCast } from '../../fields/Types';
-import { DocFocusOrOpen } from '../util/DocumentManager';
 import { ObservableReactComponent } from './ObservableReactComponent';
 import './PropertiesDocContextSelector.scss';
 import { CollectionDockingView } from './collections/CollectionDockingView';
-import { DocumentView } from './nodes/DocumentView';
+import { DocFocusOrOpen, DocumentView } from './nodes/DocumentView';
 import { OpenWhere } from './nodes/OpenWhere';
 
 type PropertiesDocContextSelectorProps = {
diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx
index ff5dcd1b8..7c16e0ddb 100644
--- a/src/client/views/PropertiesView.tsx
+++ b/src/client/views/PropertiesView.tsx
@@ -23,10 +23,8 @@ import { ComputedField } from '../../fields/ScriptField';
 import { Cast, DocCast, NumCast, StrCast } from '../../fields/Types';
 import { GetEffectiveAcl, SharingPermissions, normalizeEmail } from '../../fields/util';
 import { CollectionViewType, DocumentType } from '../documents/DocumentTypes';
-import { DocumentManager } from '../util/DocumentManager';
 import { GroupManager } from '../util/GroupManager';
 import { LinkManager } from '../util/LinkManager';
-import { SelectionManager } from '../util/SelectionManager';
 import { SettingsManager } from '../util/SettingsManager';
 import { SharingManager } from '../util/SharingManager';
 import { Transform } from '../util/Transform';
@@ -40,7 +38,7 @@ import { PropertiesDocBacklinksSelector } from './PropertiesDocBacklinksSelector
 import { PropertiesDocContextSelector } from './PropertiesDocContextSelector';
 import { PropertiesSection } from './PropertiesSection';
 import './PropertiesView.scss';
-import { DefaultStyleProvider } from './StyleProvider';
+import { DefaultStyleProvider, SetFilterOpener as SetPropertiesFilterOpener } from './StyleProvider';
 import { DocumentView } from './nodes/DocumentView';
 import { StyleProviderFuncType } from './nodes/FieldView';
 import { KeyValueBox } from './nodes/KeyValueBox';
@@ -66,6 +64,12 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
         super(props);
         makeObservable(this);
         PropertiesView.Instance = this;
+        SetPropertiesFilterOpener(
+            action(() => {
+                this.CloseAll();
+                this.openFilters = true;
+            })
+        );
     }
 
     @computed get MAX_EMBED_HEIGHT() {
@@ -73,7 +77,7 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
     }
 
     @computed get selectedDoc() {
-        return SelectionManager.SelectedSchemaDoc || this.selectedDocumentView?.Document || Doc.ActiveDashboard;
+        return DocumentView.SelectedSchemaDoc() || this.selectedDocumentView?.Document || Doc.ActiveDashboard;
     }
 
     @computed get selectedLink() {
@@ -81,12 +85,10 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
     }
 
     @computed get selectedLayoutDoc() {
-        return SelectionManager.SelectedSchemaDoc || this.selectedDocumentView?.layoutDoc || Doc.ActiveDashboard;
+        return DocumentView.SelectedSchemaDoc() || this.selectedDocumentView?.layoutDoc || Doc.ActiveDashboard;
     }
     @computed get selectedDocumentView() {
-        if (SelectionManager.Views.length) return SelectionManager.Views[0];
-        if (PresBox.Instance?.selectedArray.size) return DocumentManager.Instance.getDocumentView(PresBox.Instance.Document);
-        return undefined;
+        return DocumentView.Selected().lastElement();
     }
     @computed get isPres(): boolean {
         return this.selectedDoc?.type === DocumentType.PRES;
@@ -182,7 +184,10 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
         const rows: JSX.Element[] = [];
         if (this.dataDoc && this.selectedDoc) {
             const ids = new Set<string>(reqdKeys);
-            const docs: Doc[] = SelectionManager.Views.length < 2 ? [this.layoutFields ? Doc.Layout(this.selectedDoc) : this.dataDoc] : SelectionManager.Views.map(dv => (this.layoutFields ? dv.layoutDoc : dv.dataDoc));
+            const docs: Doc[] =
+                DocumentView.Selected().length < 2 //
+                    ? [this.layoutFields ? Doc.Layout(this.selectedDoc) : this.dataDoc]
+                    : DocumentView.Selected().map(dv => (this.layoutFields ? dv.layoutDoc : dv.dataDoc));
             docs.forEach(doc =>
                 Object.keys(doc)
                     .filter(filter)
@@ -237,7 +242,12 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
 
     @undoBatch
     setKeyValue = (value: string) => {
-        const docs = SelectionManager.Views.length < 2 && this.selectedDoc ? [this.layoutFields ? Doc.Layout(this.selectedDoc) : this.dataDoc!] : SelectionManager.Views.map(dv => (this.layoutFields ? dv.layoutDoc : dv.dataDoc));
+        const docs =
+            DocumentView.Selected().length < 2 && this.selectedDoc
+                ? [this.layoutFields
+                        ? Doc.Layout(this.selectedDoc) //
+                        : this.dataDoc!]
+                : DocumentView.Selected().map(dv => (this.layoutFields ? dv.layoutDoc : dv.dataDoc)); // prettier-ignore
         docs.forEach(doc => {
             if (value.indexOf(':') !== -1) {
                 const newVal = value[0].toUpperCase() + value.substring(1, value.length);
@@ -295,13 +305,13 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
         const selAnchor = this.selectedDocumentView?.anchorViewDoc ?? LinkManager.Instance.currentLinkAnchor ?? this.selectedDoc;
         let counter = 0;
 
-        LinkManager.Links(selAnchor).forEach(() => counter++);
+        Doc.Links(selAnchor).forEach(() => counter++);
 
         return counter;
     }
 
     @computed get layoutPreview() {
-        if (SelectionManager.Views.length > 1) {
+        if (DocumentView.Selected().length > 1) {
             return '-- multiple selected --';
         }
         if (this.selectedDoc) {
@@ -348,7 +358,7 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
      */
     @undoBatch
     changePermissions = (e: any, user: string) => {
-        const docs = SelectionManager.Views.length < 2 ? [this.selectedDoc] : SelectionManager.Views.map(dv => (this.layoutDocAcls ? dv.layoutDoc : dv.dataDoc));
+        const docs = DocumentView.Selected().length < 2 ? [this.selectedDoc] : DocumentView.Selected().map(dv => (this.layoutDocAcls ? dv.layoutDoc : dv.dataDoc));
         SharingManager.Instance.shareFromPropertiesSidebar(user, e.currentTarget.value as SharingPermissions, docs, this.layoutDocAcls);
     };
 
@@ -462,7 +472,7 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
      */
     @computed get sharingTable() {
         // all selected docs
-        const docs = SelectionManager.Views.length < 2 && this.selectedDoc ? [this.selectedDoc] : SelectionManager.Views.map(docView => docView.Document);
+        const docs = DocumentView.Selected().length < 2 && this.selectedDoc ? [this.selectedDoc] : DocumentView.SelectedDocs();
         const target = docs[0];
 
         const showAdmin = GetEffectiveAcl(target) === AclAdmin;
@@ -580,7 +590,7 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
 
     @computed get editableTitle() {
         const titles = new Set<string>();
-        SelectionManager.Views.forEach(dv => titles.add(StrCast(dv.Document.title)));
+        DocumentView.Selected().forEach(dv => titles.add(StrCast(dv.Document.title)));
         const title = Array.from(titles.keys()).length > 1 ? '--multiple selected--' : StrCast(this.selectedDoc?.title);
         return (
             <div>
@@ -631,8 +641,8 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
 
     @undoBatch
     setTitle = (value: string | number) => {
-        if (SelectionManager.Views.length > 1) {
-            SelectionManager.Views.map(dv => Doc.SetInPlace(dv.Document, 'title', value, true));
+        if (DocumentView.Selected().length > 1) {
+            DocumentView.Selected().map(dv => Doc.SetInPlace(dv.Document, 'title', value, true));
         } else if (this.dataDoc) {
             if (this.selectedDoc) Doc.SetInPlace(this.selectedDoc, 'title', value, true);
             else KeyValueBox.SetField(this.dataDoc, 'title', value as string, true);
@@ -1365,14 +1375,14 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
     @computed get destinationAnchor() {
         const ldoc = this.selectedLink;
         const lanch = this.selectedDocumentView?.anchorViewDoc ?? LinkManager.Instance.currentLinkAnchor;
-        if (ldoc && lanch) return LinkManager.getOppositeAnchor(ldoc, lanch) ?? lanch;
+        if (ldoc && lanch) return Doc.getOppositeAnchor(ldoc, lanch) ?? lanch;
         return ldoc ? DocCast(ldoc.link_anchor_2) : ldoc;
     }
 
     @computed get sourceAnchor() {
         const selAnchor = this.selectedDocumentView?.anchorViewDoc ?? LinkManager.Instance.currentLinkAnchor;
 
-        return selAnchor ?? (this.selectedLink && this.destinationAnchor ? LinkManager.getOppositeAnchor(this.selectedLink, this.destinationAnchor) : this.selectedLink);
+        return selAnchor ?? (this.selectedLink && this.destinationAnchor ? Doc.getOppositeAnchor(this.selectedLink, this.destinationAnchor) : this.selectedLink);
     }
 
     toggleAnchorProp = (e: React.PointerEvent, prop: string, anchor?: Doc, value: any = true, ovalue: any = false, cb: (val: any) => any = val => val) => {
@@ -1435,7 +1445,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 = this.selectedLink !== this.selectedDoc && LinkManager.Links(this.sourceAnchor).includes(this.selectedLink!);
+        const hasSelectedAnchor = this.selectedLink !== this.selectedDoc && Doc.Links(this.sourceAnchor).includes(this.selectedLink!);
 
         return (
             <>
diff --git a/src/client/views/ScriptBox.tsx b/src/client/views/ScriptBox.tsx
index 8de359e07..9c36e6d26 100644
--- a/src/client/views/ScriptBox.tsx
+++ b/src/client/views/ScriptBox.tsx
@@ -122,14 +122,14 @@ export class ScriptBox extends React.Component<ScriptBoxProps> {
                     if (!text) {
                         doc[DocData][fieldKey] = undefined;
                     } else {
-                        const script = CompileScript(text, {
+                        const compScript = CompileScript(text, {
                             params: { this: Doc.name, ...contextParams },
                             typecheck: false,
                             editable: true,
                             transformer: DocumentIconContainer.getTransformer(),
                         });
-                        if (!script.compiled) {
-                            onError(script.errors.map(error => error.messageText).join('\n'));
+                        if (!compScript.compiled) {
+                            onError(compScript.errors.map(error => error.messageText).join('\n'));
                             return;
                         }
 
@@ -143,7 +143,7 @@ export class ScriptBox extends React.Component<ScriptBoxProps> {
                         div.innerHTML = 'button';
                         params.length && DragManager.StartButtonDrag([div], text, doc.title + '-instance', {}, params, () => {}, clientX, clientY);
 
-                        doc[DocData][fieldKey] = new ScriptField(script);
+                        doc[DocData][fieldKey] = new ScriptField(compScript);
                         overlayDisposer();
                     }
                 }}
diff --git a/src/client/views/ScriptingRepl.tsx b/src/client/views/ScriptingRepl.tsx
index ba2e22b3b..1a2eb460f 100644
--- a/src/client/views/ScriptingRepl.tsx
+++ b/src/client/views/ScriptingRepl.tsx
@@ -5,15 +5,15 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { action, makeObservable, observable } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
-import { DocumentManager } from '../util/DocumentManager';
 import { CompileScript, Transformer, ts } from '../util/Scripting';
 import { ScriptingGlobals } from '../util/ScriptingGlobals';
-import { SettingsManager } from '../util/SettingsManager';
+import { SnappingManager } from '../util/SnappingManager';
 import { undoable } from '../util/UndoManager';
 import { ObservableReactComponent } from './ObservableReactComponent';
 import { OverlayView } from './OverlayView';
 import './ScriptingRepl.scss';
 import { DocumentIconContainer } from './nodes/DocumentIcon';
+import { DocumentView } from './nodes/DocumentView';
 
 interface replValueProps {
     scrollToBottom: () => void;
@@ -169,7 +169,7 @@ export class ScriptingRepl extends ObservableReactComponent<{}> {
             case 'Enter': {
                 e.stopPropagation();
                 const docGlobals: { [name: string]: any } = {};
-                DocumentManager.Instance.DocumentViews.forEach((dv, i) => {
+                DocumentView.allViews().forEach((dv, i) => {
                     docGlobals[`d${i}`] = dv.Document;
                 });
                 const globals = ScriptingGlobals.makeMutableGlobalsCopy(docGlobals);
@@ -262,13 +262,13 @@ export class ScriptingRepl extends ObservableReactComponent<{}> {
     render() {
         return (
             <div className="scriptingRepl-outerContainer">
-                <div className="scriptingRepl-commandsContainer" style={{ background: SettingsManager.userBackgroundColor }} ref={this.commandsRef}>
+                <div className="scriptingRepl-commandsContainer" style={{ background: SnappingManager.userBackgroundColor }} ref={this.commandsRef}>
                     {this.commands.map(({ command, result }, i) => (
-                        <div className="scriptingRepl-resultContainer" style={{ background: SettingsManager.userBackgroundColor }} key={i}>
-                            <div className="scriptingRepl-commandString" style={{ background: SettingsManager.userBackgroundColor }}>
+                        <div className="scriptingRepl-resultContainer" style={{ background: SnappingManager.userBackgroundColor }} key={i}>
+                            <div className="scriptingRepl-commandString" style={{ background: SnappingManager.userBackgroundColor }}>
                                 {command || <br />}
                             </div>
-                            <div className="scriptingRepl-commandResult" style={{ background: SettingsManager.userBackgroundColor }}>
+                            <div className="scriptingRepl-commandResult" style={{ background: SnappingManager.userBackgroundColor }}>
                                 <ScriptingValueDisplay scrollToBottom={this.maybeScrollToBottom} value={result} />
                             </div>
                         </div>
@@ -276,7 +276,7 @@ export class ScriptingRepl extends ObservableReactComponent<{}> {
                 </div>
                 <input
                     className="scriptingRepl-commandInput"
-                    style={{ background: SettingsManager.userBackgroundColor }} //
+                    style={{ background: SnappingManager.userBackgroundColor }} //
                     onFocus={this.onFocus}
                     onBlur={this.onBlur}
                     value={this.commandString}
diff --git a/src/client/views/SidebarAnnos.tsx b/src/client/views/SidebarAnnos.tsx
index 3648e613d..9b70f1ca7 100644
--- a/src/client/views/SidebarAnnos.tsx
+++ b/src/client/views/SidebarAnnos.tsx
@@ -14,12 +14,11 @@ import { DocCast, NumCast, StrCast } from '../../fields/Types';
 import { CollectionViewType, DocumentType } from '../documents/DocumentTypes';
 import { Docs } from '../documents/Documents';
 import { DocUtils } from '../documents/DocUtils';
-import { LinkManager } from '../util/LinkManager';
 import { SearchUtil } from '../util/SearchUtil';
 import { Transform } from '../util/Transform';
 import { ObservableReactComponent } from './ObservableReactComponent';
 import './SidebarAnnos.scss';
-import { StyleProp } from './StyleProvider';
+import { StyleProp } from './StyleProp';
 import { CollectionStackingView } from './collections/CollectionStackingView';
 import { FieldViewProps } from './nodes/FieldView';
 import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox';
@@ -87,7 +86,6 @@ export class SidebarAnnos extends ObservableReactComponent<FieldViewProps & Extr
         Doc.SetSelectOnLoad(target);
         FormattedTextBox.DontSelectInitialText = true;
         const link = DocUtils.MakeLink(anchor, target, { link_relationship: 'inline comment:comment on' });
-        link && (link.link_displayLine = false);
 
         const taggedContent = this.childFilters()
             .filter(data => data.split(':')[0])
@@ -187,8 +185,8 @@ export class SidebarAnnos extends ObservableReactComponent<FieldViewProps & Extr
     layout_showTitle = () => 'title';
     setHeightCallback = (height: number) => this._props.setHeight?.(height + this.filtersHeight());
     sortByLinkAnchorY = (a: Doc, b: Doc) => {
-        const ay = LinkManager.Links(a).length && DocCast(LinkManager.Links(a)[0].link_anchor_1).y;
-        const by = LinkManager.Links(b).length && DocCast(LinkManager.Links(b)[0].link_anchor_1).y;
+        const ay = Doc.Links(a).length && DocCast(Doc.Links(a)[0].link_anchor_1).y;
+        const by = Doc.Links(b).length && DocCast(Doc.Links(b)[0].link_anchor_1).y;
         return NumCast(ay) - NumCast(by);
     };
     render() {
diff --git a/src/client/views/StyleProp.ts b/src/client/views/StyleProp.ts
new file mode 100644
index 000000000..dd5b98cfe
--- /dev/null
+++ b/src/client/views/StyleProp.ts
@@ -0,0 +1,24 @@
+export enum StyleProp {
+    TreeViewIcon = 'treeView_Icon',
+    TreeViewSortings = 'treeView_Sortings', // options for how to sort tree view items
+    DocContents = 'docContents', // when specified, the JSX returned will replace the normal rendering of the document view
+    Opacity = 'opacity', // opacity of the document view
+    BoxShadow = 'boxShadow', // box shadow - used for making collections standout and for showing clusters in free form views
+    BorderRounding = 'borderRounding', // border radius of the document view
+    Color = 'color', // foreground color of Document view items
+    BackgroundColor = 'backgroundColor', // background color of a document view
+    FillColor = 'fillColor', // fill color of an ink stroke or shape
+    WidgetColor = 'widgetColor', // color to display UI widgets on a document view -- used for the sidebar divider dragger on a text note
+    PointerEvents = 'pointerEvents', // pointer events for DocumentView -- inherits pointer events if not specified
+    Decorations = 'decorations', // additional decoration to display above a DocumentView -- currently only used to display a Lock for making things background
+    HeaderMargin = 'headerMargin', // margin at top of documentview, typically for displaying a title -- doc contents will start below that
+    ShowCaption = 'layout_showCaption',
+    TitleHeight = 'titleHeight', // Height of Title area
+    ShowTitle = 'layout_showTitle', // whether to display a title on a Document (optional :hover suffix)
+    BorderPath = 'customBorder', // border path for document view
+    FontColor = 'fontColor', // color o tet
+    FontSize = 'fontSize', // size of text font
+    FontFamily = 'fontFamily', // font family of text
+    FontWeight = 'fontWeight', // font weight of text
+    Highlighting = 'highlighting', // border highlighting
+}
diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx
index 6d4f69e6e..3ecfc8ba3 100644
--- a/src/client/views/StyleProvider.tsx
+++ b/src/client/views/StyleProvider.tsx
@@ -13,53 +13,20 @@ import { FaFilter } from 'react-icons/fa';
 import { ClientUtils, DashColor, lightOrDark } from '../../ClientUtils';
 import { Doc, Opt, StrListCast } from '../../fields/Doc';
 import { DocViews } from '../../fields/DocSymbols';
+import { Id } from '../../fields/FieldSymbols';
+import { ScriptField } from '../../fields/ScriptField';
 import { BoolCast, Cast, DocCast, ImageCast, NumCast, ScriptCast, StrCast } from '../../fields/Types';
+import { AudioAnnoState } from '../../server/SharedMediaTypes';
 import { CollectionViewType, DocumentType } from '../documents/DocumentTypes';
-import { DocFocusOrOpen, DocumentManager } from '../util/DocumentManager';
-import { IsFollowLinkScript } from '../util/LinkFollower';
-import { LinkManager } from '../util/LinkManager';
-import { SettingsManager } from '../util/SettingsManager';
+import { IsFollowLinkScript } from '../documents/DocUtils';
 import { SnappingManager } from '../util/SnappingManager';
 import { undoBatch, UndoManager } from '../util/UndoManager';
-import { CollectionSchemaView } from './collections/collectionSchema/CollectionSchemaView';
 import { TreeSort } from './collections/TreeSort';
 import { Colors } from './global/globalEnums';
-import { DocumentViewProps } from './nodes/DocumentView';
+import { DocFocusOrOpen, DocumentView, DocumentViewProps } from './nodes/DocumentView';
 import { FieldViewProps } from './nodes/FieldView';
-import { KeyValueBox } from './nodes/KeyValueBox';
-import { PropertiesView } from './PropertiesView';
+import { StyleProp } from './StyleProp';
 import './StyleProvider.scss';
-import { ScriptField } from '../../fields/ScriptField';
-
-export enum StyleProp {
-    TreeViewIcon = 'treeView_Icon',
-    TreeViewSortings = 'treeView_Sortings', // options for how to sort tree view items
-    DocContents = 'docContents', // when specified, the JSX returned will replace the normal rendering of the document view
-    Opacity = 'opacity', // opacity of the document view
-    BoxShadow = 'boxShadow', // box shadow - used for making collections standout and for showing clusters in free form views
-    BorderRounding = 'borderRounding', // border radius of the document view
-    Color = 'color', // foreground color of Document view items
-    BackgroundColor = 'backgroundColor', // background color of a document view
-    FillColor = 'fillColor', // fill color of an ink stroke or shape
-    WidgetColor = 'widgetColor', // color to display UI widgets on a document view -- used for the sidebar divider dragger on a text note
-    PointerEvents = 'pointerEvents', // pointer events for DocumentView -- inherits pointer events if not specified
-    Decorations = 'decorations', // additional decoration to display above a DocumentView -- currently only used to display a Lock for making things background
-    HeaderMargin = 'headerMargin', // margin at top of documentview, typically for displaying a title -- doc contents will start below that
-    ShowCaption = 'layout_showCaption',
-    TitleHeight = 'titleHeight', // Height of Title area
-    ShowTitle = 'layout_showTitle', // whether to display a title on a Document (optional :hover suffix)
-    BorderPath = 'customBorder', // border path for document view
-    FontColor = 'fontColor', // color o tet
-    FontSize = 'fontSize', // size of text font
-    FontFamily = 'fontFamily', // font family of text
-    FontWeight = 'fontWeight', // font weight of text
-    Highlighting = 'highlighting', // border highlighting
-}
-
-export enum AudioAnnoState {
-    stopped = 'stopped',
-    playing = 'playing',
-}
 
 function toggleLockedPosition(doc: Doc) {
     UndoManager.RunInBatch(() => Doc.toggleLockedPosition(doc), 'toggleBackground');
@@ -80,7 +47,7 @@ export function styleFromLayoutString(doc: Doc, props: FieldViewProps, scale: nu
     const divKeys = ['width', 'height', 'fontSize', 'transform', 'left', 'backgroundColor', 'left', 'right', 'top', 'bottom', 'pointerEvents', 'position'];
     const replacer = (match: any, expr: string) =>
         // bcz: this executes a script to convert a property expression string:  { script }  into a value
-        ScriptField.MakeFunction(expr, { self: Doc.name, this: Doc.name, scale: 'number' })?.script.run({ this: doc, self: doc, scale }).result?.toString() ?? '';
+        ScriptField.MakeFunction(expr, { this: Doc.name, scale: 'number' })?.script.run({ this: doc, scale }).result?.toString() ?? '';
     divKeys.forEach((prop: string) => {
         const p = (props as any)[prop];
         typeof p === 'string' && (style[prop] = p?.replace(/{([^.'][^}']+)}/g, replacer));
@@ -98,6 +65,11 @@ export function wavyBorderPath(pw: number, ph: number, inset: number = 0.05) {
     } ${ph * inset}`;
 }
 
+let _filterOpener: () => void;
+export function SetFilterOpener(func: () => void) {
+    _filterOpener = func;
+}
+
 // a preliminary implementation of a dash style sheet for setting rendering properties of documents nested within a Tab
 //
 export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps & DocumentViewProps>, property: string): any {
@@ -124,7 +96,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps &
         childFiltersByRanges,
         renderDepth,
         docViewPath,
-        DocumentView,
+        DocumentView: docView,
         LayoutTemplateString,
         disableBrushing,
         NativeDimScaling,
@@ -177,21 +149,20 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps &
             return  undefined;
         case StyleProp.DocContents:  return undefined;
         case StyleProp.WidgetColor:  return isAnnotated ? Colors.LIGHT_BLUE : 'dimgrey';
-        case StyleProp.Opacity:      return LayoutTemplateString?.includes(KeyValueBox.name) ? 1 : Cast(doc?._opacity, "number", Cast(doc?.opacity, 'number', null));
+        case StyleProp.Opacity:      return docView?.().ComponentView?.isUnstyledView?.() ? 1 : Cast(doc?._opacity, "number", Cast(doc?.opacity, 'number', null));
         case StyleProp.FontColor:    return StrCast(doc?.[fieldKey + 'fontColor'],  StrCast(Doc.UserDoc().fontColor, color()));
         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.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 hideCaptions || doc?._type_collection === CollectionViewType.Carousel ? undefined: StrCast(doc?._layout_showCaption);
-        case StyleProp.TitleHeight:  return Math.min(4,(DocumentView?.().screenToViewTransform().Scale ?? 1)) * NumCast(Doc.UserDoc().headerHeight,30);
+        case StyleProp.TitleHeight:  return Math.min(4,(docView?.().screenToViewTransform().Scale ?? 1)) * NumCast(Doc.UserDoc().headerHeight,30);
         case StyleProp.ShowTitle:
             return (
                 (doc &&
-                    !(DocumentView?.().ComponentView instanceof CollectionSchemaView) &&
+                    !docView?.().ComponentView?.isUnstyledView?.() &&
                     !LayoutTemplateString &&
                     !doc.presentation_targetDoc &&
-                    !LayoutTemplateString?.includes(KeyValueBox.name) &&
                     layout_showTitle?.() !== '' &&
                     StrCast(
                         doc._layout_showTitle,
@@ -205,9 +176,9 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps &
                 ''
             );
         case StyleProp.Color: {
-            if (SettingsManager.Instance.LastPressedBtn === doc) return SettingsManager.userBackgroundColor;
-            if (Doc.IsSystem(doc!)) return SettingsManager.userColor;
-            if (doc?.type === DocumentType.FONTICON) return SettingsManager.userColor;
+            if (SnappingManager.LastPressedBtn === doc?.[Id]) return SnappingManager.userBackgroundColor;
+            if (Doc.IsSystem(doc!)) return SnappingManager.userColor;
+            if (doc?.type === DocumentType.FONTICON) return SnappingManager.userColor;
             const docColor: Opt<string> = StrCast(doc?.[fieldKey + 'color'], StrCast(doc?._color));
             if (docColor) return docColor;
             const backColor = backgroundCol();
@@ -243,7 +214,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps &
                 ? titleHeight()
                 : 0;
         case StyleProp.BackgroundColor: {
-            if (SettingsManager.Instance.LastPressedBtn === doc) return SettingsManager.userColor; // hack to indicate active menu panel item
+            if (SnappingManager.LastPressedBtn === doc?.[Id]) return SnappingManager.userColor; // hack to indicate active menu panel item
             const dataKey = doc ? Doc.LayoutFieldKey(doc) : "";
             const usePath = StrCast(doc?.[dataKey + "_usePath"]);
             const alternate = usePath.includes(":hover") ? ( doc?.isHovering ? '_' + usePath.replace(":hover","") : "") : usePath ? "_" +usePath:usePath;
@@ -268,7 +239,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps &
                 case DocumentType.VID:         docColor = docColor || (Colors.LIGHT_GRAY); break;
                 case DocumentType.COL:
                     docColor = docColor || (doc && Doc.IsSystem(doc)
-                            ? SettingsManager.userBackgroundColor
+                            ? SnappingManager.userBackgroundColor
                             : doc?.annotationOn
                             ? '#00000010' // faint interior for collections on PDFs, images, etc
                             : doc?.isGroup
@@ -288,7 +259,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps &
         case StyleProp.BoxShadow: {
             if (!doc || opacity() === 0 || doc.noShadow) return undefined; // if it's not visible, then no shadow)
             if (doc.layout_boxShadow === 'standard') return Shadows.STANDARD_SHADOW;
-            if (IsFollowLinkScript(doc?.onClick) && LinkManager.Links(doc).length && !layoutDoc?.layout_isSvg) return StrCast(doc?._linkButtonShadow, 'lightblue 0em 0em 1em');
+            if (IsFollowLinkScript(doc?.onClick) && Doc.Links(doc).length && !layoutDoc?.layout_isSvg) return StrCast(doc?._linkButtonShadow, 'lightblue 0em 0em 1em');
             switch (doc?.type) {
                 case DocumentType.COL:
                     return StrCast(
@@ -320,8 +291,8 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps &
             }
         }
         case StyleProp.PointerEvents:
-            if (StrCast(doc?.pointerEvents) && !LayoutTemplateString?.includes(KeyValueBox.name)) return StrCast(doc!.pointerEvents); // honor pointerEvents field (set by lock button usually) if it's not a keyValue view of the Doc
-            if (LayoutTemplateString?.includes(KeyValueBox.name)) return 'all';
+            if (docView?.().ComponentView?.dontRegisterView?.()) return 'all';
+            if (StrCast(doc?.pointerEvents)) return StrCast(doc!.pointerEvents); // honor pointerEvents field (set by lock button usually) if it's not a keyValue view of the Doc
             if (SnappingManager.ExploreMode || doc?.layout_unrendered) return isInk() ? 'visiblePainted' : 'all';
             if (pointerEvents?.() === 'none') return 'none';
             if (opacity() === 0) return 'none';
@@ -336,12 +307,12 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps &
                         </div>
                     );
             const paint = () => !doc?.onPaint ? null : (
-                        <div className={`styleProvider-paint${DocumentView?.().IsSelected ? "-selected":""}`} onClick={e => togglePaintView(e, doc, props)}>
+                        <div className={`styleProvider-paint${docView?.().IsSelected ? "-selected":""}`} onClick={e => togglePaintView(e, doc, props)}>
                             <FontAwesomeIcon icon='pen' size="lg" />
                         </div>
                     );
             const filter = () => {
-                const dashView = untracked(() => DocumentManager.Instance.getDocumentView(Doc.ActiveDashboard));
+                const dashView = untracked(() => DocumentView.getDocumentView(Doc.ActiveDashboard));
                 const showFilterIcon =
                     StrListCast(doc?._childFilters).length || StrListCast(doc?._childFiltersByRanges).length
                         ? 'green' // #18c718bd' //'hasFilter'
@@ -357,17 +328,11 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps &
                             // eslint-disable-next-line react/no-unstable-nested-components
                             iconProvider={() => <div className='styleProvider-filterShift'><FaFilter/></div>}
                             closeOnSelect
-                            setSelectedVal={
-                                action((dv) => {
-                                    (dv as any).select(false);
-                                    (SettingsManager.Instance.propertiesWidth = 250);
-                                    setTimeout(action(() => {
-                                        if (PropertiesView.Instance) {
-                                            PropertiesView.Instance.CloseAll();
-                                            PropertiesView.Instance.openFilters = true;
-                                        }
-                                    }));
-                                })
+                            setSelectedVal={((dv: DocumentView) => {
+                                    dv.select(false);
+                                    SnappingManager.SetPropertiesWidth(250);
+                                    _filterOpener?.();
+                                }) as any // Dropdown assumes values are strings or numbers.. 
                             }
                             size={Size.XSMALL}
                             width={15}
@@ -375,14 +340,14 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps &
                             title={showFilterIcon === 'green' ? 
                                 "This view is filtered.  Click to view/change filters":
                                 "this view inherits filters from one of its parents"}
-                            color={SettingsManager.userColor}
+                            color={SnappingManager.userColor}
                             background={showFilterIcon}
                             items={[ ...(dashView ? [dashView]: []), ...(docViewPath?.()??[])]
                                     .filter(dv => StrListCast(dv?.Document.childFilters).length || StrListCast(dv?.Document.childRangeFilters).length)
                                     .map(dv => ({
                                 text: StrCast(dv?.Document.title),
                                 val: dv as any,
-                                style: {color:SettingsManager.userColor, background:SettingsManager.userBackgroundColor},
+                                style: {color:SnappingManager.userColor, background:SnappingManager.userBackgroundColor},
                             } as IListItemProps)) }
                         />
                     </div>
@@ -395,7 +360,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps &
                 const audioIconColors: { [key: string]: string } = { playing: 'green', stopped: 'blue' };
                 return (
                     <Tooltip title={<div>{StrListCast(doc[fieldKey + 'audioAnnotations_text']).lastElement()}</div>}>
-                        <div className="styleProvider-audio" onPointerDown={() => DocumentManager.Instance.getFirstDocumentView(doc)?.playAnnotation()}>
+                        <div className="styleProvider-audio" onPointerDown={() => DocumentView.getFirstDocumentView(doc)?.playAnnotation()}>
                             <FontAwesomeIcon className="documentView-audioFont" style={{ color: audioIconColors[audioAnnoState(doc)] }} icon='file-audio' size="sm" />
                         </div>
                     </Tooltip>
@@ -415,7 +380,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps &
 }
 
 export function DashboardToggleButton(doc: Doc, field: string, onIcon: IconProp, offIcon: IconProp, clickFunc?: () => void) {
-    const color = SettingsManager.userColor;
+    const color = SnappingManager.userColor;
     return (
         <IconButton
             size={Size.XSMALL}
diff --git a/src/client/views/collections/CollectionCarousel3DView.tsx b/src/client/views/collections/CollectionCarousel3DView.tsx
index d8232c132..7617f2a52 100644
--- a/src/client/views/collections/CollectionCarousel3DView.tsx
+++ b/src/client/views/collections/CollectionCarousel3DView.tsx
@@ -11,8 +11,7 @@ import { Id } from '../../../fields/FieldSymbols';
 import { DocCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
 import { DocumentType } from '../../documents/DocumentTypes';
 import { DragManager } from '../../util/DragManager';
-import { SelectionManager } from '../../util/SelectionManager';
-import { StyleProp } from '../StyleProvider';
+import { StyleProp } from '../StyleProp';
 import { DocumentView } from '../nodes/DocumentView';
 import { FocusViewOptions } from '../nodes/FocusViewOptions';
 import './CollectionCarousel3DView.scss';
@@ -101,7 +100,7 @@ export class CollectionCarousel3DView extends CollectionSubView() {
     }
 
     changeSlide = (direction: number) => {
-        SelectionManager.DeselectAll();
+        DocumentView.DeselectAll();
         this.layoutDoc._carousel_index = (NumCast(this.layoutDoc._carousel_index) + direction + this.carouselItems.length) % this.carouselItems.length;
     };
 
diff --git a/src/client/views/collections/CollectionCarouselView.tsx b/src/client/views/collections/CollectionCarouselView.tsx
index fda320077..9d3657995 100644
--- a/src/client/views/collections/CollectionCarouselView.tsx
+++ b/src/client/views/collections/CollectionCarouselView.tsx
@@ -11,7 +11,7 @@ import { Doc, Opt } from '../../../fields/Doc';
 import { DocCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
 import { DocumentType } from '../../documents/DocumentTypes';
 import { DragManager } from '../../util/DragManager';
-import { StyleProp } from '../StyleProvider';
+import { StyleProp } from '../StyleProp';
 import { DocumentView } from '../nodes/DocumentView';
 import { FieldViewProps } from '../nodes/FieldView';
 import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox';
diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index b98aceb16..fc9e2e39b 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -16,27 +16,33 @@ import { DocServer } from '../../DocServer';
 import { Docs } from '../../documents/Documents';
 import { CollectionViewType, DocumentType } from '../../documents/DocumentTypes';
 import * as GoldenLayout from '../../goldenLayout';
-import { DocumentManager } from '../../util/DocumentManager';
 import { DragManager } from '../../util/DragManager';
 import { InteractionUtils } from '../../util/InteractionUtils';
 import { ScriptingGlobals } from '../../util/ScriptingGlobals';
-import { SelectionManager } from '../../util/SelectionManager';
 import { SnappingManager } from '../../util/SnappingManager';
 import { undoable, undoBatch, UndoManager } from '../../util/UndoManager';
 import { DashboardView } from '../DashboardView';
 import { LightboxView } from '../LightboxView';
+import { DocumentView } from '../nodes/DocumentView';
 import { OpenWhere, OpenWhereMod } from '../nodes/OpenWhere';
 import { OverlayView } from '../OverlayView';
 import { ScriptingRepl } from '../ScriptingRepl';
 import { UndoStack } from '../UndoStack';
 import './CollectionDockingView.scss';
 import { CollectionSubView } from './CollectionSubView';
-import { TabDocView } from './TabDocView';
 
 const _global = (window /* browser */ || global) /* node */ as any;
 
 @observer
 export class CollectionDockingView extends CollectionSubView() {
+    static tabClass: JSX.Element | null = null;
+    /**
+     * Configure golden layout to render its documents using the specified React component
+     * @param ele - typically would be set to TabDocView
+     */
+    static setTabJSXComponent(ele: any) {
+        this.tabClass = ele;
+    }
     // eslint-disable-next-line no-use-before-define
     @observable public static Instance: CollectionDockingView | undefined = undefined;
 
@@ -295,7 +301,7 @@ export class CollectionDockingView extends CollectionSubView() {
             glay.on('tabCreated', this.tabCreated);
             glay.on('tabDestroyed', this.tabDestroyed);
             glay.on('stackCreated', this.stackCreated);
-            glay.registerComponent('DocumentFrameRenderer', TabDocView);
+            glay.registerComponent('DocumentFrameRenderer', CollectionDockingView.tabClass);
             glay.container = this._containerRef.current;
             glay.init();
             glay.root.layoutManager.on('itemDropped', this.tabItemDropped);
@@ -431,8 +437,8 @@ export class CollectionDockingView extends CollectionSubView() {
                 } else {
                     const tabTarget = (e.target as HTMLElement)?.parentElement?.className.includes('lm_tab') ? (e.target as HTMLElement).parentElement : (e.target as HTMLElement);
                     const map = Array.from(this.tabMap).find(tab => tab.element[0] === tabTarget);
-                    if (map?.DashDoc && DocumentManager.Instance.getFirstDocumentView(map.DashDoc)) {
-                        SelectionManager.SelectView(DocumentManager.Instance.getFirstDocumentView(map.DashDoc), false);
+                    if (map?.DashDoc && DocumentView.getFirstDocumentView(map.DashDoc)) {
+                        DocumentView.SelectView(DocumentView.getFirstDocumentView(map.DashDoc), false);
                     }
                 }
             }
diff --git a/src/client/views/collections/CollectionMenu.tsx b/src/client/views/collections/CollectionMenu.tsx
index f945a7aa4..e53071584 100644
--- a/src/client/views/collections/CollectionMenu.tsx
+++ b/src/client/views/collections/CollectionMenu.tsx
@@ -21,13 +21,11 @@ import { BoolCast, Cast, DocCast, NumCast, StrCast } from '../../../fields/Types
 import { CollectionViewType, DocumentType } from '../../documents/DocumentTypes';
 import { DragManager } from '../../util/DragManager';
 import { dropActionType } from '../../util/DropActionTypes';
-import { SelectionManager } from '../../util/SelectionManager';
-import { SettingsManager } from '../../util/SettingsManager';
+import { SnappingManager } from '../../util/SnappingManager';
 import { Transform } from '../../util/Transform';
 import { undoBatch } from '../../util/UndoManager';
 import { AntimodeMenu } from '../AntimodeMenu';
 import { EditableView } from '../EditableView';
-import { MainView } from '../MainView';
 import { DefaultStyleProvider } from '../StyleProvider';
 import { DocumentView, DocumentViewInternal, returnEmptyDocViewList } from '../nodes/DocumentView';
 import './CollectionMenu.scss';
@@ -38,6 +36,7 @@ interface CollectionMenuProps {
     panelWidth: () => number;
     toggleTopBar: () => void;
     topBarHeight: () => number;
+    togglePropertiesFlyout: () => void;
 }
 
 @observer
@@ -59,7 +58,7 @@ export class CollectionMenu extends AntimodeMenu<CollectionMenuProps> {
 
     componentDidMount() {
         reaction(
-            () => SelectionManager.Views.lastElement(),
+            () => DocumentView.Selected().lastElement(),
             view => view && this.SetSelection(view)
         );
     }
@@ -77,15 +76,6 @@ export class CollectionMenu extends AntimodeMenu<CollectionMenuProps> {
         }
     };
 
-    @action
-    toggleProperties = () => {
-        if (MainView.Instance.propertiesWidth() > 0) {
-            SettingsManager.Instance.propertiesWidth = 0;
-        } else {
-            SettingsManager.Instance.propertiesWidth = 300;
-        }
-    };
-
     buttonBarXf = () => {
         if (!this._docBtnRef.current) return Transform.Identity();
         const { scale, translateX, translateY } = ClientUtils.GetScreenTransform(this._docBtnRef.current);
@@ -128,15 +118,15 @@ export class CollectionMenu extends AntimodeMenu<CollectionMenuProps> {
     render() {
         const headerIcon = this.props.topBarHeight() > 0 ? 'angle-double-up' : 'angle-double-down';
         const headerTitle = this.props.topBarHeight() > 0 ? 'Close Header Bar' : 'Open Header Bar';
-        const propIcon = SettingsManager.Instance.propertiesWidth > 0 ? 'angle-double-right' : 'angle-double-left';
-        const propTitle = SettingsManager.Instance.propertiesWidth > 0 ? 'Close Properties' : 'Open Properties';
+        const propIcon = SnappingManager.PropertiesWidth > 0 ? 'angle-double-right' : 'angle-double-left';
+        const propTitle = SnappingManager.PropertiesWidth > 0 ? 'Close Properties' : 'Open Properties';
 
         const hardCodedButtons = (
             <div className="hardCodedButtons">
                 <Toggle
                     toggleType={ToggleType.BUTTON}
                     type={Type.PRIM}
-                    color={SettingsManager.userColor}
+                    color={SnappingManager.userColor}
                     onClick={this.props.toggleTopBar}
                     toggleStatus={this.props.topBarHeight() > 0}
                     icon={<FontAwesomeIcon icon={headerIcon} size="lg" />}
@@ -145,9 +135,9 @@ export class CollectionMenu extends AntimodeMenu<CollectionMenuProps> {
                 <Toggle
                     toggleType={ToggleType.BUTTON}
                     type={Type.PRIM}
-                    color={SettingsManager.userColor}
-                    onClick={this.toggleProperties}
-                    toggleStatus={SettingsManager.Instance.propertiesWidth > 0}
+                    color={SnappingManager.userColor}
+                    onClick={this._props.togglePropertiesFlyout}
+                    toggleStatus={SnappingManager.PropertiesWidth > 0}
                     icon={<FontAwesomeIcon icon={propIcon} size="lg" />}
                     tooltip={propTitle}
                 />
@@ -159,7 +149,7 @@ export class CollectionMenu extends AntimodeMenu<CollectionMenuProps> {
             <div
                 className="collectionMenu-container"
                 style={{
-                    background: SettingsManager.userBackgroundColor,
+                    background: SnappingManager.userBackgroundColor,
                     // borderColor: SettingsManager.userColor
                 }}>
                 {this.contMenuButtons}
@@ -245,7 +235,7 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewMenu
     _viewCommand = {
         params: ['target'],
         title: 'bookmark view',
-        script: "this.target._freeform_panX = self['target-freeform_panX']; this.target._freeform_panY = this['target-freeform_panY']; this.target._freeform_scale = this['target_freeform_scale']; gotoFrame(this.target, this['target-currentFrame']);",
+        script: "this.target._freeform_panX = this.target_freeform_panX; this.target._freeform_panY = this['target-freeform_panY']; this.target._freeform_scale = this['target_freeform_scale']; gotoFrame(this.target, this['target-currentFrame']);",
         immediate: undoBatch(() => {
             this.target._freeform_panX = 0;
             this.target._freeform_panY = 0;
diff --git a/src/client/views/collections/CollectionNoteTakingView.tsx b/src/client/views/collections/CollectionNoteTakingView.tsx
index e940a1aef..3f9eed1d6 100644
--- a/src/client/views/collections/CollectionNoteTakingView.tsx
+++ b/src/client/views/collections/CollectionNoteTakingView.tsx
@@ -27,7 +27,7 @@ import { CollectionFreeFormDocumentView } from '../nodes/CollectionFreeFormDocum
 import { DocumentView } from '../nodes/DocumentView';
 import { FieldViewProps } from '../nodes/FieldView';
 import { FocusViewOptions } from '../nodes/FocusViewOptions';
-import { StyleProp } from '../StyleProvider';
+import { StyleProp } from '../StyleProp';
 import './CollectionNoteTakingView.scss';
 import { CollectionNoteTakingViewColumn } from './CollectionNoteTakingViewColumn';
 import { CollectionNoteTakingViewDivider } from './CollectionNoteTakingViewDivider';
diff --git a/src/client/views/collections/CollectionPileView.tsx b/src/client/views/collections/CollectionPileView.tsx
index e02570d3e..5b3f625db 100644
--- a/src/client/views/collections/CollectionPileView.tsx
+++ b/src/client/views/collections/CollectionPileView.tsx
@@ -10,13 +10,13 @@ import { NumCast, StrCast, toList } from '../../../fields/Types';
 import { emptyFunction } from '../../../Utils';
 import { DocUtils } from '../../documents/DocUtils';
 import { dropActionType } from '../../util/DropActionTypes';
-import { SelectionManager } from '../../util/SelectionManager';
 import { undoBatch, UndoManager } from '../../util/UndoManager';
 import { OpenWhere } from '../nodes/OpenWhere';
 import { computePassLayout, computeStarburstLayout } from './collectionFreeForm';
 import { CollectionFreeFormView } from './collectionFreeForm/CollectionFreeFormView';
 import './CollectionPileView.scss';
 import { CollectionSubView } from './CollectionSubView';
+import { DocumentView } from '../nodes/DocumentView';
 
 @observer
 export class CollectionPileView extends CollectionSubView() {
@@ -150,7 +150,7 @@ export class CollectionPileView extends CollectionSubView() {
     @undoBatch
     onClick = (e: React.MouseEvent) => {
         if (e.button === 0) {
-            SelectionManager.DeselectAll();
+            DocumentView.DeselectAll();
             this.toggleStarburst();
             e.stopPropagation();
         }
diff --git a/src/client/views/collections/CollectionStackedTimeline.tsx b/src/client/views/collections/CollectionStackedTimeline.tsx
index 48d24c910..f9b123bb6 100644
--- a/src/client/views/collections/CollectionStackedTimeline.tsx
+++ b/src/client/views/collections/CollectionStackedTimeline.tsx
@@ -19,11 +19,8 @@ import { ImageField } from '../../../fields/URLField';
 import { emptyFunction, formatTime } from '../../../Utils';
 import { Docs } from '../../documents/Documents';
 import { DocumentType } from '../../documents/DocumentTypes';
-import { FollowLinkScript } from '../../documents/DocUtils';
-import { DocumentManager } from '../../util/DocumentManager';
+import { FollowLinkScript, IsFollowLinkScript } from '../../documents/DocUtils';
 import { DragManager } from '../../util/DragManager';
-import { IsFollowLinkScript, LinkFollower } from '../../util/LinkFollower';
-import { LinkManager } from '../../util/LinkManager';
 import { ScriptingGlobals } from '../../util/ScriptingGlobals';
 import { SnappingManager } from '../../util/SnappingManager';
 import { Transform } from '../../util/Transform';
@@ -87,7 +84,6 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack
     private _timeline: HTMLDivElement | null = null; // ref to actual timeline div
     private _timelineWrapper: HTMLDivElement | null = null; // ref to timeline wrapper div for zooming and scrolling
     private _markerStart: number = 0;
-    @observable public static CurrentlyPlaying: DocumentView[] = [];
     @observable _selectingRegion = false;
     @observable _markerEnd: number | undefined = undefined;
     @observable _trimming: number = TrimScope.None;
@@ -182,7 +178,7 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack
     getView = async (doc: Doc, options: FocusViewOptions): Promise<Opt<DocumentView>> =>
         new Promise<Opt<DocumentView>>(res => {
             if (doc.hidden) options.didMove = !(doc.hidden = false);
-            const findDoc = (finish: (dv: DocumentView) => void) => DocumentManager.Instance.AddViewRenderedCb(doc, dv => finish(dv));
+            const findDoc = (finish: (dv: DocumentView) => void) => DocumentView.addViewRenderedCb(doc, dv => finish(dv));
             findDoc(dv => res(dv));
         });
 
@@ -296,7 +292,7 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack
                     }
                     if (!isClick && Math.abs(movement[0]) > 15 && !this.IsTrimming) {
                         const anchor = CollectionStackedTimeline.createAnchor(this.Document, this.dataDoc, this._props.fieldKey, this._markerStart, this._markerEnd, undefined, true);
-                        setTimeout(() => DocumentManager.Instance.getDocumentView(anchor)?.select(false));
+                        setTimeout(() => DocumentView.getDocumentView(anchor)?.select(false));
                     }
                     (!isClick || !wasSelecting) && (this._markerEnd = undefined);
                     this._timelineWrapper && (this._timelineWrapper.style.cursor = '');
@@ -481,7 +477,7 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack
     @action
     clickAnchor = (anchorDoc: Doc, clientX: number) => {
         if (IsFollowLinkScript(anchorDoc.onClick)) {
-            LinkFollower.FollowLink(undefined, anchorDoc, false);
+            DocumentView.FollowLink(undefined, anchorDoc, false);
         }
         const seekTimeInSeconds = this.anchorStart(anchorDoc) - 0.05;
         const endTime = this.anchorEnd(anchorDoc);
@@ -761,12 +757,12 @@ class StackedTimelineAnchor extends ObservableReactComponent<StackedTimelineAnch
                     // for now, we won't follow any links when the lightbox is oepn to avoid "losing" the video.
                     /* (isDictation || !Doc.AreProtosEqual(LightboxView.LightboxDoc, this._props.layoutDoc)) */
                     !this._props.layoutDoc.dontAutoFollowLinks &&
-                    LinkManager.Links(this._props.mark).length &&
+                    Doc.Links(this._props.mark).length &&
                     time > NumCast(this._props.mark[this._props.startTag]) &&
                     time < NumCast(this._props.mark[this._props.endTag]) &&
                     this._lastTimecode < NumCast(this._props.mark[this._props.startTag]) - 1e-5
                 ) {
-                    LinkFollower.FollowLink(undefined, this._props.mark, false);
+                    DocumentView.FollowLink(undefined, this._props.mark, false);
                 }
                 this._lastTimecode = time;
             }
diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx
index 8ae0f2832..3f12a281e 100644
--- a/src/client/views/collections/CollectionStackingView.tsx
+++ b/src/client/views/collections/CollectionStackingView.tsx
@@ -31,7 +31,7 @@ import { CollectionFreeFormDocumentView } from '../nodes/CollectionFreeFormDocum
 import { DocumentView } from '../nodes/DocumentView';
 import { FieldViewProps } from '../nodes/FieldView';
 import { FocusViewOptions } from '../nodes/FocusViewOptions';
-import { StyleProp } from '../StyleProvider';
+import { StyleProp } from '../StyleProp';
 import { CollectionMasonryViewFieldRow } from './CollectionMasonryViewFieldRow';
 import './CollectionStackingView.scss';
 import { CollectionStackingViewFieldColumn } from './CollectionStackingViewFieldColumn';
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index 7c08aedb1..a4708bed5 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -21,12 +21,11 @@ import { Docs, DocumentOptions } from '../../documents/Documents';
 import { DragManager } from '../../util/DragManager';
 import { dropActionType } from '../../util/DropActionTypes';
 import { ImageUtils } from '../../util/Import & Export/ImageUtils';
-import { SelectionManager } from '../../util/SelectionManager';
 import { SnappingManager } from '../../util/SnappingManager';
 import { UndoManager, undoBatch } from '../../util/UndoManager';
 import { ViewBoxBaseComponent } from '../DocComponent';
 import { FieldViewProps } from '../nodes/FieldView';
-import { LoadingBox } from '../nodes/LoadingBox';
+import { DocumentView } from '../nodes/DocumentView';
 
 export interface CollectionViewProps extends React.PropsWithChildren<FieldViewProps> {
     isAnnotationOverlay?: boolean; // is the collection an annotation overlay (eg an overlay on an image/video/etc)
@@ -377,7 +376,7 @@ export function CollectionSubView<X>() {
                             }
                         });
                     } else {
-                        const srcWeb = SelectionManager.Views.lastElement();
+                        const srcWeb = DocumentView.Selected().lastElement();
                         const srcUrl = (srcWeb?.Document.data as WebField)?.url?.href?.match(/https?:\/\/[^/]*/)?.[0];
                         const reg = new RegExp(ClientUtils.prepend(''), 'g');
                         const modHtml = srcUrl ? html.replace(reg, srcUrl) : html;
@@ -386,7 +385,7 @@ export function CollectionSubView<X>() {
                         Doc.GetProto(htmlDoc)['data-text'] = Doc.GetProto(htmlDoc).text = text;
                         addDocument(htmlDoc);
                         if (srcWeb) {
-                            const iframe = SelectionManager.Views[0].ContentDiv?.getElementsByTagName('iframe')?.[0];
+                            const iframe = DocumentView.Selected()[0].ContentDiv?.getElementsByTagName('iframe')?.[0];
                             const focusNode = iframe?.contentDocument?.getSelection()?.focusNode as any;
                             if (focusNode) {
                                 const anchor = srcWeb?.ComponentView?.getAnchor?.(true);
@@ -496,13 +495,13 @@ export function CollectionSubView<X>() {
             if (typeof files === 'string') {
                 const loading = Docs.Create.LoadingDocument(files, options);
                 generatedDocuments.push(loading);
-                LoadingBox.addCurrentlyLoading(loading);
+                Doc.addCurrentlyLoading(loading);
                 DocUtils.uploadYoutubeVideoLoading(files, {}, loading);
             } else {
                 generatedDocuments.push(
                     ...files.map(file => {
                         const loading = Docs.Create.LoadingDocument(file, options);
-                        LoadingBox.addCurrentlyLoading(loading);
+                        Doc.addCurrentlyLoading(loading);
                         DocUtils.uploadFileToDoc(file, {}, loading);
                         return loading;
                     })
diff --git a/src/client/views/collections/CollectionTimeView.tsx b/src/client/views/collections/CollectionTimeView.tsx
index b3760d4af..0369e4a2a 100644
--- a/src/client/views/collections/CollectionTimeView.tsx
+++ b/src/client/views/collections/CollectionTimeView.tsx
@@ -12,7 +12,6 @@ import { listSpec } from '../../../fields/Schema';
 import { ComputedField, ScriptField } from '../../../fields/ScriptField';
 import { Cast, NumCast, StrCast } from '../../../fields/Types';
 import { Docs } from '../../documents/Documents';
-import { DocumentManager } from '../../util/DocumentManager';
 import { ScriptingGlobals } from '../../util/ScriptingGlobals';
 import { ContextMenu } from '../ContextMenu';
 import { ContextMenuProps } from '../ContextMenuItem';
@@ -257,7 +256,7 @@ ScriptingGlobals.add(function pivotColumnClick(pivotDoc: Doc, bounds: ViewDefBou
         action(() => {
             const filterVals = bounds.payload as string[];
             filterVals.map(filterVal => Doc.setDocFilter(pivotDoc, pivotField, filterVal, 'check'));
-            const pivotView = DocumentManager.Instance.getDocumentView(pivotDoc);
+            const pivotView = DocumentView.getDocumentView(pivotDoc);
             if (pivotDoc && pivotView?.ComponentView instanceof CollectionTimeView && filterVals.length === 1) {
                 if (pivotView?.ComponentView.childDocs.length && pivotView.ComponentView.childDocs[0][filterVals[0]]) {
                     // eslint-disable-next-line prefer-destructuring
diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx
index d015e73ad..c1247f5b0 100644
--- a/src/client/views/collections/CollectionTreeView.tsx
+++ b/src/client/views/collections/CollectionTreeView.tsx
@@ -14,11 +14,9 @@ import { TraceMobx } from '../../../fields/util';
 import { emptyFunction, Utils } from '../../../Utils';
 import { Docs } from '../../documents/Documents';
 import { DocUtils } from '../../documents/DocUtils';
-import { DocumentManager } from '../../util/DocumentManager';
 import { DragManager } from '../../util/DragManager';
 import { dropActionType } from '../../util/DropActionTypes';
 import { ScriptingGlobals } from '../../util/ScriptingGlobals';
-import { SelectionManager } from '../../util/SelectionManager';
 import { SnappingManager } from '../../util/SnappingManager';
 import { Transform } from '../../util/Transform';
 import { undoBatch, UndoManager } from '../../util/UndoManager';
@@ -27,10 +25,11 @@ import { ContextMenuProps } from '../ContextMenuItem';
 import { EditableView } from '../EditableView';
 import { DocumentView } from '../nodes/DocumentView';
 import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox';
-import { StyleProp } from '../StyleProvider';
+import { StyleProp } from '../StyleProp';
 import { CollectionFreeFormView } from './collectionFreeForm';
 import { CollectionSubView } from './CollectionSubView';
 import './CollectionTreeView.scss';
+import { TreeViewType } from './CollectionTreeViewType';
 import { TreeView } from './TreeView';
 
 const _global = (window /* browser */ || global) /* node */ as any;
@@ -49,12 +48,6 @@ export type collectionTreeViewProps = {
     hierarchyIndex?: number[];
 };
 
-export enum TreeViewType {
-    outline = 'outline',
-    fileSystem = 'fileSystem',
-    default = 'default',
-}
-
 @observer
 export class CollectionTreeView extends CollectionSubView<Partial<collectionTreeViewProps>>() {
     public static AddTreeFunc = 'addTreeFolder(this.embedContainer)';
@@ -183,7 +176,7 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree
         const targetDataDoc = this.Document[DocData];
         const value = DocListCast(targetDataDoc[this._props.fieldKey]);
         const result = value.filter(v => !docs.includes(v));
-        if (docs.some(doc => SelectionManager.Views.some(dv => Doc.AreProtosEqual(dv.Document, doc)))) SelectionManager.DeselectAll();
+        if (docs.some(doc => DocumentView.Selected().some(dv => Doc.AreProtosEqual(dv.Document, doc)))) DocumentView.DeselectAll();
         if (result.length !== value.length) {
             if (docIn instanceof Doc) {
                 const ind = DocListCast(targetDataDoc[this._props.fieldKey]).indexOf(docIn);
@@ -191,7 +184,7 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree
                 this._props.removeDocument?.(docIn);
                 if (ind > 0 && prev) {
                     Doc.SetSelectOnLoad(prev);
-                    DocumentManager.Instance.getDocumentView(prev, this.DocumentView?.())?.select(false);
+                    DocumentView.getDocumentView(prev, this.DocumentView?.())?.select(false);
                 }
                 return true;
             }
@@ -468,7 +461,7 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree
                                 minHeight: '100%',
                             }}
                             onWheel={e => e.stopPropagation()}
-                            onClick={() => (!this.layoutDoc.forceActive ? this._props.select(false) : SelectionManager.DeselectAll())}
+                            onClick={() => (!this.layoutDoc.forceActive ? this._props.select(false) : DocumentView.DeselectAll())}
                             onDrop={this.onTreeDrop}>
                             <ul className={`no-indent${this.outlineMode ? '-outline' : ''}`}>{this.treeViewElements}</ul>
                         </div>
diff --git a/src/client/views/collections/CollectionTreeViewType.ts b/src/client/views/collections/CollectionTreeViewType.ts
new file mode 100644
index 000000000..53b88160e
--- /dev/null
+++ b/src/client/views/collections/CollectionTreeViewType.ts
@@ -0,0 +1,5 @@
+export enum TreeViewType {
+    outline = 'outline',
+    fileSystem = 'fileSystem',
+    default = 'default',
+}
diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx
index 2d900bb73..2d8b2564d 100644
--- a/src/client/views/collections/TabDocView.tsx
+++ b/src/client/views/collections/TabDocView.tsx
@@ -9,7 +9,7 @@ import * as ReactDOM from 'react-dom/client';
 import { ClientUtils, DashColor, lightOrDark, returnEmptyDoclist, returnFalse, returnTrue, setupMoveUpEvents, simulateMouseClick } from '../../../ClientUtils';
 import { emptyFunction } from '../../../Utils';
 import { Doc, Opt } from '../../../fields/Doc';
-import { DocData } from '../../../fields/DocSymbols';
+import { DocData, DocViews } from '../../../fields/DocSymbols';
 import { Id } from '../../../fields/FieldSymbols';
 import { List } from '../../../fields/List';
 import { FieldId } from '../../../fields/RefField';
@@ -18,10 +18,8 @@ import { Cast, DocCast, NumCast, StrCast, toList } from '../../../fields/Types';
 import { DocServer } from '../../DocServer';
 import { CollectionViewType, DocumentType } from '../../documents/DocumentTypes';
 import { Docs } from '../../documents/Documents';
-import { DocumentManager } from '../../util/DocumentManager';
 import { DragManager } from '../../util/DragManager';
 import { dropActionType } from '../../util/DropActionTypes';
-import { SelectionManager } from '../../util/SelectionManager';
 import { SnappingManager } from '../../util/SnappingManager';
 import { Transform } from '../../util/Transform';
 import { UndoManager, undoable } from '../../util/UndoManager';
@@ -29,7 +27,8 @@ import { DashboardView } from '../DashboardView';
 import { LightboxView } from '../LightboxView';
 import { ObservableReactComponent } from '../ObservableReactComponent';
 import { PinDocView, PinProps } from '../PinFuncs';
-import { DefaultStyleProvider, StyleProp } from '../StyleProvider';
+import { StyleProp } from '../StyleProp';
+import { DefaultStyleProvider } from '../StyleProvider';
 import { Colors } from '../global/globalEnums';
 import { DocumentView, returnEmptyDocViewList } from '../nodes/DocumentView';
 import { FieldViewProps } from '../nodes/FieldView';
@@ -194,9 +193,101 @@ export class TabDocView extends ObservableReactComponent<TabDocViewProps> {
     _mainCont: HTMLDivElement | null = null;
     _tabReaction: IReactionDisposer | undefined;
 
+    /**
+     * Adds a document to the presentation view
+     * */
+    @action
+    public static PinDoc(docIn: Doc | Doc[], pinProps: PinProps) {
+        const docs = toList(docIn);
+
+        const batch = UndoManager.StartBatch('Pin doc to pres trail');
+        const curPres = Doc.ActivePresentation ?? Doc.MakeCopy(Doc.UserDoc().emptyTrail as Doc, true);
+
+        if (!Doc.ActivePresentation) {
+            Doc.AddDocToList(Doc.MyTrails, 'data', curPres);
+            Doc.ActivePresentation = curPres;
+        }
+
+        docs.forEach(doc => {
+            // Edge Case 1: Cannot pin document to itself
+            if (doc === curPres) {
+                alert('Cannot pin presentation document to itself');
+                return;
+            }
+            const anchorDoc = DocumentView.getDocumentView(doc)?.ComponentView?.getAnchor?.(false, pinProps);
+            const pinDoc = anchorDoc?.type === DocumentType.CONFIG ? anchorDoc : Docs.Create.ConfigDocument({});
+            const targDoc = (pinDoc.presentation_targetDoc = anchorDoc ?? doc);
+            pinDoc.title = doc.title + ' - Slide';
+            pinDoc.data = targDoc.type === DocumentType.PRES ? ComputedField.MakeFunction('copyField(this.presentation_targetDoc.data') : new List<Doc>(); // the children of the embedding's layout are the presentation slide children.  the embedding's data field might be children of a collection, PDF data, etc -- in any case we don't want the tree view to "see" this data
+            pinDoc.presentation_movement = doc.type === DocumentType.SCRIPTING || pinProps?.pinDocLayout ? PresMovement.None : PresMovement.Zoom;
+            pinDoc.presentation_duration = pinDoc.presentation_duration ?? 1000;
+            pinDoc.presentation_groupWithUp = false;
+            Doc.SetContainer(pinDoc, curPres);
+            // these should potentially all be props passed down by the CollectionTreeView to the TreeView elements.  That way the PresBox could configure all of its children at render time
+            pinDoc.treeView = ''; // not really needed, but makes key value pane look better
+            pinDoc.treeView_RenderAsBulletHeader = true; // forces a tree view to render the document next to the bullet in the header area
+            pinDoc.treeView_HeaderWidth = '100%'; // forces the header to grow to be the same size as its largest sibling.
+            pinDoc.treeView_FieldKey = 'data'; // tree view will treat the 'data' field as the field where the hierarchical children are located instead of using the document's layout string field
+            pinDoc.treeView_ExpandedView = 'data'; // in case the data doc has an expandedView set, this will mask that field and use the 'data' field when expanding the tree view
+            pinDoc.treeView_HideHeaderIfTemplate = true; // this will force the document to render itself as the tree view header
+            const duration = NumCast(doc[`${Doc.LayoutFieldKey(pinDoc)}_duration`], null);
+
+            if (pinProps.pinViewport) PinDocView(pinDoc, pinProps, anchorDoc ?? doc);
+            if (!pinProps?.audioRange && duration !== undefined) {
+                pinDoc.presentation_mediaStart = 'manual';
+                pinDoc.presentation_mediaStop = 'manual';
+            }
+            if (pinProps?.activeFrame !== undefined) {
+                pinDoc.config_activeFrame = pinProps?.activeFrame;
+                pinDoc.title = doc.title + ' (move)';
+                pinDoc.presentation_movement = PresMovement.Pan;
+            }
+            if (pinProps?.currentFrame !== undefined) {
+                pinDoc.config_currentFrame = pinProps?.currentFrame;
+                pinDoc.title = doc.title + ' (move)';
+                pinDoc.presentation_movement = PresMovement.Pan;
+            }
+            if (pinDoc.stroke_isInkMask) {
+                pinDoc.presentation_hideAfter = true;
+                pinDoc.presentation_hideBefore = true;
+                pinDoc.presentation_movement = PresMovement.None;
+            }
+            if (curPres.expandBoolean) pinDoc.presentation_expandInlineButton = true;
+            Doc.AddDocToList(curPres, 'data', pinDoc, PresBox.Instance?.sortArray()?.lastElement());
+            PresBox.Instance?.clearSelectedArray();
+            pinDoc && PresBox.Instance?.addToSelectedArray(pinDoc); // Update selected array
+        });
+        if (
+            // open the presentation trail if it's not already opened
+            !Array.from(CollectionDockingView.Instance?.tabMap ?? [])
+                .map(d => d.DashDoc)
+                .includes(curPres)
+        ) {
+            if (Doc.IsInMyOverlay(curPres)) Doc.RemFromMyOverlay(curPres);
+            CollectionDockingView.AddSplit(curPres, OpenWhereMod.right);
+            setTimeout(() => DocumentView.showDocument(docs.lastElement(), { willPan: true }), 100); // keeps the pinned doc in view since the sidebar shifts things
+        }
+        setTimeout(batch.end, 500); // need to wait until dockingview (goldenlayout) updates all its structurs
+    }
+
+    static Activate = (tabDoc: Doc) => {
+        const tab = Array.from(CollectionDockingView.Instance?.tabMap!).find(findTab => findTab.DashDoc === tabDoc && !findTab.contentItem.config.props.keyValue);
+        tab?.header.parent.setActiveContentItem(tab.contentItem); // glr: Panning does not work when this is set - (this line is for trying to make a tab that is not topmost become topmost)
+        return tab !== undefined;
+    };
+    // static ActivateTabView(doc: Doc) {
+    //     const tabView = Array.from(TabDocView._allTabs).find(view => view._document === doc);
+    //     if (!tabView?._activated && tabView?._document) {
+    //         TabDocView.Activate(tabView?._document);
+    //         return tabView;
+    //     }
+    //     return undefined;
+    // }
     constructor(props: any) {
         super(props);
         makeObservable(this);
+        DocumentView.activateTabView = TabDocView.Activate;
+        DocumentView.PinDoc = TabDocView.PinDoc;
     }
 
     @observable _activated: boolean = false;
@@ -205,8 +296,14 @@ export class TabDocView extends ObservableReactComponent<TabDocViewProps> {
     @observable _hovering = false;
     @observable _isActive: boolean = false;
     @observable _isAnyChildContentActive = false;
+    public static IsSelected = (doc?: Doc) => {
+        if (Array.from(doc?.[DocViews] ?? []).some(dv => dv?.IsSelected)) {
+            return true;
+        }
+        return false;
+    };
     @computed get _isUserActivated() {
-        return SelectionManager.IsSelected(this._document) || this._isAnyChildContentActive;
+        return TabDocView.IsSelected(this._document) || this._isAnyChildContentActive;
     }
     get _isContentActive() {
         return this._isUserActivated || this._hovering;
@@ -279,12 +376,12 @@ export class TabDocView extends ObservableReactComponent<TabDocViewProps> {
                         returnFalse,
                         action(clickEv => {
                             if (this.view) {
-                                SelectionManager.SelectView(this.view, false);
+                                DocumentView.SelectView(this.view, false);
                                 const child = getChild();
                                 simulateMouseClick(child, clickEv.clientX, clickEv.clientY + 30, clickEv.screenX, clickEv.screenY + 30);
                             } else {
                                 this._activated = true;
-                                setTimeout(() => this.view && SelectionManager.SelectView(this.view, false));
+                                setTimeout(() => this.view && DocumentView.SelectView(this.view, false));
                             }
                         })
                     );
@@ -350,7 +447,7 @@ export class TabDocView extends ObservableReactComponent<TabDocViewProps> {
             // select the tab document when the tab is directly clicked and activate the tab whenver the tab document is selected
             titleEle.onpointerdown = action((e: any) => {
                 if (e.target.className !== 'lm_iconWrap') {
-                    if (this.view) SelectionManager.SelectView(this.view, false);
+                    if (this.view) DocumentView.SelectView(this.view, false);
                     else this._activated = true;
                     if (Date.now() - titleEle.lastClick < 1000) titleEle.select();
                     titleEle.lastClick = Date.now();
@@ -358,7 +455,7 @@ export class TabDocView extends ObservableReactComponent<TabDocViewProps> {
                 }
             });
             tab._disposers.selectionDisposer = reaction(
-                () => SelectionManager.IsSelected(this._document),
+                () => TabDocView.IsSelected(this._document),
                 action(selected => {
                     if (selected) this._activated = true;
                     if (selected && tab.contentItem !== tab.header.parent.getActiveContentItem()) {
@@ -382,89 +479,12 @@ export class TabDocView extends ObservableReactComponent<TabDocViewProps> {
                 .off('click') // unbind the current click handler
                 .click(() => {
                     Object.values(tab._disposers).forEach((disposer: any) => disposer?.());
-                    SelectionManager.DeselectAll();
+                    DocumentView.DeselectAll();
                     UndoManager.RunInBatch(() => tab.contentItem.remove(), 'delete tab');
                 });
         }
     };
 
-    /**
-     * Adds a document to the presentation view
-     * */
-    @action
-    public static PinDoc(docIn: Doc | Doc[], pinProps: PinProps) {
-        const docs = toList(docIn);
-
-        const batch = UndoManager.StartBatch('Pin doc to pres trail');
-        const curPres = Doc.ActivePresentation ?? Doc.MakeCopy(Doc.UserDoc().emptyTrail as Doc, true);
-
-        if (!Doc.ActivePresentation) {
-            Doc.AddDocToList(Doc.MyTrails, 'data', curPres);
-            Doc.ActivePresentation = curPres;
-        }
-
-        docs.forEach(doc => {
-            // Edge Case 1: Cannot pin document to itself
-            if (doc === curPres) {
-                alert('Cannot pin presentation document to itself');
-                return;
-            }
-            const anchorDoc = DocumentManager.Instance.getDocumentView(doc)?.ComponentView?.getAnchor?.(false, pinProps);
-            const pinDoc = anchorDoc?.type === DocumentType.CONFIG ? anchorDoc : Docs.Create.ConfigDocument({});
-            const targDoc = (pinDoc.presentation_targetDoc = anchorDoc ?? doc);
-            pinDoc.title = doc.title + ' - Slide';
-            pinDoc.data = targDoc.type === DocumentType.PRES ? ComputedField.MakeFunction('copyField(this.presentation_targetDoc.data') : new List<Doc>(); // the children of the embedding's layout are the presentation slide children.  the embedding's data field might be children of a collection, PDF data, etc -- in any case we don't want the tree view to "see" this data
-            pinDoc.presentation_movement = doc.type === DocumentType.SCRIPTING || pinProps?.pinDocLayout ? PresMovement.None : PresMovement.Zoom;
-            pinDoc.presentation_duration = pinDoc.presentation_duration ?? 1000;
-            pinDoc.presentation_groupWithUp = false;
-            Doc.SetContainer(pinDoc, curPres);
-            // these should potentially all be props passed down by the CollectionTreeView to the TreeView elements.  That way the PresBox could configure all of its children at render time
-            pinDoc.treeView = ''; // not really needed, but makes key value pane look better
-            pinDoc.treeView_RenderAsBulletHeader = true; // forces a tree view to render the document next to the bullet in the header area
-            pinDoc.treeView_HeaderWidth = '100%'; // forces the header to grow to be the same size as its largest sibling.
-            pinDoc.treeView_FieldKey = 'data'; // tree view will treat the 'data' field as the field where the hierarchical children are located instead of using the document's layout string field
-            pinDoc.treeView_ExpandedView = 'data'; // in case the data doc has an expandedView set, this will mask that field and use the 'data' field when expanding the tree view
-            pinDoc.treeView_HideHeaderIfTemplate = true; // this will force the document to render itself as the tree view header
-            const duration = NumCast(doc[`${Doc.LayoutFieldKey(pinDoc)}_duration`], null);
-
-            if (pinProps.pinViewport) PinDocView(pinDoc, pinProps, anchorDoc ?? doc);
-            if (!pinProps?.audioRange && duration !== undefined) {
-                pinDoc.presentation_mediaStart = 'manual';
-                pinDoc.presentation_mediaStop = 'manual';
-            }
-            if (pinProps?.activeFrame !== undefined) {
-                pinDoc.config_activeFrame = pinProps?.activeFrame;
-                pinDoc.title = doc.title + ' (move)';
-                pinDoc.presentation_movement = PresMovement.Pan;
-            }
-            if (pinProps?.currentFrame !== undefined) {
-                pinDoc.config_currentFrame = pinProps?.currentFrame;
-                pinDoc.title = doc.title + ' (move)';
-                pinDoc.presentation_movement = PresMovement.Pan;
-            }
-            if (pinDoc.stroke_isInkMask) {
-                pinDoc.presentation_hideAfter = true;
-                pinDoc.presentation_hideBefore = true;
-                pinDoc.presentation_movement = PresMovement.None;
-            }
-            if (curPres.expandBoolean) pinDoc.presentation_expandInlineButton = true;
-            Doc.AddDocToList(curPres, 'data', pinDoc, PresBox.Instance?.sortArray()?.lastElement());
-            PresBox.Instance?.clearSelectedArray();
-            pinDoc && PresBox.Instance?.addToSelectedArray(pinDoc); // Update selected array
-        });
-        if (
-            // open the presentation trail if it's not already opened
-            !Array.from(CollectionDockingView.Instance?.tabMap ?? [])
-                .map(d => d.DashDoc)
-                .includes(curPres)
-        ) {
-            if (Doc.IsInMyOverlay(curPres)) Doc.RemFromMyOverlay(curPres);
-            CollectionDockingView.AddSplit(curPres, OpenWhereMod.right);
-            setTimeout(() => DocumentManager.Instance.showDocument(docs.lastElement(), { willPan: true }), 100); // keeps the pinned doc in view since the sidebar shifts things
-        }
-        setTimeout(batch.end, 500); // need to wait until dockingview (goldenlayout) updates all its structurs
-    }
-
     componentDidMount() {
         new _global.ResizeObserver(
             action((entries: any) => {
@@ -484,12 +504,12 @@ export class TabDocView extends ObservableReactComponent<TabDocViewProps> {
     }
     componentDidUpdate(prevProps: Readonly<TabDocViewProps>) {
         super.componentDidUpdate(prevProps);
-        this._view && DocumentManager.Instance.AddView(this._view);
+        this._view && DocumentView.addView(this._view);
     }
 
     componentWillUnmount() {
         this._tabReaction?.();
-        this._view && DocumentManager.Instance.RemoveView(this._view);
+        this._view && DocumentView.removeView(this._view);
         runInAction(() => TabDocView._allTabs.delete(this));
 
         this._props.glContainer.layoutManager.off('activeContentItemChanged', this.onActiveContentItemChanged);
@@ -503,7 +523,7 @@ export class TabDocView extends ObservableReactComponent<TabDocViewProps> {
     private onActiveContentItemChanged(contentItem: any) {
         if (!contentItem || (this.stack === contentItem.parent && ((contentItem?.tab === this.tab && !this._isActive) || (contentItem?.tab !== this.tab && this._isActive)))) {
             this._activated = this._isActive = !contentItem || contentItem?.tab === this.tab;
-            if (!this._view && this.tab?.contentItem?.config?.props?.panelName !== TabDocView.DontSelectOnActivate) setTimeout(() => SelectionManager.SelectView(this._view, false));
+            if (!this._view && this.tab?.contentItem?.config?.props?.panelName !== TabDocView.DontSelectOnActivate) setTimeout(() => DocumentView.SelectView(this._view, false));
             !this._isActive && this._document && Doc.UnBrushDoc(this._document); // bcz: bad -- trying to simulate a pointer leave event when a new tab is opened up on top of an existing one.
         }
     }
@@ -517,7 +537,7 @@ export class TabDocView extends ObservableReactComponent<TabDocViewProps> {
     //  lightbox - will add the document to any collection along the path from the document to the docking view that has a field isLightbox. if none is found, it adds to the full screen lightbox
     addDocTab = (docsIn: Doc | Doc[], location: OpenWhere) => {
         const docs = toList(docsIn);
-        SelectionManager.DeselectAll();
+        DocumentView.DeselectAll();
         const whereFields = location.split(':');
         const keyValue = whereFields.includes(OpenWhereMod.keyvalue);
         const whereMods = whereFields.length > 1 ? (whereFields[1] as OpenWhereMod) : OpenWhereMod.none;
@@ -527,7 +547,7 @@ export class TabDocView extends ObservableReactComponent<TabDocViewProps> {
         switch (whereFields[0]) {
             case undefined:
             case OpenWhere.lightbox:   if (this.layoutDoc?._isLightbox) {
-                                            const lightboxView = !docs[0].annotationOn && DocCast(docs[0].embedContainer) ? DocumentManager.Instance.getFirstDocumentView(DocCast(docs[0].embedContainer)) : undefined;
+                                            const lightboxView = !docs[0].annotationOn && DocCast(docs[0].embedContainer) ? DocumentView.getFirstDocumentView(DocCast(docs[0].embedContainer)) : undefined;
                                             const data = lightboxView?.dataDoc[Doc.LayoutFieldKey(lightboxView.Document)];
                                             if (lightboxView && (!data || data instanceof List)) {
                                                 lightboxView.layoutDoc[Doc.LayoutFieldKey(lightboxView.Document)] = new List<Doc>(docs);
@@ -543,7 +563,7 @@ export class TabDocView extends ObservableReactComponent<TabDocViewProps> {
     };
     remDocTab = (doc: Doc | Doc[]) => {
         if (doc === this._document) {
-            SelectionManager.DeselectAll();
+            DocumentView.DeselectAll();
             CollectionDockingView.CloseSplit(this._document);
             return true;
         }
@@ -551,11 +571,7 @@ export class TabDocView extends ObservableReactComponent<TabDocViewProps> {
     };
 
     getCurrentFrame = () => NumCast(Cast(PresBox.Instance.activeItem.presentation_targetDoc, Doc, null)._currentFrame);
-    static Activate = (tabDoc: Doc) => {
-        const tab = Array.from(CollectionDockingView.Instance?.tabMap!).find(findTab => findTab.DashDoc === tabDoc && !findTab.contentItem.config.props.keyValue);
-        tab?.header.parent.setActiveContentItem(tab.contentItem); // glr: Panning does not work when this is set - (this line is for trying to make a tab that is not topmost become topmost)
-        return tab !== undefined;
-    };
+
     @action
     focusFunc = () => {
         if (!this.tab.header.parent._activeContentItem || this.tab.header.parent._activeContentItem !== this.tab.contentItem) {
@@ -586,7 +602,7 @@ export class TabDocView extends ObservableReactComponent<TabDocViewProps> {
                 <DocumentView
                     key={this._document[Id]}
                     ref={action((r: DocumentView) => {
-                        this._lastView && DocumentManager.Instance.RemoveView(this._lastView);
+                        this._lastView && DocumentView.removeView(this._lastView);
                         this._view = r;
                         this._lastView = this._view;
                     })}
@@ -635,7 +651,7 @@ export class TabDocView extends ObservableReactComponent<TabDocViewProps> {
                     this._mainCont = ref;
                     if (this._mainCont) {
                         if (this._lastTab) {
-                            this._view && DocumentManager.Instance.RemoveView(this._view);
+                            this._view && DocumentView.removeView(this._view);
                         }
                         this._lastTab = this.tab;
                         (this._mainCont as any).InitTab = (tab: any) => this.init(tab, this._document);
diff --git a/src/client/views/collections/TreeView.tsx b/src/client/views/collections/TreeView.tsx
index 2f5af0564..969b98d91 100644
--- a/src/client/views/collections/TreeView.tsx
+++ b/src/client/views/collections/TreeView.tsx
@@ -21,24 +21,23 @@ import { TraceMobx } from '../../../fields/util';
 import { DocUtils } from '../../documents/DocUtils';
 import { CollectionViewType, DocumentType } from '../../documents/DocumentTypes';
 import { Docs } from '../../documents/Documents';
-import { DocumentManager } from '../../util/DocumentManager';
 import { DragManager } from '../../util/DragManager';
 import { dropActionType } from '../../util/DropActionTypes';
-import { LinkManager } from '../../util/LinkManager';
 import { SettingsManager } from '../../util/SettingsManager';
 import { SnappingManager } from '../../util/SnappingManager';
 import { Transform } from '../../util/Transform';
 import { UndoManager, undoBatch, undoable } from '../../util/UndoManager';
 import { EditableView } from '../EditableView';
 import { ObservableReactComponent } from '../ObservableReactComponent';
-import { StyleProp } from '../StyleProvider';
+import { StyleProp } from '../StyleProp';
 import { DocumentView, DocumentViewInternal } from '../nodes/DocumentView';
 import { FieldViewProps, StyleProviderFuncType } from '../nodes/FieldView';
 import { KeyValueBox } from '../nodes/KeyValueBox';
 import { OpenWhere } from '../nodes/OpenWhere';
 import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox';
 import { RichTextMenu } from '../nodes/formattedText/RichTextMenu';
-import { CollectionTreeView, TreeViewType } from './CollectionTreeView';
+import { CollectionTreeView } from './CollectionTreeView';
+import { TreeViewType } from './CollectionTreeViewType';
 import { CollectionView } from './CollectionView';
 import { TreeSort } from './TreeSort';
 import './TreeView.scss';
@@ -208,7 +207,7 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> {
         const ind = DocListCast(this.dataDoc[key]).indexOf(docs.lastElement());
 
         const res = docs.reduce((flg, doc) => flg && Doc.RemoveDocFromList(this.dataDoc, key, doc), true);
-        res && ind > 0 && DocumentManager.Instance.getDocumentView(DocListCast(this.dataDoc[key])[ind - 1], this.treeView.DocumentView?.())?.select(false);
+        res && ind > 0 && DocumentView.getDocumentView(DocListCast(this.dataDoc[key])[ind - 1], this.treeView.DocumentView?.())?.select(false);
         return res;
     };
 
@@ -379,7 +378,7 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> {
         const bulletData = bullet[DocData];
         bulletData.title = ComputedField.MakeFunction('this.text?.Text');
         bulletData.data = new List<Doc>([]);
-        DocumentManager.Instance.AddViewRenderedCb(bullet, dv => dv.ComponentView?.setFocus?.());
+        DocumentView.addViewRenderedCb(bullet, dv => dv.ComponentView?.setFocus?.());
 
         return bullet;
     }
@@ -823,7 +822,7 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> {
 
     @computed get validExpandViewTypes() {
         const annos = () => (DocListCast(this.Document[this.fieldKey + '_annotations']).length && !this.treeView.dashboardMode ? 'annotations' : '');
-        const links = () => (LinkManager.Links(this.Document).length && !this.treeView.dashboardMode ? 'links' : '');
+        const links = () => (Doc.Links(this.Document).length && !this.treeView.dashboardMode ? 'links' : '');
         const data = () => (this.childDocs || this.treeView.dashboardMode ? this.fieldKey : '');
         const embeddings = () => (this.treeView.dashboardMode ? '' : 'embeddings');
         const fields = () => (Doc.noviceMode ? '' : 'fields');
@@ -1210,7 +1209,7 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> {
                 className={`treeView-container${this._props.isContentActive() ? '-active' : ''}`}
                 ref={this.createTreeDropTarget}
                 onDrop={this.onTreeDrop}
-                // onPointerDown={e => this._props.isContentActive(true) && SelectionManager.DeselectAll()} // bcz: this breaks entering a text filter in a filterBox since it deselects the filter's target document
+                // onPointerDown={e => this._props.isContentActive(true) && DocumentView.DeselectAll()} // bcz: this breaks entering a text filter in a filterBox since it deselects the filter's target document
                 // onKeyDown={this.onKeyDown}
             >
                 <li className="collection-child">
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormClusters.ts b/src/client/views/collections/collectionFreeForm/CollectionFreeFormClusters.ts
index 6fc295e27..26a52cd2a 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormClusters.ts
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormClusters.ts
@@ -1,17 +1,16 @@
 import { action, observable } from 'mobx';
+import { CollectionFreeFormView } from '.';
+import { intersectRect } from '../../../../Utils';
 import { Doc, Opt } from '../../../../fields/Doc';
 import { NumCast, StrCast } from '../../../../fields/Types';
-import { intersectRect } from '../../../../Utils';
 import { DocumentType } from '../../../documents/DocumentTypes';
-import { DocumentManager } from '../../../util/DocumentManager';
 import { DragManager } from '../../../util/DragManager';
 import { dropActionType } from '../../../util/DropActionTypes';
-import { SelectionManager } from '../../../util/SelectionManager';
+import { StyleProp } from '../../StyleProp';
 import { CollectionFreeFormDocumentView } from '../../nodes/CollectionFreeFormDocumentView';
+import { DocumentView } from '../../nodes/DocumentView';
 import { FieldViewProps } from '../../nodes/FieldView';
-import { StyleProp } from '../../StyleProvider';
 import './CollectionFreeFormView.scss';
-import { CollectionFreeFormView } from '.';
 
 export class CollectionFreeFormClusters {
     private _view: CollectionFreeFormView;
@@ -44,7 +43,7 @@ export class CollectionFreeFormClusters {
         const h = NumCast(doc1Layout._height) + clusterDistance;
         return doc1.z === doc2.z && intersectRect({ left: x, top: y, width: w, height: h }, { left: x2, top: y2, width: w2, height: h2 });
     }
-    pickCluster(probe: number[]) {
+    handlePointerDown(probe: number[]) {
         this._hitCluster = this.childLayoutPairs
             .map(pair => pair.layout)
             .reduce((cluster, cd) => {
@@ -62,13 +61,13 @@ export class CollectionFreeFormClusters {
         return this._hitCluster;
     }
 
-    tryDragCluster(e: PointerEvent) {
+    tryToDrag(e: PointerEvent) {
         const cluster = this._hitCluster;
         if (cluster !== -1) {
             const ptsParent = e;
             if (ptsParent) {
                 const eles = this.childLayoutPairs.map(pair => pair.layout).filter(cd => (this.Document._freeform_useClusters ? NumCast(cd.layout_cluster) : NumCast(cd.group, -1)) === cluster);
-                const clusterDocs = eles.map(ele => DocumentManager.Instance.getDocumentView(ele, this.DocumentView)!);
+                const clusterDocs = eles.map(ele => DocumentView.getDocumentView(ele, this.DocumentView)!);
                 const { left, top } = clusterDocs[0].getBounds || { left: 0, top: 0 };
                 const de = new DragManager.DocumentDragData(eles, e.ctrlKey || e.altKey ? dropActionType.embed : undefined);
                 de.moveDocument = this.viewMoveDocument;
@@ -87,7 +86,7 @@ export class CollectionFreeFormClusters {
         return false;
     }
 
-    initClusters() {
+    initLayout() {
         if (this.Document._freeform_useClusters && !this._clusterSets.length && this.childDocs.length) {
             return this.updateClusters(true);
         }
@@ -97,11 +96,11 @@ export class CollectionFreeFormClusters {
     updateClusters(useClusters: boolean) {
         this.Document._freeform_useClusters = useClusters;
         this._clusterSets.length = 0;
-        this.childLayoutPairs.map(pair => pair.layout).map(c => this.updateCluster(c));
+        this.childLayoutPairs.map(pair => pair.layout).map(c => this.addDocument(c));
     }
 
     @action
-    updateClusterDocs(docs: Doc[]) {
+    addDocuments(docs: Doc[]) {
         const childLayouts = this.childLayoutPairs.map(pair => pair.layout);
         if (this.Document._freeform_useClusters) {
             const docFirst = docs[0];
@@ -143,12 +142,12 @@ export class CollectionFreeFormClusters {
                     this._clusterSets[(doc.layout_cluster = NumCast(docFirst.layout_cluster))].push(doc);
                 });
             }
-            childLayouts.map(child => !this._clusterSets.some((set, i) => Doc.IndexOf(child, set) !== -1 && child.layout_cluster === i) && this.updateCluster(child));
+            childLayouts.map(child => !this._clusterSets.some((set, i) => Doc.IndexOf(child, set) !== -1 && child.layout_cluster === i) && this.addDocument(child));
         }
     }
 
     @action
-    updateCluster = (doc: Doc) => {
+    addDocument = (doc: Doc) => {
         const childLayouts = this.childLayoutPairs.map(pair => pair.layout);
         if (this.Document._freeform_useClusters) {
             this._clusterSets.forEach(set => Doc.IndexOf(doc, set) !== -1 && set.splice(Doc.IndexOf(doc, set), 1));
@@ -188,7 +187,7 @@ export class CollectionFreeFormClusters {
                         const cluster = NumCast(doc?.layout_cluster);
                         if (this.Document._freeform_useClusters && doc?.type !== DocumentType.IMG) {
                             if (this._clusterSets.length <= cluster) {
-                                setTimeout(() => doc && this.updateCluster(doc));
+                                setTimeout(() => doc && this.addDocument(doc));
                             } else {
                                 // choose a cluster color from a palette
                                 const colors = ['#da42429e', '#31ea318c', 'rgba(197, 87, 20, 0.55)', '#4a7ae2c4', 'rgba(216, 9, 255, 0.5)', '#ff7601', '#1dffff', 'yellow', 'rgba(27, 130, 49, 0.55)', 'rgba(0, 0, 0, 0.268)'];
@@ -212,9 +211,9 @@ export class CollectionFreeFormClusters {
         return styleProp;
     };
 
-    trySelectCluster = (addToSel: boolean) => {
+    tryToSelect = (addToSel: boolean) => {
         if (addToSel && this._hitCluster !== -1) {
-            !addToSel && SelectionManager.DeselectAll();
+            !addToSel && DocumentView.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);
             return true;
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormInfoUI.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormInfoUI.tsx
index 65a529d62..5d8373fc7 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormInfoUI.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormInfoUI.tsx
@@ -4,13 +4,13 @@ import * as React from 'react';
 import { Doc, DocListCast, FieldType, FieldResult } from '../../../../fields/Doc';
 import { InkTool } from '../../../../fields/InkField';
 import { StrCast } from '../../../../fields/Types';
-import { LinkManager } from '../../../util/LinkManager';
 import { ObservableReactComponent } from '../../ObservableReactComponent';
 import { DocButtonState, DocumentLinksButton } from '../../nodes/DocumentLinksButton';
 import { TopBar } from '../../topbar/TopBar';
 import { CollectionFreeFormInfoState, InfoState, StateEntryFunc, infoState } from './CollectionFreeFormInfoState';
 import './CollectionFreeFormView.scss';
 import { DocData } from '../../../../fields/DocSymbols';
+import { CollectionFreeFormView } from './CollectionFreeFormView';
 
 export interface CollectionFreeFormInfoUIProps {
     Document: Doc;
@@ -21,6 +21,12 @@ export interface CollectionFreeFormInfoUIProps {
 
 @observer
 export class CollectionFreeFormInfoUI extends ObservableReactComponent<CollectionFreeFormInfoUIProps> {
+    public static Init() {
+        CollectionFreeFormView.SetInfoUICreator((doc: Doc, layout: Doc, childDocs: () => Doc[], close: () => void) => (
+            //
+            <CollectionFreeFormInfoUI Document={doc} LayoutDoc={layout} childDocs={childDocs} close={close} />
+        ));
+    }
     _firstDocPos = { x: 0, y: 0 };
 
     constructor(props: any) {
@@ -63,7 +69,7 @@ export class CollectionFreeFormInfoUI extends ObservableReactComponent<Collectio
         const linkStart = () => DocumentLinksButton.StartLink;
         const linkUnstart = () => !DocumentLinksButton.StartLink;
 
-        const numDocLinks = () => LinkManager.Instance.getAllDirectLinks(firstDoc())?.length;
+        const numDocLinks = () => Doc.Links(firstDoc())?.length;
         const linkMenuOpen = () => DocButtonState.Instance.LinkEditorDocView;
 
         const activeTool = () => Doc.ActiveTool;
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormPannableContents.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormPannableContents.tsx
index 90977d955..65a2fe0aa 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormPannableContents.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormPannableContents.tsx
@@ -3,9 +3,8 @@ import { observer } from 'mobx-react';
 import * as React from 'react';
 import { Doc } from '../../../../fields/Doc';
 import { ScriptField } from '../../../../fields/ScriptField';
-import { PresBox } from '../../nodes/trails/PresBox';
-import './CollectionFreeFormView.scss';
 import { ObservableReactComponent } from '../../ObservableReactComponent';
+import './CollectionFreeFormView.scss';
 
 export interface CollectionFreeFormPannableContentsProps {
     Document: Doc;
@@ -20,12 +19,16 @@ export interface CollectionFreeFormPannableContentsProps {
 
 @observer
 export class CollectionFreeFormPannableContents extends ObservableReactComponent<CollectionFreeFormPannableContentsProps> {
+    static _overlayPlugin: ((fform: Doc) => React.JSX.Element) | null = null;
+    public static SetOverlayPlugin(plugin: ((fform: Doc) => React.JSX.Element) | null) {
+        CollectionFreeFormPannableContents._overlayPlugin = plugin;
+    }
     constructor(props: CollectionFreeFormPannableContentsProps) {
         super(props);
         makeObservable(this);
     }
     @computed get presPaths() {
-        return this._props.showPresPaths() ? PresBox.Instance.pathLines(this._props.Document) : null;
+        return this._props.showPresPaths() ? CollectionFreeFormPannableContents._overlayPlugin?.(this._props.Document) : null;
     }
     // rectangle highlight used when following trail/link to a region of a collection that isn't a document
     showViewport = (viewport: { panX: number; panY: number; width: number; height: number } | undefined) =>
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index f55d5a23f..6d901119e 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -26,15 +26,12 @@ import { aggregateBounds, emptyFunction, intersectRect, Utils } from '../../../.
 import { Docs } from '../../../documents/Documents';
 import { CollectionViewType, DocumentType } from '../../../documents/DocumentTypes';
 import { DocUtils } from '../../../documents/DocUtils';
-import { DocumentManager } from '../../../util/DocumentManager';
 import { DragManager } from '../../../util/DragManager';
 import { dropActionType } from '../../../util/DropActionTypes';
 import { ReplayMovements } from '../../../util/ReplayMovements';
 import { CompileScript } from '../../../util/Scripting';
 import { ScriptingGlobals } from '../../../util/ScriptingGlobals';
-import { SelectionManager } from '../../../util/SelectionManager';
-import { freeformScrollMode } from '../../../util/SettingsManager';
-import { SnappingManager } from '../../../util/SnappingManager';
+import { freeformScrollMode, SnappingManager } from '../../../util/SnappingManager';
 import { Transform } from '../../../util/Transform';
 import { undoable, undoBatch, UndoManager } from '../../../util/UndoManager';
 import { Timeline } from '../../animationtimeline/Timeline';
@@ -50,12 +47,11 @@ import { FocusViewOptions } from '../../nodes/FocusViewOptions';
 import { FormattedTextBox } from '../../nodes/formattedText/FormattedTextBox';
 import { OpenWhere } from '../../nodes/OpenWhere';
 import { PinDocView, PinProps } from '../../PinFuncs';
-import { StyleProp } from '../../StyleProvider';
+import { StyleProp } from '../../StyleProp';
 import { CollectionSubView } from '../CollectionSubView';
-import { TreeViewType } from '../CollectionTreeView';
+import { TreeViewType } from '../CollectionTreeViewType';
 import { CollectionFreeFormBackgroundGrid } from './CollectionFreeFormBackgroundGrid';
 import { CollectionFreeFormClusters } from './CollectionFreeFormClusters';
-import { CollectionFreeFormInfoUI } from './CollectionFreeFormInfoUI';
 import { computePassLayout, computePivotLayout, computeStarburstLayout, computeTimelineLayout, PoolData, ViewDefBounds, ViewDefResult } from './CollectionFreeFormLayoutEngines';
 import { CollectionFreeFormPannableContents } from './CollectionFreeFormPannableContents';
 import { CollectionFreeFormRemoteCursors } from './CollectionFreeFormRemoteCursors';
@@ -87,8 +83,9 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     } // this makes mobx trace() statements more descriptive
     public unprocessedDocs: Doc[] = [];
     public static collectionsWithUnprocessedInk = new Set<CollectionFreeFormView>();
-    public static from(dv?: DocumentView) {
-        return CollectionFreeFormDocumentView.from(dv)?.CollectionFreeFormView;
+    public static from(dv?: DocumentView): CollectionFreeFormView | undefined {
+        const parent = CollectionFreeFormDocumentView.from(dv)?._props.parent;
+        return parent instanceof CollectionFreeFormView ? parent : undefined;
     }
 
     _oldWheel: any;
@@ -227,25 +224,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     public static gotoKeyframe(timer: NodeJS.Timeout | undefined, docs: Doc[], duration: number) {
         return DocumentView.SetViewTransition(docs, 'all', duration, timer, undefined, true);
     }
-    public static updateKeyframe(timer: NodeJS.Timeout | undefined, docs: Doc[], time: number) {
-        const newTimer = DocumentView.SetViewTransition(docs, 'all', 1000, timer, undefined, true);
-        const timecode = Math.round(time);
-        docs.forEach(doc => {
-            CollectionFreeFormDocumentView.animFields.forEach(val => {
-                const findexed = Cast(doc[`${val.key}_indexed`], listSpec('number'), null);
-                findexed?.length <= timecode + 1 && findexed.push(undefined as any as number);
-            });
-            CollectionFreeFormDocumentView.animStringFields.forEach(val => {
-                const findexed = Cast(doc[`${val}_indexed`], listSpec('string'), null);
-                findexed?.length <= timecode + 1 && findexed.push(undefined as any as string);
-            });
-            CollectionFreeFormDocumentView.animDataFields(doc).forEach(val => {
-                const findexed = Cast(doc[`${val}_indexed`], listSpec(InkField), null);
-                findexed?.length <= timecode + 1 && findexed.push(undefined as any);
-            });
-        });
-        return newTimer;
-    }
     changeKeyFrame = (back = false) => {
         const currentFrame = Cast(this.Document._currentFrame, 'number', null);
         if (currentFrame === undefined) {
@@ -256,7 +234,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
             this._keyTimer = CollectionFreeFormView.gotoKeyframe(this._keyTimer, [...this.childDocs, this.layoutDoc], 1000);
             this.Document._currentFrame = Math.max(0, (currentFrame || 0) - 1);
         } else {
-            this._keyTimer = CollectionFreeFormView.updateKeyframe(this._keyTimer, [...this.childDocs, this.layoutDoc], currentFrame || 0);
+            this._keyTimer = CollectionFreeFormDocumentView.updateKeyframe(this._keyTimer, [...this.childDocs, this.layoutDoc], currentFrame || 0);
             this.Document._currentFrame = Math.max(0, (currentFrame || 0) + 1);
             this.Document.lastFrame = Math.max(NumCast(this.Document._currentFrame), NumCast(this.Document.lastFrame));
         }
@@ -292,8 +270,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         this.addDocument(newDoc);
     };
     selectDocuments = (docs: Doc[]) => {
-        SelectionManager.DeselectAll();
-        docs.map(doc => DocumentManager.Instance.getDocumentView(doc, this.DocumentView?.())).forEach(dv => dv && SelectionManager.SelectView(dv, true));
+        DocumentView.DeselectAll();
+        docs.map(doc => DocumentView.getDocumentView(doc, this.DocumentView?.())).forEach(dv => dv && DocumentView.SelectView(dv, true));
     };
     addDocument = (newBox: Doc | Doc[]) => {
         let retVal = false;
@@ -301,7 +279,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
             retVal = this._props.addDocument?.(newBox) || false;
             if (retVal) {
                 this.bringToFront(newBox);
-                this._clusters.updateCluster(newBox);
+                this._clusters.addDocument(newBox);
             }
         } else {
             retVal = this._props.addDocument?.(newBox) || false;
@@ -416,7 +394,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                 res(this.DocumentView?.());
                 return;
             }
-            const findDoc = (finish: (dv: DocumentView) => void) => DocumentManager.Instance.AddViewRenderedCb(doc, dv => finish(dv));
+            const findDoc = (finish: (dv: DocumentView) => void) => DocumentView.addViewRenderedCb(doc, dv => finish(dv));
             findDoc(dv => res(dv));
         });
 
@@ -459,7 +437,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
             !d._keepZWhenDragged && (d.zIndex = zsorted.length + 1 + i); // bringToFront
         }
 
-        (docDragData.droppedDocuments.length === 1 || de.shiftKey) && this._clusters.updateClusterDocs(docDragData.droppedDocuments);
+        (docDragData.droppedDocuments.length === 1 || de.shiftKey) && this._clusters.addDocuments(docDragData.droppedDocuments);
         return true;
     }
 
@@ -528,7 +506,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                         break;
                     case InkTool.None:
                         if (!(this._props.layoutEngine?.() || StrCast(this.layoutDoc._layoutEngine))) {
-                            const hit = this._clusters.pickCluster(this.screenToFreeformContentsXf.transformPoint(e.clientX, e.clientY));
+                            const hit = this._clusters.handlePointerDown(this.screenToFreeformContentsXf.transformPoint(e.clientX, e.clientY));
                             setupMoveUpEvents(this, e, this.onPointerMove, emptyFunction, emptyFunction, hit !== -1, false);
                         }
                         break;
@@ -654,7 +632,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     };
 
     onPointerMove = (e: PointerEvent) => {
-        if (this._clusters.tryDragCluster(e)) {
+        if (this._clusters.tryToDrag(e)) {
             e.stopPropagation(); // we're moving a cluster, so stop propagation and return true to end panning and let the document drag take over
             return true;
         }
@@ -675,7 +653,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         const eraserMax = { X: Math.max(lastPoint.X, currPoint.X), Y: Math.max(lastPoint.Y, currPoint.Y) };
         // prettier-ignore
         return this.childDocs 
-            .map(doc => DocumentManager.Instance.getDocumentView(doc, this.DocumentView?.()))
+            .map(doc => DocumentView.getDocumentView(doc, this.DocumentView?.()))
             .filter(inkView => inkView?.ComponentView instanceof InkingStroke)
             .map(inkView => inkView!)
             .map(inkView => ({ inkViewBounds: inkView.getBounds, inkStroke: inkView.ComponentView as InkingStroke, inkView }))
@@ -776,7 +754,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         this.childDocs
             .filter(doc => doc.type === DocumentType.INK && !doc.dontIntersect)
             .forEach(doc => {
-                const otherInk = DocumentManager.Instance.getDocumentView(doc, this.DocumentView?.())?.ComponentView as InkingStroke;
+                const otherInk = DocumentView.getDocumentView(doc, this.DocumentView?.())?.ComponentView as InkingStroke;
                 const { inkData: otherInkData } = otherInk?.inkScaledData() ?? { inkData: [] };
                 const otherScreenPts = otherInkData.map(point => otherInk.ptToScreen(point));
                 const otherCtrlPts = otherScreenPts.map(spt => (ink.ComponentView as InkingStroke).ptFromScreen(spt));
@@ -1078,17 +1056,18 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                 {...OmitKeys(entry, ['replica', 'pair']).omit}
                 key={childLayout[Id] + (entry.replica || '')}
                 Document={childLayout}
+                parent={this}
                 containerViewPath={this.DocumentView?.().docViewPath}
                 styleProvider={this._clusters.styleProvider}
                 TemplateDataDocument={childData}
                 dragStarting={this.dragStarting}
                 dragEnding={this.dragEnding}
+                isAnyChildContentActive={this.isAnyChildContentActive}
                 isGroupActive={this._props.isGroupActive}
                 renderDepth={this._props.renderDepth + 1}
                 hideDecorations={BoolCast(childLayout._layout_isSvg && childLayout.type === DocumentType.LINK)}
                 suppressSetHeight={!!this.layoutEngine}
                 RenderCutoffProvider={this.renderCutoffProvider}
-                CollectionFreeFormView={this}
                 LayoutTemplate={childLayout.z ? undefined : this._props.childLayoutTemplate}
                 LayoutTemplateString={childLayout.z ? undefined : this._props.childLayoutString}
                 rootSelected={childData ? this.rootSelected : returnFalse}
@@ -1282,7 +1261,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                 })
             );
 
-        this._clusters.initClusters();
+        this._clusters.initLayout();
         return elements;
     };
 
@@ -1308,7 +1287,14 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
 
     childDocsFunc = () => this.childDocs;
     closeInfo = action(() => { Doc.IsInfoUIDisabled = true }); // prettier-ignore
-    infoUI = () => (Doc.IsInfoUIDisabled || this.Document.annotationOn || this._props.renderDepth ? null : <CollectionFreeFormInfoUI Document={this.Document} LayoutDoc={this.layoutDoc} childDocs={this.childDocsFunc} close={this.closeInfo} />);
+    static _infoUI: ((doc: Doc, layout: Doc, childDocs: () => Doc[], close: () => void) => JSX.Element) | null = null;
+    static SetInfoUICreator(func: (doc: Doc, layout: Doc, childDocs: () => Doc[], close: () => void) => JSX.Element) {
+        CollectionFreeFormView._infoUI = func;
+    }
+    infoUI = () =>
+        Doc.IsInfoUIDisabled || this.Document.annotationOn || this._props.renderDepth
+            ? null //
+            : CollectionFreeFormView._infoUI?.(this.Document, this.layoutDoc, this.childDocsFunc, this.closeInfo) || null;
 
     componentDidMount() {
         this._props.setContentViewBox?.(this);
@@ -1551,9 +1537,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         const isDocInView = (doc: Doc, rect: { left: number; top: number; width: number; height: number }) => intersectRect(docDims(doc), rect);
 
         const snappableDocs = activeDocs.filter(doc => doc.z === undefined && isDocInView(doc, selRect)); // first see if there are any foreground docs to snap to
-        activeDocs
-            .filter(doc => doc.isGroup && SnappingManager.IsResizing !== doc[Id] && !DragManager.docsBeingDragged.includes(doc))
-            .forEach(doc => DocumentManager.Instance.getDocumentView(doc)?.ComponentView?.dragStarting?.(snapToDraggedDoc, false, visited));
+        activeDocs.filter(doc => doc.isGroup && SnappingManager.IsResizing !== doc[Id] && !DragManager.docsBeingDragged.includes(doc)).forEach(doc => DocumentView.getDocumentView(doc)?.ComponentView?.dragStarting?.(snapToDraggedDoc, false, visited));
 
         const horizLines: number[] = [];
         const vertLines: number[] = [];
@@ -1654,7 +1638,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                 nudge={this.isAnnotationOverlay || this._props.renderDepth > 0 ? undefined : this.nudge}
                 addDocTab={this.addDocTab}
                 slowLoadDocuments={this.slowLoadDocuments}
-                trySelectCluster={this._clusters.trySelectCluster}
+                trySelectCluster={this._clusters.tryToSelect}
                 activeDocuments={this.getActiveDocuments}
                 selectDocuments={this.selectDocuments}
                 addDocument={this.addDocument}
@@ -1744,22 +1728,22 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
 }
 // eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function nextKeyFrame(readOnly: boolean) {
-    !readOnly && (SelectionManager.Views[0].ComponentView as CollectionFreeFormView)?.changeKeyFrame();
+    !readOnly && (DocumentView.Selected()[0].ComponentView as CollectionFreeFormView)?.changeKeyFrame();
 });
 // eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function prevKeyFrame(readOnly: boolean) {
-    !readOnly && (SelectionManager.Views[0].ComponentView as CollectionFreeFormView)?.changeKeyFrame(true);
+    !readOnly && (DocumentView.Selected()[0].ComponentView as CollectionFreeFormView)?.changeKeyFrame(true);
 });
 // eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function curKeyFrame(readOnly: boolean) {
-    const selView = SelectionManager.Views;
+    const selView = DocumentView.Selected();
     if (readOnly) return selView[0].ComponentView?.getKeyFrameEditing?.() ? Colors.MEDIUM_BLUE : 'transparent';
     runInAction(() => selView[0].ComponentView?.setKeyFrameEditing?.(!selView[0].ComponentView?.getKeyFrameEditing?.()));
     return undefined;
 });
 // eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function pinWithView(pinContent: boolean) {
-    SelectionManager.Views.forEach(view =>
+    DocumentView.Selected().forEach(view =>
         view._props.pinToPres(view.Document, {
             currentFrame: Cast(view.Document.currentFrame, 'number', null),
             pinData: {
@@ -1772,16 +1756,16 @@ ScriptingGlobals.add(function pinWithView(pinContent: boolean) {
 });
 // eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function bringToFront() {
-    SelectionManager.Views.forEach(view => CollectionFreeFormView.from(view)?.bringToFront(view.Document));
+    DocumentView.Selected().forEach(view => CollectionFreeFormView.from(view)?.bringToFront(view.Document));
 });
 // eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function sendToBack() {
-    SelectionManager.Views.forEach(view => CollectionFreeFormView.from(view)?.bringToFront(view.Document, true));
+    DocumentView.Selected().forEach(view => CollectionFreeFormView.from(view)?.bringToFront(view.Document, true));
 });
 // eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function datavizFromSchema() {
     // creating a dataviz doc to represent the schema table
-    SelectionManager.Views.forEach(viewIn => {
+    DocumentView.Selected().forEach(viewIn => {
         const view = viewIn;
         if (!view.layoutDoc.schema_columnKeys) {
             view.layoutDoc.schema_columnKeys = new List<string>(['title', 'type', 'author', 'author_date']);
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index 3ab04f403..b96444024 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -17,15 +17,14 @@ import { CognitiveServices } from '../../../cognitive_services/CognitiveServices
 import { DocUtils } from '../../../documents/DocUtils';
 import { DocumentType } from '../../../documents/DocumentTypes';
 import { Docs, DocumentOptions } from '../../../documents/Documents';
-import { SelectionManager } from '../../../util/SelectionManager';
-import { freeformScrollMode } from '../../../util/SettingsManager';
-import { SnappingManager } from '../../../util/SnappingManager';
+import { SnappingManager, freeformScrollMode } from '../../../util/SnappingManager';
 import { Transform } from '../../../util/Transform';
 import { UndoManager, undoBatch } from '../../../util/UndoManager';
 import { ContextMenu } from '../../ContextMenu';
 import { ObservableReactComponent } from '../../ObservableReactComponent';
 import { MarqueeViewBounds } from '../../PinFuncs';
 import { PreviewCursor } from '../../PreviewCursor';
+import { DocumentView } from '../../nodes/DocumentView';
 import { OpenWhere } from '../../nodes/OpenWhere';
 import { pasteImageBitmap } from '../../nodes/WebBoxRenderer';
 import { FormattedTextBox } from '../../nodes/formattedText/FormattedTextBox';
@@ -175,7 +174,7 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
                 this.pasteTable(ns, x, y);
             })();
             e.stopPropagation();
-        } else if (!e.ctrlKey && !e.metaKey && SelectionManager.Views.length < 2) {
+        } else if (!e.ctrlKey && !e.metaKey && DocumentView.Selected().length < 2) {
             FormattedTextBox.SelectOnLoadChar = Doc.UserDoc().defaultTextLayout && !this._props.childLayoutString ? e.key : '';
             FormattedTextBox.LiveTextUndo = UndoManager.StartBatch('type new note');
             this._props.addLiveTextDocument(DocUtils.GetNewTextDoc('-typed text-', x, y, 200, 100));
@@ -249,7 +248,7 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
         if (this._visible) {
             const mselect = this.marqueeSelect();
             if (!e.shiftKey) {
-                SelectionManager.DeselectAll(mselect.length ? undefined : this._props.Document);
+                DocumentView.DeselectAll(mselect.length ? undefined : this._props.Document);
             }
             const docs = mselect.length ? mselect : [this._props.Document];
             this._props.selectDocuments(docs);
@@ -344,7 +343,7 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
     @undoBatch
     delete = action((e?: React.PointerEvent<Element> | KeyboardEvent | undefined, hide?: boolean) => {
         const selected = this.marqueeSelect(false);
-        SelectionManager.DeselectAll();
+        DocumentView.DeselectAll();
         selected.forEach(doc => {
             hide ? (doc.hidden = true) : this._props.removeDocument?.(doc);
         });
@@ -380,7 +379,7 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
     @undoBatch
     pileup = action(() => {
         const selected = this.marqueeSelect(false);
-        SelectionManager.DeselectAll();
+        DocumentView.DeselectAll();
         selected.forEach(d => this._props.removeDocument?.(d));
         const newCollection = DocUtils.pileup(selected, this.Bounds.left + this.Bounds.width / 2, this.Bounds.top + this.Bounds.height / 2)!;
         this._props.addDocument?.(newCollection);
@@ -390,7 +389,7 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
     });
 
     /**
-     * This triggers the TabDocView.PinDoc method which is the universal method
+     * This triggers the DocumentView.PinDoc method which is the universal method
      * used to pin documents to the currently active presentation trail.
      *
      * This one is unique in that it includes the bounds associated with marquee view.
diff --git a/src/client/views/collections/collectionLinear/CollectionLinearView.tsx b/src/client/views/collections/collectionLinear/CollectionLinearView.tsx
index e68ed0e17..eac0dc0e1 100644
--- a/src/client/views/collections/collectionLinear/CollectionLinearView.tsx
+++ b/src/client/views/collections/collectionLinear/CollectionLinearView.tsx
@@ -14,7 +14,6 @@ import { Id } from '../../../../fields/FieldSymbols';
 import { BoolCast, Cast, DocCast, NumCast, ScriptCast, StrCast } from '../../../../fields/Types';
 import { CollectionViewType } from '../../../documents/DocumentTypes';
 import { BranchingTrailManager } from '../../../util/BranchingTrailManager';
-import { DocumentManager } from '../../../util/DocumentManager';
 import { DragManager } from '../../../util/DragManager';
 import { dropActionType } from '../../../util/DropActionTypes';
 import { SettingsManager } from '../../../util/SettingsManager';
@@ -23,7 +22,6 @@ import { UndoStack } from '../../UndoStack';
 import { DocumentLinksButton } from '../../nodes/DocumentLinksButton';
 import { DocumentView } from '../../nodes/DocumentView';
 import { LinkDescriptionPopup } from '../../nodes/LinkDescriptionPopup';
-import { CollectionStackedTimeline } from '../CollectionStackedTimeline';
 import { CollectionSubView } from '../CollectionSubView';
 import './CollectionLinearView.scss';
 
@@ -128,14 +126,14 @@ export class CollectionLinearView extends CollectionSubView() {
             </span>
         );
     getCurrentlyPlayingUI = () =>
-        !CollectionStackedTimeline.CurrentlyPlaying?.length ? null : (
+        !DocumentView.CurrentlyPlaying?.length ? null : (
             <span className="bottomPopup-background">
                 <span className="bottomPopup-text">
                     Currently playing:
-                    {CollectionStackedTimeline.CurrentlyPlaying.map((clip, i) => (
+                    {DocumentView.CurrentlyPlaying.map((clip, i) => (
                         <>
-                            <span className="audio-title" onPointerDown={() => DocumentManager.Instance.showDocument(clip.Document, { willZoomCentered: true })}>
-                                {clip.Document.title + (i === CollectionStackedTimeline.CurrentlyPlaying.length - 1 ? '  ' : ',')}
+                            <span className="audio-title" onPointerDown={() => DocumentView.showDocument(clip.Document, { willZoomCentered: true })}>
+                                {clip.Document.title + (i === DocumentView.CurrentlyPlaying.length - 1 ? '  ' : ',')}
                             </span>
                             <FontAwesomeIcon icon={!clip.ComponentView?.IsPlaying?.() ? 'play' : 'pause'} size="lg" onPointerDown={() => clip.ComponentView?.TogglePause?.()} />{' '}
                             <FontAwesomeIcon icon="times" size="lg" onPointerDown={() => clip.ComponentView?.Pause?.()} />{' '}
diff --git a/src/client/views/collections/collectionMulticolumn/MulticolumnResizer.tsx b/src/client/views/collections/collectionMulticolumn/MulticolumnResizer.tsx
index 49ba85524..931e2c5e0 100644
--- a/src/client/views/collections/collectionMulticolumn/MulticolumnResizer.tsx
+++ b/src/client/views/collections/collectionMulticolumn/MulticolumnResizer.tsx
@@ -5,7 +5,7 @@ import * as React from 'react';
 import { Doc } from '../../../../fields/Doc';
 import { NumCast, StrCast } from '../../../../fields/Types';
 import { UndoManager } from '../../../util/UndoManager';
-import { StyleProp } from '../../StyleProvider';
+import { StyleProp } from '../../StyleProp';
 import { StyleProviderFuncType } from '../../nodes/FieldView';
 import { DimUnit } from './CollectionMulticolumnView';
 
diff --git a/src/client/views/collections/collectionMulticolumn/MultirowResizer.tsx b/src/client/views/collections/collectionMulticolumn/MultirowResizer.tsx
index ad77c327d..cff0a8b4c 100644
--- a/src/client/views/collections/collectionMulticolumn/MultirowResizer.tsx
+++ b/src/client/views/collections/collectionMulticolumn/MultirowResizer.tsx
@@ -5,7 +5,7 @@ import * as React from 'react';
 import { Doc } from '../../../../fields/Doc';
 import { NumCast, StrCast } from '../../../../fields/Types';
 import { UndoManager } from '../../../util/UndoManager';
-import { StyleProp } from '../../StyleProvider';
+import { StyleProp } from '../../StyleProp';
 import { StyleProviderFuncType } from '../../nodes/FieldView';
 import { DimUnit } from './CollectionMultirowView';
 
diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx
index 406a7d626..b30954ffd 100644
--- a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx
+++ b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx
@@ -15,16 +15,15 @@ import { ColumnType } from '../../../../fields/SchemaHeaderField';
 import { BoolCast, Cast, DocCast, NumCast, StrCast } from '../../../../fields/Types';
 import { DocUtils } from '../../../documents/DocUtils';
 import { Docs, DocumentOptions, FInfo } from '../../../documents/Documents';
-import { DocumentManager } from '../../../util/DocumentManager';
 import { DragManager } from '../../../util/DragManager';
 import { dropActionType } from '../../../util/DropActionTypes';
-import { SelectionManager } from '../../../util/SelectionManager';
 import { SettingsManager } from '../../../util/SettingsManager';
 import { undoBatch, undoable } from '../../../util/UndoManager';
 import { ContextMenu } from '../../ContextMenu';
 import { EditableView } from '../../EditableView';
 import { ObservableReactComponent } from '../../ObservableReactComponent';
-import { DefaultStyleProvider, StyleProp } from '../../StyleProvider';
+import { StyleProp } from '../../StyleProp';
+import { DefaultStyleProvider } from '../../StyleProvider';
 import { Colors } from '../../global/globalEnums';
 import { DocumentView } from '../../nodes/DocumentView';
 import { FieldViewProps } from '../../nodes/FieldView';
@@ -92,12 +91,12 @@ export class CollectionSchemaView extends CollectionSubView() {
     }
 
     @computed get _selectedDocs() {
-        const selected = SelectionManager.Docs.filter(doc => Doc.AreProtosEqual(DocCast(doc.embedContainer), this.Document));
+        const selected = DocumentView.SelectedDocs().filter(doc => Doc.AreProtosEqual(DocCast(doc.embedContainer), this.Document));
         if (!selected.length) {
             // if no schema doc is directly selected, test if a child of a schema doc is selected (such as in the preview window)
-            const childOfSchemaDoc = SelectionManager.Docs.find(sel => DocumentManager.GetContextPath(sel, true).includes(this.Document));
+            const childOfSchemaDoc = DocumentView.SelectedDocs().find(sel => DocumentView.getContextPath(sel, true).includes(this.Document));
             if (childOfSchemaDoc) {
-                const contextPath = DocumentManager.GetContextPath(childOfSchemaDoc, true);
+                const contextPath = DocumentView.getContextPath(childOfSchemaDoc, true);
                 return [contextPath[contextPath.indexOf(childOfSchemaDoc) - 1]]; // the schema doc that is "selected" by virtue of one of its children being selected
             }
         }
@@ -175,6 +174,7 @@ export class CollectionSchemaView extends CollectionSubView() {
         document.removeEventListener('keydown', this.onKeyDown);
     }
 
+    isUnstyledView = returnTrue; // used by style provide via ViewBoxInterface
     rowIndex = (doc: Doc) => this.sortedDocs.docs.indexOf(doc);
 
     @action
@@ -190,7 +190,7 @@ export class CollectionSchemaView extends CollectionSubView() {
                             !e.shiftKey && this.clearSelection();
                             const newDoc = this.sortedDocs.docs[lastIndex + 1];
                             if (this._selectedDocs.includes(newDoc)) {
-                                SelectionManager.DeselectView(DocumentManager.Instance.getFirstDocumentView(curDoc));
+                                DocumentView.DeselectView(DocumentView.getFirstDocumentView(curDoc));
                             } else {
                                 this.addDocToSelection(newDoc, e.shiftKey);
                                 this._selectedCell && (this._selectedCell[0] = newDoc);
@@ -209,7 +209,7 @@ export class CollectionSchemaView extends CollectionSubView() {
                         if (firstIndex > 0 && firstIndex < this.childDocs.length) {
                             !e.shiftKey && this.clearSelection();
                             const newDoc = this.sortedDocs.docs[firstIndex - 1];
-                            if (this._selectedDocs.includes(newDoc)) SelectionManager.DeselectView(DocumentManager.Instance.getFirstDocumentView(curDoc));
+                            if (this._selectedDocs.includes(newDoc)) DocumentView.DeselectView(DocumentView.getFirstDocumentView(curDoc));
                             else {
                                 this.addDocToSelection(newDoc, e.shiftKey);
                                 this._selectedCell && (this._selectedCell[0] = newDoc);
@@ -416,12 +416,12 @@ export class CollectionSchemaView extends CollectionSubView() {
 
     @action
     addDocToSelection = (doc: Doc, extendSelection: boolean) => {
-        const rowDocView = DocumentManager.Instance.getDocumentView(doc);
-        if (rowDocView) SelectionManager.SelectView(rowDocView, extendSelection);
+        const rowDocView = DocumentView.getDocumentView(doc);
+        if (rowDocView) DocumentView.SelectView(rowDocView, extendSelection);
     };
 
     @action
-    clearSelection = () => SelectionManager.DeselectAll();
+    clearSelection = () => DocumentView.DeselectAll();
 
     selectRows = (doc: Doc, lastSelected: Doc) => {
         const index = this.rowIndex(doc);
@@ -476,9 +476,9 @@ export class CollectionSchemaView extends CollectionSubView() {
             this.dataDoc[this.fieldKey ?? 'data'] = new List<Doc>([...removed, ...draggedDocs, ...pushedDocs]);
             this.clearSelection();
             draggedDocs.forEach(doc => {
-                const draggedView = DocumentManager.Instance.getFirstDocumentView(doc);
-                if (draggedView) DocumentManager.Instance.RemoveView(draggedView);
-                DocumentManager.Instance.AddViewRenderedCb(doc, dv => dv.select(true));
+                const draggedView = DocumentView.getFirstDocumentView(doc);
+                if (draggedView) DocumentView.removeView(draggedView);
+                DocumentView.addViewRenderedCb(doc, dv => dv.select(true));
             });
             return true;
         }
diff --git a/src/client/views/collections/collectionSchema/SchemaTableCell.tsx b/src/client/views/collections/collectionSchema/SchemaTableCell.tsx
index dfef3aa48..ee6987e89 100644
--- a/src/client/views/collections/collectionSchema/SchemaTableCell.tsx
+++ b/src/client/views/collections/collectionSchema/SchemaTableCell.tsx
@@ -19,9 +19,7 @@ import { ColumnType } from '../../../../fields/SchemaHeaderField';
 import { BoolCast, Cast, DateCast, DocCast, FieldValue, StrCast, toList } from '../../../../fields/Types';
 import { ImageField } from '../../../../fields/URLField';
 import { FInfo, FInfoFieldType } from '../../../documents/Documents';
-import { DocFocusOrOpen } from '../../../util/DocumentManager';
 import { dropActionType } from '../../../util/DropActionTypes';
-import { SettingsManager } from '../../../util/SettingsManager';
 import { SnappingManager } from '../../../util/SnappingManager';
 import { Transform } from '../../../util/Transform';
 import { undoBatch, undoable } from '../../../util/UndoManager';
@@ -29,7 +27,7 @@ import { EditableView } from '../../EditableView';
 import { ObservableReactComponent } from '../../ObservableReactComponent';
 import { DefaultStyleProvider } from '../../StyleProvider';
 import { Colors } from '../../global/globalEnums';
-import { returnEmptyDocViewList } from '../../nodes/DocumentView';
+import { DocFocusOrOpen, returnEmptyDocViewList } from '../../nodes/DocumentView';
 import { FieldViewProps } from '../../nodes/FieldView';
 import { KeyValueBox } from '../../nodes/KeyValueBox';
 import { FormattedTextBox } from '../../nodes/formattedText/FormattedTextBox';
@@ -300,8 +298,8 @@ export class SchemaDateCell extends ObservableReactComponent<SchemaTableCellProp
                         icon={<FontAwesomeIcon size="sm" icon="caret-down" />}
                         size={Size.XSMALL}
                         type={Type.TERT}
-                        color={SettingsManager.userColor}
-                        background={SettingsManager.userBackgroundColor}
+                        color={SnappingManager.userColor}
+                        background={SnappingManager.userBackgroundColor}
                         popup={
                             <div style={{ width: 'fit-content', height: '200px' }}>
                                 <DatePicker open dateFormat="Pp" selected={this.date?.date ?? Date.now()} onChange={this.handleChange} />
diff --git a/src/client/views/global/globalScripts.ts b/src/client/views/global/globalScripts.ts
index e75b01ab6..c595681b7 100644
--- a/src/client/views/global/globalScripts.ts
+++ b/src/client/views/global/globalScripts.ts
@@ -10,7 +10,6 @@ import { Gestures } from '../../../pen-gestures/GestureTypes';
 import { DocumentType } from '../../documents/DocumentTypes';
 import { LinkManager } from '../../util/LinkManager';
 import { ScriptingGlobals } from '../../util/ScriptingGlobals';
-import { SelectionManager } from '../../util/SelectionManager';
 import { UndoManager, undoable } from '../../util/UndoManager';
 import { GestureOverlay } from '../GestureOverlay';
 import { InkingStroke } from '../InkingStroke';
@@ -26,14 +25,14 @@ import { RichTextMenu } from '../nodes/formattedText/RichTextMenu';
 
 // eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function IsNoneSelected() {
-    return SelectionManager.Views.length <= 0;
+    return DocumentView.Selected().length <= 0;
 }, 'are no document selected');
 
 // toggle: Set overlay status of selected document
 // eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function setView(view: string, getSelected: boolean) {
-    if (getSelected) return SelectionManager.Docs;
-    const selected = SelectionManager.Docs.lastElement();
+    if (getSelected) return DocumentView.SelectedDocs();
+    const selected = DocumentView.SelectedDocs().lastElement();
     selected ? (selected._type_collection = view) : console.log('[FontIconBox.tsx] changeView failed');
     return undefined;
 });
@@ -41,7 +40,7 @@ ScriptingGlobals.add(function setView(view: string, getSelected: boolean) {
 // toggle: Set overlay status of selected document
 // eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function setBackgroundColor(color?: string, checkResult?: boolean) {
-    const selectedViews = SelectionManager.Views;
+    const selectedViews = DocumentView.Selected();
     if (Doc.ActiveTool !== InkTool.None) {
         if (checkResult) {
             return ActiveFillColor();
@@ -71,7 +70,7 @@ ScriptingGlobals.add(function setBackgroundColor(color?: string, checkResult?: b
             }
         });
     } else {
-        const selected = SelectionManager.Docs.length ? SelectionManager.Docs : LinkManager.Instance.currentLink ? [LinkManager.Instance.currentLink] : [];
+        const selected = DocumentView.SelectedDocs().length ? DocumentView.SelectedDocs() : LinkManager.Instance.currentLink ? [LinkManager.Instance.currentLink] : [];
         if (checkResult) {
             return selected.lastElement()?._backgroundColor ?? 'transparent';
         }
@@ -90,10 +89,10 @@ ScriptingGlobals.add(function setDefaultTemplate(checkResult?: boolean) {
 // eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function setHeaderColor(color?: string, checkResult?: boolean) {
     if (checkResult) {
-        return SelectionManager.Views.length ? StrCast(SelectionManager.Docs.lastElement().layout_headingColor) : Doc.SharingDoc().headingColor;
+        return DocumentView.Selected().length ? StrCast(DocumentView.SelectedDocs().lastElement().layout_headingColor) : Doc.SharingDoc().headingColor;
     }
-    if (SelectionManager.Views.length) {
-        SelectionManager.Docs.forEach(doc => {
+    if (DocumentView.Selected().length) {
+        DocumentView.SelectedDocs().forEach(doc => {
             doc[DocData].layout_headingColor = color === 'transparent' ? undefined : color;
             doc.layout_showTitle = color === 'transparent' ? undefined : StrCast(doc.layout_showTitle, 'title');
         });
@@ -108,7 +107,7 @@ ScriptingGlobals.add(function setHeaderColor(color?: string, checkResult?: boole
 // toggle: Set overlay status of selected document
 // eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function toggleOverlay(checkResult?: boolean) {
-    const selected = SelectionManager.Views.length ? SelectionManager.Views[0] : undefined;
+    const selected = DocumentView.Selected().length ? DocumentView.Selected()[0] : undefined;
     if (checkResult) {
         if (NumCast(selected?.Document.z) >= 1) return true;
         return false;
@@ -119,7 +118,7 @@ ScriptingGlobals.add(function toggleOverlay(checkResult?: boolean) {
 
 // eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function showFreeform(attr: 'center' | 'grid' | 'snaplines' | 'clusters' | 'viewAll' | 'fitOnce', checkResult?: boolean, persist?: boolean) {
-    const selected = SelectionManager.Docs.lastElement();
+    const selected = DocumentView.SelectedDocs().lastElement();
     // prettier-ignore
     const map: Map<'center' |'grid' | 'snaplines' | 'clusters' | 'viewAll' | 'fitOnce', { waitForRender?: boolean, checkResult: (doc:Doc) => any; setDoc: (doc:Doc, dv:DocumentView) => void;}> = new Map([
         ['grid', {
@@ -153,7 +152,7 @@ ScriptingGlobals.add(function showFreeform(attr: 'center' | 'grid' | 'snaplines'
         return map.get(attr)?.checkResult(selected);
     }
     const batch = map.get(attr)?.waitForRender ? UndoManager.StartBatch('set freeform attribute') : { end: () => {} };
-    SelectionManager.Views.map(dv => map.get(attr)?.setDoc(dv.layoutDoc, dv));
+    DocumentView.Selected().map(dv => map.get(attr)?.setDoc(dv.layoutDoc, dv));
     setTimeout(() => batch.end(), 100);
     return undefined;
 });
@@ -347,7 +346,7 @@ ScriptingGlobals.add(setActiveTool, 'sets the active ink tool mode');
 // toggle: Set overlay status of selected document
 // eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function setInkProperty(option: 'inkMask' | 'labels' | 'fillColor' | 'strokeWidth' | 'strokeColor', value: any, checkResult?: boolean) {
-    const selected = SelectionManager.Docs.lastElement() ?? Doc.UserDoc();
+    const selected = DocumentView.SelectedDocs().lastElement() ?? Doc.UserDoc();
     // prettier-ignore
     const map: Map<'inkMask' | 'labels' | 'fillColor' | 'strokeWidth' | 'strokeColor', { checkResult: () => any; setInk: (doc: Doc) => void; setMode: () => void }> = new Map([
         ['inkMask', {
@@ -381,7 +380,20 @@ ScriptingGlobals.add(function setInkProperty(option: 'inkMask' | 'labels' | 'fil
         return map.get(option)?.checkResult();
     }
     map.get(option)?.setMode();
-    SelectionManager.Docs.filter(doc => doc._layout_isSvg).map(doc => map.get(option)?.setInk(doc));
+    DocumentView.SelectedDocs()
+        .filter(doc => doc._layout_isSvg)
+        .map(doc => map.get(option)?.setInk(doc));
+    return undefined;
+});
+
+// eslint-disable-next-line prefer-arrow-callback
+ScriptingGlobals.add(function toggleRaiseOnDrag(readOnly?: boolean) {
+    if (readOnly) {
+        return DocumentView.Selected().some(dv => dv.Document.keepZWhenDragged);
+    }
+    DocumentView.Selected().forEach(dv => {
+        dv.Document.keepZWhenDragged = !dv.Document.keepZWhenDragged;
+    });
     return undefined;
 });
 
@@ -390,7 +402,7 @@ ScriptingGlobals.add(function setInkProperty(option: 'inkMask' | 'labels' | 'fil
  * */
 // eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function webSetURL(url: string, checkResult?: boolean) {
-    const selected = SelectionManager.Views.lastElement();
+    const selected = DocumentView.Selected().lastElement();
     if (selected?.Document.type === DocumentType.WEB) {
         if (checkResult) {
             return StrCast(selected.Document.data, Cast(selected.Document.data, WebField, null)?.url?.href);
@@ -401,7 +413,7 @@ ScriptingGlobals.add(function webSetURL(url: string, checkResult?: boolean) {
 });
 // eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function webForward(checkResult?: boolean) {
-    const selected = SelectionManager.Views.lastElement()?.ComponentView as WebBox;
+    const selected = DocumentView.Selected().lastElement()?.ComponentView as WebBox;
     if (checkResult) {
         return selected?.forward(checkResult) ? undefined : 'lightGray';
     }
@@ -410,24 +422,24 @@ ScriptingGlobals.add(function webForward(checkResult?: boolean) {
 });
 // eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function webBack() {
-    const selected = SelectionManager.Views.lastElement()?.ComponentView as WebBox;
+    const selected = DocumentView.Selected().lastElement()?.ComponentView as WebBox;
     selected?.back();
 });
 
 // eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function videoSnapshot() {
-    const selected = SelectionManager.Views.lastElement()?.ComponentView as VideoBox;
+    const selected = DocumentView.Selected().lastElement()?.ComponentView as VideoBox;
     selected?.Snapshot();
 });
 
 // eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function imageSetPixelSize() {
-    const selected = SelectionManager.Views.lastElement()?.ComponentView as ImageBox;
+    const selected = DocumentView.Selected().lastElement()?.ComponentView as ImageBox;
     selected?.setNativeSize();
 });
 // eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function imageRotate90() {
-    const selected = SelectionManager.Views.lastElement()?.ComponentView as ImageBox;
+    const selected = DocumentView.Selected().lastElement()?.ComponentView as ImageBox;
     selected?.rotate();
 });
 
@@ -436,7 +448,7 @@ ScriptingGlobals.add(function imageRotate90() {
  * */
 // eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function toggleSchemaPreview(checkResult?: boolean) {
-    const selected = SelectionManager.Docs.lastElement();
+    const selected = DocumentView.SelectedDocs().lastElement();
     if (checkResult && selected) {
         const result: boolean = NumCast(selected.schema_previewWidth) > 0;
         if (result) return Colors.MEDIUM_BLUE;
@@ -453,7 +465,7 @@ ScriptingGlobals.add(function toggleSchemaPreview(checkResult?: boolean) {
 });
 // eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function toggleSingleLineSchema(checkResult?: boolean) {
-    const selected = SelectionManager.Docs.lastElement();
+    const selected = DocumentView.SelectedDocs().lastElement();
     if (checkResult && selected) {
         return NumCast(selected._schema_singleLine) > 0 ? Colors.MEDIUM_BLUE : 'transparent';
     }
@@ -468,7 +480,7 @@ ScriptingGlobals.add(function toggleSingleLineSchema(checkResult?: boolean) {
  */
 // eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function setGroupBy(key: string, checkResult?: boolean) {
-    SelectionManager.Docs.forEach(doc => { doc._text_fontFamily = key; }); // prettier-ignore
+    DocumentView.SelectedDocs().forEach(doc => { doc._text_fontFamily = key; }); // prettier-ignore
     const editorView = RichTextMenu.Instance?.TextView?.EditorView;
     if (checkResult) {
         return StrCast((editorView ? RichTextMenu.Instance : Doc.UserDoc())?.fontFamily);
diff --git a/src/client/views/linking/LinkMenuGroup.tsx b/src/client/views/linking/LinkMenuGroup.tsx
index f99a18db2..cd735318e 100644
--- a/src/client/views/linking/LinkMenuGroup.tsx
+++ b/src/client/views/linking/LinkMenuGroup.tsx
@@ -8,7 +8,6 @@ import { Doc, StrListCast } from '../../../fields/Doc';
 import { Id } from '../../../fields/FieldSymbols';
 import { Cast, DocCast } from '../../../fields/Types';
 import { DocumentType } from '../../documents/DocumentTypes';
-import { LinkManager } from '../../util/LinkManager';
 import { DocumentView } from '../nodes/DocumentView';
 import './LinkMenu.scss';
 import { LinkMenuItem } from './LinkMenuItem';
@@ -59,8 +58,7 @@ export class LinkMenuGroup extends React.Component<LinkMenuGroupProps> {
                   ? this.props.docView._props.LayoutTemplateString?.includes('link_anchor_1')
                       ? 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));
+                  : Doc.getOppositeAnchor(linkDoc, sourceDoc) || Doc.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/linking/LinkMenuItem.tsx b/src/client/views/linking/LinkMenuItem.tsx
index df3accb0d..9ce04ffac 100644
--- a/src/client/views/linking/LinkMenuItem.tsx
+++ b/src/client/views/linking/LinkMenuItem.tsx
@@ -11,13 +11,10 @@ import { Doc } from '../../../fields/Doc';
 import { Cast, DocCast, StrCast } from '../../../fields/Types';
 import { WebField } from '../../../fields/URLField';
 import { DocumentType } from '../../documents/DocumentTypes';
-import { DocumentManager } from '../../util/DocumentManager';
 import { DragManager } from '../../util/DragManager';
 import { dropActionType } from '../../util/DropActionTypes';
-import { LinkFollower } from '../../util/LinkFollower';
 import { LinkManager } from '../../util/LinkManager';
-import { SelectionManager } from '../../util/SelectionManager';
-import { SettingsManager } from '../../util/SettingsManager';
+import { SnappingManager } from '../../util/SnappingManager';
 import { undoBatch } from '../../util/UndoManager';
 import { ObservableReactComponent } from '../ObservableReactComponent';
 import { DocumentView, DocumentViewInternal } from '../nodes/DocumentView';
@@ -38,7 +35,7 @@ interface LinkMenuItemProps {
 
 // drag links and drop link targets (embedding them if needed)
 export async function StartLinkTargetsDrag(dragEle: HTMLElement, docView: DocumentView, downX: number, downY: number, sourceDoc: Doc, specificLinks?: Doc[]) {
-    const draggedDocs = (specificLinks || LinkManager.Links(sourceDoc)).map(link => LinkManager.getOppositeAnchor(link, sourceDoc)).filter(l => l) as Doc[];
+    const draggedDocs = (specificLinks || LinkManager.Links(sourceDoc)).map(link => Doc.getOppositeAnchor(link, sourceDoc)).filter(l => l) as Doc[];
 
     if (draggedDocs.length) {
         const moddrag: Doc[] = [];
@@ -81,7 +78,7 @@ export class LinkMenuItem extends ObservableReactComponent<LinkMenuItemProps> {
 
     onIconDown = (e: React.PointerEvent) => {
         setupMoveUpEvents(this, e, returnFalse, returnFalse, () => {
-            const ancestor = DocumentManager.LinkCommonAncestor(this._props.linkDoc);
+            const ancestor = DocumentView.linkCommonAncestor(this._props.linkDoc);
             if (!ancestor?.ComponentView?.removeDocument?.(this._props.linkDoc)) {
                 ancestor?.ComponentView?.addDocument?.(this._props.linkDoc);
             }
@@ -105,14 +102,14 @@ export class LinkMenuItem extends ObservableReactComponent<LinkMenuItemProps> {
                     Doc.ActivePresentation = trail;
                     DocumentViewInternal.addDocTabFunc(trail, OpenWhere.replaceRight);
                 } else {
-                    SelectionManager.SelectView(this._props.docView, false);
+                    DocumentView.SelectView(this._props.docView, false);
                     LinkManager.Instance.currentLink = this._props.linkDoc === LinkManager.Instance.currentLink ? undefined : this._props.linkDoc;
                     LinkManager.Instance.currentLinkAnchor = LinkManager.Instance.currentLink ? this.sourceAnchor : undefined;
 
-                    if ((SettingsManager.Instance.propertiesWidth ?? 0) < 100) {
+                    if ((SnappingManager.PropertiesWidth ?? 0) < 100) {
                         setTimeout(
                             action(() => {
-                                SettingsManager.Instance.propertiesWidth = 250;
+                                SnappingManager.SetPropertiesWidth(250);
                             })
                         );
                     }
@@ -126,10 +123,12 @@ export class LinkMenuItem extends ObservableReactComponent<LinkMenuItemProps> {
             this,
             e,
             moveEv => {
-                const eleClone: any = this._drag.current!.cloneNode(true);
-                eleClone.style.transform = `translate(${moveEv.x}px, ${moveEv.y}px)`;
-                StartLinkTargetsDrag(eleClone, this._props.docView, moveEv.x, moveEv.y, this._props.sourceDoc, [this._props.linkDoc]);
-                this._props.clearLinkEditor?.();
+                const eleClone: any = this._drag.current?.cloneNode(true);
+                if (eleClone) {
+                    eleClone.style.transform = `translate(${moveEv.x}px, ${moveEv.y}px)`;
+                    StartLinkTargetsDrag(eleClone, this._props.docView, moveEv.x, moveEv.y, this._props.sourceDoc, [this._props.linkDoc]);
+                    this._props.clearLinkEditor?.();
+                }
                 return true;
             },
             emptyFunction,
@@ -146,13 +145,13 @@ export class LinkMenuItem extends ObservableReactComponent<LinkMenuItemProps> {
                               : undefined;
 
                     if (focusDoc) this._props.docView._props.focus(focusDoc, { instant: true });
-                    LinkFollower.FollowLink(this._props.linkDoc, this._props.sourceDoc, false);
+                    DocumentView.FollowLink(this._props.linkDoc, this._props.sourceDoc, false);
                 }
             }
         );
     };
 
-    deleteLink = (e: React.PointerEvent): void => setupMoveUpEvents(this, e, returnFalse, emptyFunction, undoBatch(action(() => LinkManager.Instance.deleteLink(this._props.linkDoc))));
+    deleteLink = (e: React.PointerEvent): void => setupMoveUpEvents(this, e, returnFalse, emptyFunction, undoBatch(action(() => Doc.DeleteLink?.(this._props.linkDoc))));
     @observable _hover = false;
     docView = () => this._props.docView;
     render() {
@@ -181,7 +180,7 @@ export class LinkMenuItem extends ObservableReactComponent<LinkMenuItemProps> {
                 style={{
                     fontSize: this._hover ? 'larger' : undefined,
                     fontWeight: this._hover ? 'bold' : undefined,
-                    background: LinkManager.Instance.currentLink === this._props.linkDoc ? SettingsManager.userVariantColor : SettingsManager.userBackgroundColor,
+                    background: LinkManager.Instance.currentLink === this._props.linkDoc ? SnappingManager.userVariantColor : SnappingManager.userBackgroundColor,
                 }}>
                 <div className="linkMenu-item-content expand-two">
                     <div
diff --git a/src/client/views/newlightbox/ButtonMenu/ButtonMenu.tsx b/src/client/views/newlightbox/ButtonMenu/ButtonMenu.tsx
index 72d81e9eb..e48e993cf 100644
--- a/src/client/views/newlightbox/ButtonMenu/ButtonMenu.tsx
+++ b/src/client/views/newlightbox/ButtonMenu/ButtonMenu.tsx
@@ -2,7 +2,6 @@ import { action } from 'mobx';
 import * as React from 'react';
 import { Doc } from '../../../../fields/Doc';
 import { InkTool } from '../../../../fields/InkField';
-import { SelectionManager } from '../../../util/SelectionManager';
 import { SnappingManager } from '../../../util/SnappingManager';
 import { CollectionDockingView } from '../../collections/CollectionDockingView';
 import { OpenWhereMod } from '../../nodes/OpenWhere';
@@ -26,7 +25,7 @@ export const ButtonMenu = (props: IButtonMenu) => {
                 onClick={e => {
                     e.stopPropagation();
                     CollectionDockingView.AddSplit(NewLightboxView.LightboxDoc || NewLightboxView.LightboxDoc!, OpenWhereMod.none);
-                    SelectionManager.DeselectAll();
+                    DocumentView.DeselectAll();
                     NewLightboxView.SetNewLightboxDoc(undefined);
                 }}></div>
             <div
diff --git a/src/client/views/newlightbox/NewLightboxView.tsx b/src/client/views/newlightbox/NewLightboxView.tsx
index dc6bf3e9c..558ce7e38 100644
--- a/src/client/views/newlightbox/NewLightboxView.tsx
+++ b/src/client/views/newlightbox/NewLightboxView.tsx
@@ -9,16 +9,11 @@ import { emptyFunction } from '../../../Utils';
 import { CreateLinkToActiveAudio, Doc, DocListCast, Opt } from '../../../fields/Doc';
 import { InkTool } from '../../../fields/InkField';
 import { Cast, NumCast, StrCast, toList } from '../../../fields/Types';
-import { DocumentManager } from '../../util/DocumentManager';
-import { LinkManager } from '../../util/LinkManager';
-import { SelectionManager } from '../../util/SelectionManager';
 import { SnappingManager } from '../../util/SnappingManager';
 import { Transform } from '../../util/Transform';
 import { GestureOverlay } from '../GestureOverlay';
 import { LightboxView } from '../LightboxView';
 import { DefaultStyleProvider } from '../StyleProvider';
-import { CollectionStackedTimeline } from '../collections/CollectionStackedTimeline';
-import { TabDocView } from '../collections/TabDocView';
 import { DocumentView } from '../nodes/DocumentView';
 import { OpenWhere } from '../nodes/OpenWhere';
 import { ExploreView } from './ExploreView';
@@ -79,7 +74,7 @@ export class NewLightboxView extends React.Component<LightboxViewProps> {
         }
     }
     public static AddDocTab = (docsIn: Doc | Doc[], location: OpenWhere, layoutTemplate?: Doc | string) => {
-        SelectionManager.DeselectAll();
+        DocumentView.DeselectAll();
         const doc = toList(docsIn).lastElement();
         return (
             doc &&
@@ -110,8 +105,8 @@ export class NewLightboxView extends React.Component<LightboxViewProps> {
         } else {
             const l = CreateLinkToActiveAudio(() => doc).lastElement();
             l && (Cast(l.link_anchor_2, Doc, null).backgroundColor = 'lightgreen');
-            CollectionStackedTimeline.CurrentlyPlaying?.forEach(dv => dv.ComponentView?.Pause?.());
-            // TabDocView.PinDoc(doc, { hidePresBox: true });
+            DocumentView.CurrentlyPlaying?.forEach(dv => dv.ComponentView?.Pause?.());
+            // DocumentView.PinDoc(doc, { hidePresBox: true });
             this._history ? this._history.push({ doc, target }) : (this._history = [{ doc, target }]);
             if (doc !== LightboxView.LightboxDoc) {
                 this._savedState = {
@@ -130,7 +125,7 @@ export class NewLightboxView extends React.Component<LightboxViewProps> {
                 ...future
                     .slice()
                     .sort((a, b) => NumCast(b._timecodeToShow) - NumCast(a._timecodeToShow))
-                    .sort((a, b) => LinkManager.Links(a).length - LinkManager.Links(b).length),
+                    .sort((a, b) => Doc.Links(a).length - Doc.Links(b).length),
             ];
         }
         this._doc = doc;
@@ -148,11 +143,11 @@ export class NewLightboxView extends React.Component<LightboxViewProps> {
     @action public static Next() {
         const doc = NewLightboxView._doc!;
         const target = (NewLightboxView._docTarget = this._future?.pop());
-        const targetDocView = target && DocumentManager.Instance.getLightboxDocumentView(target);
+        const targetDocView = target && DocumentView.getLightboxDocumentView(target);
         if (targetDocView && target) {
             const l = CreateLinkToActiveAudio(() => targetDocView.ComponentView?.getAnchor?.(true) || target).lastElement();
             l && (Cast(l.link_anchor_2, Doc, null).backgroundColor = 'lightgreen');
-            DocumentManager.Instance.showDocument(target, { willZoomCentered: true, zoomScale: 0.9 });
+            DocumentView.showDocument(target, { willZoomCentered: true, zoomScale: 0.9 });
             if (NewLightboxView._history?.lastElement().target !== target) NewLightboxView._history?.push({ doc, target });
         } else if (!target && NewLightboxView.path.length) {
             const saved = NewLightboxView._savedState;
@@ -182,10 +177,10 @@ export class NewLightboxView extends React.Component<LightboxViewProps> {
             return;
         }
         const { doc, target } = NewLightboxView._history?.lastElement();
-        const docView = DocumentManager.Instance.getLightboxDocumentView(target || doc);
+        const docView = DocumentView.getLightboxDocumentView(target || doc);
         if (docView) {
             NewLightboxView._docTarget = target;
-            target && DocumentManager.Instance.showDocument(target, { willZoomCentered: true, zoomScale: 0.9 });
+            target && DocumentView.showDocument(target, { willZoomCentered: true, zoomScale: 0.9 });
         } else {
             NewLightboxView.SetNewLightboxDoc(doc, target);
         }
@@ -277,7 +272,7 @@ export class NewLightboxView extends React.Component<LightboxViewProps> {
                     removeDocument={undefined}
                     whenChildContentsActiveChanged={emptyFunction}
                     addDocTab={this.addDocTab}
-                    pinToPres={TabDocView.PinDoc}
+                    pinToPres={DocumentView.PinDoc}
                     focus={emptyFunction}
                 />
             </GestureOverlay>
@@ -316,8 +311,8 @@ export class NewLightboxView extends React.Component<LightboxViewProps> {
         if (coll) {
             const fieldKey = Doc.LayoutFieldKey(coll);
             const contents = [...DocListCast(coll[fieldKey]), ...DocListCast(coll[fieldKey + '_annotations'])];
-            const links = LinkManager.Links(coll)
-                .map(link => LinkManager.getOppositeAnchor(link, coll))
+            const links = Doc.Links(coll)
+                .map(link => Doc.getOppositeAnchor(link, coll))
                 .filter(doc => doc)
                 .map(doc => doc!);
             NewLightboxView.SetNewLightboxDoc(coll, undefined, contents.length ? contents : links);
diff --git a/src/client/views/newlightbox/components/Recommendation/Recommendation.tsx b/src/client/views/newlightbox/components/Recommendation/Recommendation.tsx
index 23027808f..375408d01 100644
--- a/src/client/views/newlightbox/components/Recommendation/Recommendation.tsx
+++ b/src/client/views/newlightbox/components/Recommendation/Recommendation.tsx
@@ -1,12 +1,12 @@
 import * as React from 'react';
-import { IRecommendation } from './utils';
-import './Recommendation.scss';
-import { getType } from '../../utils';
 import { FaEyeSlash } from 'react-icons/fa';
-import { NewLightboxView } from '../../NewLightboxView';
-import { DocumentManager } from '../../../../util/DocumentManager';
 import { Doc } from '../../../../../fields/Doc';
 import { Docs } from '../../../../documents/Documents';
+import { DocumentView } from '../../../nodes/DocumentView';
+import { NewLightboxView } from '../../NewLightboxView';
+import { getType } from '../../utils';
+import './Recommendation.scss';
+import { IRecommendation } from './utils';
 
 export const Recommendation = (props: IRecommendation) => {
     const { title, data, type, text, transcript, loading, source, previewUrl, related_concepts, distance, docId } = props;
@@ -17,7 +17,7 @@ export const Recommendation = (props: IRecommendation) => {
             onClick={() => {
                 let doc: Doc | null = null;
                 if (source == 'Dash' && docId) {
-                    const docView = DocumentManager.Instance.getDocumentViewsById(docId).lastElement();
+                    const docView = DocumentView.getDocumentViewsById(docId).lastElement();
                     if (docView) {
                         doc = docView.Document;
                     }
diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx
index 4697491e0..9deed4de4 100644
--- a/src/client/views/nodes/AudioBox.tsx
+++ b/src/client/views/nodes/AudioBox.tsx
@@ -18,14 +18,15 @@ import { DocumentType } from '../../documents/DocumentTypes';
 import { DocUtils } from '../../documents/DocUtils';
 import { Networking } from '../../Network';
 import { DragManager } from '../../util/DragManager';
-import { LinkManager } from '../../util/LinkManager';
 import { undoBatch } from '../../util/UndoManager';
 import { CollectionStackedTimeline, TrimScope } from '../collections/CollectionStackedTimeline';
 import { ContextMenu } from '../ContextMenu';
 import { ContextMenuProps } from '../ContextMenuItem';
 import { ViewBoxAnnotatableComponent } from '../DocComponent';
+import { DocViewUtils } from '../DocViewUtils';
 import { PinDocView, PinProps } from '../PinFuncs';
 import './AudioBox.scss';
+import { DocumentView } from './DocumentView';
 import { FieldView, FieldViewProps } from './FieldView';
 import { OpenWhere } from './OpenWhere';
 
@@ -99,7 +100,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
         return this._props.PanelHeight() < 50;
     } // used to collapse timeline when node is shrunk
     @computed get links() {
-        return LinkManager.Links(this.dataDoc);
+        return Doc.Links(this.dataDoc);
     }
     @computed get mediaState() {
         return this.dataDoc.mediaState as mediaState;
@@ -218,9 +219,9 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
     @action
     removeCurrentlyPlaying = () => {
         const docView = this.DocumentView?.();
-        if (CollectionStackedTimeline.CurrentlyPlaying && docView) {
-            const index = CollectionStackedTimeline.CurrentlyPlaying.indexOf(docView);
-            index !== -1 && CollectionStackedTimeline.CurrentlyPlaying.splice(index, 1);
+        if (DocumentView.CurrentlyPlaying && docView) {
+            const index = DocumentView.CurrentlyPlaying.indexOf(docView);
+            index !== -1 && DocumentView.CurrentlyPlaying.splice(index, 1);
         }
     };
 
@@ -228,11 +229,11 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
     @action
     addCurrentlyPlaying = () => {
         const docView = this.DocumentView?.();
-        if (!CollectionStackedTimeline.CurrentlyPlaying) {
-            CollectionStackedTimeline.CurrentlyPlaying = [];
+        if (!DocumentView.CurrentlyPlaying) {
+            DocumentView.CurrentlyPlaying = [];
         }
-        if (docView && CollectionStackedTimeline.CurrentlyPlaying.indexOf(docView) === -1) {
-            CollectionStackedTimeline.CurrentlyPlaying.push(docView);
+        if (docView && DocumentView.CurrentlyPlaying.indexOf(docView) === -1) {
+            DocumentView.CurrentlyPlaying.push(docView);
         }
     };
 
@@ -251,7 +252,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
         this._stream = await navigator.mediaDevices.getUserMedia({ audio: true });
         this._recorder = new MediaRecorder(this._stream);
         this.dataDoc[this.fieldKey + '_recordingStart'] = new DateField();
-        DocUtils.ActiveRecordings.push(this);
+        DocViewUtils.ActiveRecordings.push(this);
         this._recorder.ondataavailable = async (e: any) => {
             const [{ result }] = await Networking.UploadFilesToServer({ file: e.data });
             if (!(result instanceof Error)) {
@@ -278,8 +279,8 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
             this.dataDoc[this.fieldKey + '_duration'] = (now - this._recordStart - this._pausedTime) / 1000;
             this.mediaState = mediaState.Paused;
             this._stream?.getAudioTracks()[0].stop();
-            const ind = DocUtils.ActiveRecordings.indexOf(this);
-            ind !== -1 && DocUtils.ActiveRecordings.splice(ind, 1);
+            const ind = DocViewUtils.ActiveRecordings.indexOf(this);
+            ind !== -1 && DocViewUtils.ActiveRecordings.splice(ind, 1);
         }
     };
 
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
index 691d07e31..685a5aca4 100644
--- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
@@ -5,18 +5,16 @@ import { OmitKeys } from '../../../ClientUtils';
 import { numberRange } from '../../../Utils';
 import { Doc, DocListCast, Opt } from '../../../fields/Doc';
 import { TransitionTimer } from '../../../fields/DocSymbols';
+import { InkField } from '../../../fields/InkField';
 import { List } from '../../../fields/List';
 import { listSpec } from '../../../fields/Schema';
 import { ComputedField } from '../../../fields/ScriptField';
 import { Cast, NumCast, StrCast } from '../../../fields/Types';
 import { TraceMobx } from '../../../fields/util';
-import { DocumentManager } from '../../util/DocumentManager';
 import { DragManager } from '../../util/DragManager';
 import { ScriptingGlobals } from '../../util/ScriptingGlobals';
-import { SelectionManager } from '../../util/SelectionManager';
 import { DocComponent } from '../DocComponent';
-import { StyleProp } from '../StyleProvider';
-import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView';
+import { StyleProp } from '../StyleProp';
 import './CollectionFreeFormDocumentView.scss';
 import { DocumentView, DocumentViewProps } from './DocumentView';
 import { FieldViewProps } from './FieldView';
@@ -43,7 +41,8 @@ interface freeFormProps {
 
 export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps {
     RenderCutoffProvider: (doc: Doc) => boolean;
-    CollectionFreeFormView: CollectionFreeFormView;
+    isAnyChildContentActive: () => boolean;
+    parent: any;
 }
 @observer
 export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeFormDocumentViewProps & freeFormProps>() {
@@ -66,7 +65,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
     ]; // fields that are configured to be animatable using animation frames
     public static animStringFields = ['backgroundColor', 'color', 'fillColor']; // fields that are configured to be animatable using animation frames
     public static animDataFields = (doc: Doc) => (Doc.LayoutFieldKey(doc) ? [Doc.LayoutFieldKey(doc)] : []); // fields that are configured to be animatable using animation frames
-    public static from(dv?: DocumentView) {
+    public static from(dv?: DocumentView): CollectionFreeFormDocumentView | undefined {
         return dv?._props.parent instanceof CollectionFreeFormDocumentView ? dv._props.parent : undefined;
     }
 
@@ -120,7 +119,6 @@ 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.Transition || StrCast(this.Document.dataTransition); // prettier-ignore
     RenderCutoffProvider = this.props.RenderCutoffProvider; // needed for type checking
@@ -185,13 +183,32 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
             const currentFrame = Cast(doc._currentFrame, 'number', null);
             if (currentFrame === undefined) {
                 doc._currentFrame = 0;
-                CollectionFreeFormDocumentView.setupKeyframes(childDocs, 0);
+                this.setupKeyframes(childDocs, 0);
             }
-            CollectionFreeFormView.updateKeyframe(undefined, [...childDocs, doc], currentFrame || 0);
+            this.updateKeyframe(undefined, [...childDocs, doc], currentFrame || 0);
             doc._currentFrame = newFrame === undefined ? 0 : Math.max(0, newFrame);
         }
     }
 
+    public static updateKeyframe(timer: NodeJS.Timeout | undefined, docs: Doc[], time: number) {
+        const newTimer = DocumentView.SetViewTransition(docs, 'all', 1000, timer, undefined, true);
+        const timecode = Math.round(time);
+        docs.forEach(doc => {
+            this.animFields.forEach(val => {
+                const findexed = Cast(doc[`${val.key}_indexed`], listSpec('number'), null);
+                findexed?.length <= timecode + 1 && findexed.push(undefined as any as number);
+            });
+            this.animStringFields.forEach(val => {
+                const findexed = Cast(doc[`${val}_indexed`], listSpec('string'), null);
+                findexed?.length <= timecode + 1 && findexed.push(undefined as any as string);
+            });
+            this.animDataFields(doc).forEach(val => {
+                const findexed = Cast(doc[`${val}_indexed`], listSpec(InkField), null);
+                findexed?.length <= timecode + 1 && findexed.push(undefined as any);
+            });
+        });
+        return newTimer;
+    }
     public static setupKeyframes(docs: Doc[], currTimecode: number, makeAppear: boolean = false) {
         docs.forEach(doc => {
             if (doc.appearFrame === undefined) doc.appearFrame = currTimecode;
@@ -220,7 +237,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
         const containerDocView = this._props.containerViewPath?.().lastElement();
         const screenXf = containerDocView?.screenToContentsTransform();
         if (screenXf) {
-            SelectionManager.DeselectAll();
+            DocumentView.DeselectAll();
             if (topDoc.z) {
                 const spt = screenXf.inverse().transformPoint(NumCast(topDoc.x), NumCast(topDoc.y));
                 topDoc.z = 0;
@@ -233,7 +250,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
                 topDoc.z = 1;
                 [topDoc.x, topDoc.y] = fpt;
             }
-            setTimeout(() => SelectionManager.SelectView(DocumentManager.Instance.getDocumentView(topDoc, containerDocView), false), 0);
+            setTimeout(() => DocumentView.SelectView(DocumentView.getDocumentView(topDoc, containerDocView), false), 0);
         }
     };
 
@@ -255,7 +272,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
     //  'inactive' - this is a group child but it is not active
     //  undefined - this is not activated by a group
     isGroupActive = () => {
-        if (this.CollectionFreeFormView.isAnyChildContentActive()) return undefined;
+        if (this._props.isAnyChildContentActive()) return undefined;
         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;
diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx
index c42ca4468..6ae6bb228 100644
--- a/src/client/views/nodes/ComparisonBox.tsx
+++ b/src/client/views/nodes/ComparisonBox.tsx
@@ -15,7 +15,7 @@ import { dropActionType } from '../../util/DropActionTypes';
 import { undoBatch } from '../../util/UndoManager';
 import { ViewBoxAnnotatableComponent, ViewBoxInterface } from '../DocComponent';
 import { PinDocView, PinProps } from '../PinFuncs';
-import { StyleProp } from '../StyleProvider';
+import { StyleProp } from '../StyleProp';
 import './ComparisonBox.scss';
 import { DocumentView } from './DocumentView';
 import { FieldView, FieldViewProps } from './FieldView';
diff --git a/src/client/views/nodes/DataVizBox/DataVizBox.tsx b/src/client/views/nodes/DataVizBox/DataVizBox.tsx
index ecfdcc229..15187b4e4 100644
--- a/src/client/views/nodes/DataVizBox/DataVizBox.tsx
+++ b/src/client/views/nodes/DataVizBox/DataVizBox.tsx
@@ -16,7 +16,6 @@ import { TraceMobx } from '../../../../fields/util';
 import { DocUtils } from '../../../documents/DocUtils';
 import { DocumentType } from '../../../documents/DocumentTypes';
 import { Docs } from '../../../documents/Documents';
-import { DocumentManager } from '../../../util/DocumentManager';
 import { UndoManager, undoable } from '../../../util/UndoManager';
 import { ContextMenu } from '../../ContextMenu';
 import { ViewBoxAnnotatableComponent, ViewBoxInterface } from '../../DocComponent';
@@ -254,7 +253,7 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() im
             this.toggleSidebar();
         }
         return new Promise<Opt<DocumentView>>(res => {
-            DocumentManager.Instance.AddViewRenderedCb(doc, dv => res(dv));
+            DocumentView.addViewRenderedCb(doc, dv => res(dv));
         });
     };
     @computed get sidebarWidthPercent() {
diff --git a/src/client/views/nodes/DataVizBox/components/LineChart.tsx b/src/client/views/nodes/DataVizBox/components/LineChart.tsx
index 1c3134185..bc35ab8c8 100644
--- a/src/client/views/nodes/DataVizBox/components/LineChart.tsx
+++ b/src/client/views/nodes/DataVizBox/components/LineChart.tsx
@@ -8,7 +8,6 @@ import { List } from '../../../../../fields/List';
 import { listSpec } from '../../../../../fields/Schema';
 import { Cast, DocCast, StrCast } from '../../../../../fields/Types';
 import { Docs } from '../../../../documents/Documents';
-import { DocumentManager } from '../../../../util/DocumentManager';
 import { undoable } from '../../../../util/UndoManager';
 import {} from '../../../DocComponent';
 import { ObservableReactComponent } from '../../../ObservableReactComponent';
@@ -16,6 +15,7 @@ import { PinProps, PinDocView } from '../../../PinFuncs';
 import { DataVizBox } from '../DataVizBox';
 import { createLineGenerator, drawLine, minMaxRange, scaleCreatorNumerical, xAxisCreator, xGrid, yAxisCreator, yGrid } from '../utils/D3Utils';
 import './Chart.scss';
+import { DocumentView } from '../../DocumentView';
 
 export interface DataPoint {
     x: number;
@@ -71,7 +71,7 @@ export class LineChart extends ObservableReactComponent<LineChartProps> {
     }
     @computed get parentViz() {
         return DocCast(this._props.Document.dataViz_parentViz);
-        // return LinkManager.Instance.getAllRelatedLinks(this._props.Document) // out of all links
+        // return LinkManager.Links(this._props.Document) // out of all links
         //     .filter(link => {
         //         return link.link_anchor_1 == this._props.Document.dataViz_parentViz;
         //     }) // get links where this chart doc is the target of the link
@@ -80,7 +80,7 @@ export class LineChart extends ObservableReactComponent<LineChartProps> {
     @computed get incomingHighlited() {
         // return selected x and y axes
         // otherwise, use the selection of whatever is linked to us
-        const incomingVizBox = DocumentManager.Instance.getFirstDocumentView(this.parentViz)?.ComponentView as DataVizBox;
+        const incomingVizBox = DocumentView.getFirstDocumentView(this.parentViz)?.ComponentView as DataVizBox;
         const highlitedRowIds = NumListCast(incomingVizBox?.layoutDoc?.dataViz_highlitedRows);
         return this._tableData.filter((record, i) => highlitedRowIds.includes(this._tableDataIds[i])); // get all the datapoints they have selected field set by incoming anchor
     }
diff --git a/src/client/views/nodes/DataVizBox/components/PieChart.tsx b/src/client/views/nodes/DataVizBox/components/PieChart.tsx
index ffb2f528a..ef6d1d412 100644
--- a/src/client/views/nodes/DataVizBox/components/PieChart.tsx
+++ b/src/client/views/nodes/DataVizBox/components/PieChart.tsx
@@ -84,7 +84,7 @@ export class PieChart extends ObservableReactComponent<PieChartProps> {
 
     @computed get parentViz() {
         return DocCast(this._props.Document.dataViz_parentViz);
-        // return LinkManager.Instance.getAllRelatedLinks(this._props.Document) // out of all links
+        // return LinkManager.Links(this._props.Document) // out of all links
         //     .filter(link => link.link_anchor_1 == this._props.Document.dataViz_parentViz) // get links where this chart doc is the target of the link
         //     .map(link => DocCast(link.link_anchor_1)); // then return the source of the link
     }
diff --git a/src/client/views/nodes/DataVizBox/components/TableBox.tsx b/src/client/views/nodes/DataVizBox/components/TableBox.tsx
index 8038b2cd4..5cd77e274 100644
--- a/src/client/views/nodes/DataVizBox/components/TableBox.tsx
+++ b/src/client/views/nodes/DataVizBox/components/TableBox.tsx
@@ -7,6 +7,7 @@ import * as React from 'react';
 import { ClientUtils, setupMoveUpEvents } from '../../../../../ClientUtils';
 import { emptyFunction } from '../../../../../Utils';
 import { Doc, Field, NumListCast } from '../../../../../fields/Doc';
+import { DocData } from '../../../../../fields/DocSymbols';
 import { List } from '../../../../../fields/List';
 import { listSpec } from '../../../../../fields/Schema';
 import { Cast, DocCast } from '../../../../../fields/Types';
@@ -152,11 +153,9 @@ export class TableBox extends ObservableReactComponent<TableBoxProps> {
                     DragManager.StartAnchorAnnoDrag(moveEv.target instanceof HTMLElement ? [moveEv.target] : [], new DragManager.AnchorAnnoDragData(this._props.docView()!, sourceAnchorCreator, targetCreator), downX, downY, {
                         dragComplete: completeEv => {
                             if (!completeEv.aborted && completeEv.annoDragData && completeEv.annoDragData.linkSourceDoc && completeEv.annoDragData.dropDocument && completeEv.linkDocument) {
-                                completeEv.linkDocument.link_displayLine = true;
-                                completeEv.linkDocument.link_matchEmbeddings = true;
-                                completeEv.linkDocument.link_displayArrow = true;
-                                // e.annoDragData.linkSourceDoc.followLinkToggle = e.annoDragData.dropDocument.annotationOn === this._props.Document;
-                                // e.annoDragData.linkSourceDoc.followLinkZoom = false;
+                                completeEv.linkDocument[DocData].link_matchEmbeddings = true;
+                                completeEv.linkDocument[DocData].stroke_startMarker = true;
+                                this._props.docView?.()?._props.addDocument?.(completeEv.linkDocument);
                             }
                         },
                     });
diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx
index e902d1792..ec9db8480 100644
--- a/src/client/views/nodes/DocumentContentsView.tsx
+++ b/src/client/views/nodes/DocumentContentsView.tsx
@@ -31,7 +31,6 @@ import { FunctionPlotBox } from './FunctionPlotBox';
 import { ImageBox } from './ImageBox';
 import { KeyValueBox } from './KeyValueBox';
 import { LabelBox } from './LabelBox';
-import { LinkAnchorBox } from './LinkAnchorBox';
 import { LinkBox } from './LinkBox';
 import { LoadingBox } from './LoadingBox';
 import { MapBox } from './MapBox/MapBox';
@@ -87,18 +86,18 @@ interface HTMLtagProps {
 export class HTMLtag extends React.Component<HTMLtagProps> {
     click = () => {
         const clickScript = (this.props as any).onClick as Opt<ScriptField>;
-        clickScript?.script.run({ this: this.props.Document, self: this.props.Document, scale: this.props.scaling });
+        clickScript?.script.run({ this: this.props.Document, scale: this.props.scaling });
     };
     onInput = (e: React.FormEvent<HTMLDivElement>) => {
         const onInputScript = (this.props as any).onInput as Opt<ScriptField>;
-        onInputScript?.script.run({ this: this.props.Document, self: this.props.Document, value: (e.target as any).textContent });
+        onInputScript?.script.run({ this: this.props.Document, value: (e.target as any).textContent });
     };
     render() {
         const style: { [key: string]: any } = {};
         const divKeys = OmitKeys(this.props, ['children', 'dragStarting', 'dragEnding', 'htmltag', 'scaling', 'Document', 'key', 'onInput', 'onClick', '__proto__']).omit;
         const replacer = (match: any, expr: string) =>
             // bcz: this executes a script to convert a property expression string:  { script }  into a value
-            (ScriptField.MakeFunction(expr, { self: Doc.name, this: Doc.name, scale: 'number' })?.script.run({ self: this.props.Document, this: this.props.Document, scale: this.props.scaling }).result as string) || '';
+            (ScriptField.MakeFunction(expr, { this: Doc.name, scale: 'number' })?.script.run({ this: this.props.Document, scale: this.props.scaling }).result as string) || '';
         Object.keys(divKeys).forEach((prop: string) => {
             const p = (this.props as any)[prop] as string;
             style[prop] = p?.replace(/{([^.'][^}']+)}/g, replacer);
@@ -121,7 +120,6 @@ export class DocumentContentsView extends ObservableReactComponent<DocumentConte
         super(props);
         makeObservable(this);
     }
-
     @computed get layout(): string {
         TraceMobx();
         if (this._props.LayoutTemplateString) return this._props.LayoutTemplateString;
@@ -186,7 +184,7 @@ export class DocumentContentsView extends ObservableReactComponent<DocumentConte
         let layoutFrame = this.layout;
 
         // replace code content with a script  >{content}<   as in  <HTMLdiv>{this.title}</HTMLdiv>
-        const replacer = (match: any, prefix: string, expr: string, postfix: string) => prefix + ((ScriptField.MakeFunction(expr, { self: Doc.name, this: Doc.name })?.script.run({ this: this._props.Document }).result as string) || '') + postfix;
+        const replacer = (match: any, prefix: string, expr: string, postfix: string) => prefix + ((ScriptField.MakeFunction(expr, { this: Doc.name })?.script.run({ this: this._props.Document }).result as string) || '') + postfix;
         layoutFrame = layoutFrame.replace(/(>[^{]*)[^=]\{([^.'][^<}]+)\}([^}]*<)/g, replacer);
 
         // replace HTML<tag> with corresponding HTML tag as in:  <HTMLdiv> becomes  <HTMLtag Document={props.Document} htmltag='div'>
@@ -205,7 +203,7 @@ export class DocumentContentsView extends ObservableReactComponent<DocumentConte
                 const code = XRegExp.matchRecursive(splits[1], '{', '}', '', { valueNames: ['between', 'left', 'match', 'right', 'between'] });
                 layoutFrame = splits[0] + ` ${func}={props.${func}} ` + splits[1].substring(code[1].end + 1);
                 const script = code[1].value.replace(/^‘/, '').replace(/’$/, ''); // ‘’ are not valid quotes in javascript so get rid of them -- they may be present to make it easier to write complex scripts - see headerTemplate in currentUserUtils.ts
-                return ScriptField.MakeScript(script, { this: Doc.name, self: Doc.name, scale: 'number', value: 'string' });
+                return ScriptField.MakeScript(script, { this: Doc.name, scale: 'number', value: 'string' });
             }
             return undefined;
             // add input function to props
@@ -247,7 +245,6 @@ export class DocumentContentsView extends ObservableReactComponent<DocumentConte
                     PresElementBox,
                     SearchBox,
                     FunctionPlotBox,
-                    LinkAnchorBox,
                     InkingStroke,
                     LinkBox,
                     ScriptingBox,
diff --git a/src/client/views/nodes/DocumentIcon.tsx b/src/client/views/nodes/DocumentIcon.tsx
index 364406197..23d934edb 100644
--- a/src/client/views/nodes/DocumentIcon.tsx
+++ b/src/client/views/nodes/DocumentIcon.tsx
@@ -6,10 +6,8 @@ import { factory } from 'typescript';
 import { FieldType } from '../../../fields/Doc';
 import { Id } from '../../../fields/FieldSymbols';
 import { StrCast } from '../../../fields/Types';
-import { DocumentManager } from '../../util/DocumentManager';
 import { Transformer, ts } from '../../util/Scripting';
 import { SnappingManager } from '../../util/SnappingManager';
-import { LightboxView } from '../LightboxView';
 import { ObservableReactComponent } from '../ObservableReactComponent';
 import { DocumentView } from './DocumentView';
 
@@ -25,9 +23,6 @@ export class DocumentIcon extends ObservableReactComponent<DocumentIconProps> {
         makeObservable(this);
     }
 
-    static get DocViews() {
-        return LightboxView.LightboxDoc ? DocumentManager.Instance.DocumentViews.filter(v => LightboxView.Contains(v)) : DocumentManager.Instance.DocumentViews;
-    }
     render() {
         const { view } = this._props;
         const { left, top, right } = view.getBounds || { left: 0, top: 0, right: 0, bottom: 0 };
@@ -69,7 +64,7 @@ export class DocumentIconContainer extends React.Component {
                             const match = node.text.match(/d([0-9]+)/);
                             if (match) {
                                 const m = parseInt(match[1]);
-                                const doc = DocumentIcon.DocViews[m].Document;
+                                const doc = DocumentView.allViews()[m].Document;
                                 usedDocuments.add(m);
                                 return factory.createIdentifier(`idToDoc("${doc[Id]}")`);
                             }
@@ -81,7 +76,7 @@ export class DocumentIconContainer extends React.Component {
                 return ts.visitNode(root, visit);
             },
             getVars() {
-                const docs = DocumentIcon.DocViews;
+                const docs = DocumentView.allViews();
                 const capturedVariables: { [name: string]: FieldType } = {};
                 usedDocuments.forEach(index => {
                     capturedVariables[`d${index}`] = docs.length > index ? docs[index].Document : `d${index}`;
@@ -91,6 +86,6 @@ export class DocumentIconContainer extends React.Component {
         };
     }
     render() {
-        return DocumentIcon.DocViews.map((dv, i) => <DocumentIcon key={dv.DocUniqueId} index={i} view={dv} />);
+        return DocumentView.allViews().map((dv, i) => <DocumentIcon key={dv.DocUniqueId} index={i} view={dv} />);
     }
 }
diff --git a/src/client/views/nodes/DocumentLinksButton.tsx b/src/client/views/nodes/DocumentLinksButton.tsx
index 977899589..0c5156339 100644
--- a/src/client/views/nodes/DocumentLinksButton.tsx
+++ b/src/client/views/nodes/DocumentLinksButton.tsx
@@ -8,11 +8,8 @@ import * as React from 'react';
 import { StopEvent, returnFalse, setupMoveUpEvents } from '../../../ClientUtils';
 import { emptyFunction } from '../../../Utils';
 import { Doc } from '../../../fields/Doc';
-import { DocData } from '../../../fields/DocSymbols';
-import { StrCast } from '../../../fields/Types';
 import { DocUtils } from '../../documents/DocUtils';
 import { DragManager } from '../../util/DragManager';
-import { Hypothesis } from '../../util/HypothesisUtils';
 import { LinkManager } from '../../util/LinkManager';
 import { UndoManager, undoBatch } from '../../util/UndoManager';
 import { ObservableReactComponent } from '../ObservableReactComponent';
@@ -169,16 +166,6 @@ export class DocumentLinksButton extends ObservableReactComponent<DocumentLinksB
                 LinkManager.Instance.currentLink = linkDoc;
 
                 if (linkDoc) {
-                    if (DocumentLinksButton.AnnotationId && DocumentLinksButton.AnnotationUri) {
-                        // if linking from a Hypothes.is annotation
-                        const linkDocData = linkDoc[DocData];
-                        linkDocData.linksToAnnotation = true;
-                        linkDocData.annotationId = DocumentLinksButton.AnnotationId;
-                        linkDocData.annotationUri = DocumentLinksButton.AnnotationUri;
-                        const dashHyperlink = Doc.globalServerPath(startIsAnnotation ? endLink : startLink);
-                        Hypothesis.makeLink(StrCast(startIsAnnotation ? endLink.title : startLink.title), dashHyperlink, DocumentLinksButton.AnnotationId, startIsAnnotation ? startLink : endLink); // edit annotation to add a Dash hyperlink to the linked doc
-                    }
-
                     TaskCompletionBox.textDisplayed = 'Link Created';
                     TaskCompletionBox.popupX = screenX;
                     TaskCompletionBox.popupY = screenY - 133;
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 1e7f1015a..f8ce50c98 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -7,10 +7,10 @@ import { IReactionDisposer, action, computed, makeObservable, observable, reacti
 import { observer } from 'mobx-react';
 import * as React from 'react';
 import { Bounce, Fade, Flip, JackInTheBox, Roll, Rotate, Zoom } from 'react-awesome-reveal';
-import { ClientUtils, DivWidth, isTargetChildOf as isParentOf, lightOrDark, returnEmptyString, returnFalse, returnTrue, returnVal, simulateMouseClick } from '../../../ClientUtils';
+import { ClientUtils, DivWidth, isTargetChildOf as isParentOf, lightOrDark, returnFalse, returnVal, simulateMouseClick } from '../../../ClientUtils';
 import { Utils, emptyFunction, emptyPath } from '../../../Utils';
 import { Doc, DocListCast, Field, FieldType, Opt, StrListCast } from '../../../fields/Doc';
-import { AclPrivate, Animation, AudioPlay, DocData, DocViews } from '../../../fields/DocSymbols';
+import { AclAdmin, AclEdit, AclPrivate, Animation, AudioPlay, DocData, DocViews } from '../../../fields/DocSymbols';
 import { Id } from '../../../fields/FieldSymbols';
 import { InkTool } from '../../../fields/InkField';
 import { List } from '../../../fields/List';
@@ -20,20 +20,17 @@ import { ScriptField } from '../../../fields/ScriptField';
 import { BoolCast, Cast, DocCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
 import { AudioField } from '../../../fields/URLField';
 import { GetEffectiveAcl, TraceMobx } from '../../../fields/util';
+import { AudioAnnoState } from '../../../server/SharedMediaTypes';
 import { DocServer } from '../../DocServer';
-import { Networking } from '../../Network';
 import { DocUtils, FollowLinkScript } from '../../documents/DocUtils';
 import { CollectionViewType, DocumentType } from '../../documents/DocumentTypes';
 import { Docs } from '../../documents/Documents';
-import { DictationManager } from '../../util/DictationManager';
-import { DocumentManager } from '../../util/DocumentManager';
 import { DragManager } from '../../util/DragManager';
 import { dropActionType } from '../../util/DropActionTypes';
 import { MakeTemplate, makeUserTemplateButton } from '../../util/DropConverter';
-import { LinkManager, UPDATE_SERVER_CACHE } from '../../util/LinkManager';
+import { UPDATE_SERVER_CACHE } from '../../util/LinkManager';
 import { ScriptingGlobals } from '../../util/ScriptingGlobals';
 import { SearchUtil } from '../../util/SearchUtil';
-import { SelectionManager } from '../../util/SelectionManager';
 import { SharingManager } from '../../util/SharingManager';
 import { SnappingManager } from '../../util/SnappingManager';
 import { UndoManager, undoBatch, undoable } from '../../util/UndoManager';
@@ -42,16 +39,15 @@ import { ContextMenuProps } from '../ContextMenuItem';
 import { DocComponent, ViewBoxInterface } from '../DocComponent';
 import { EditableView } from '../EditableView';
 import { FieldsDropdown } from '../FieldsDropdown';
-import { GestureOverlay } from '../GestureOverlay';
 import { LightboxView } from '../LightboxView';
-import { AudioAnnoState, StyleProp } from '../StyleProvider';
+import { PinProps } from '../PinFuncs';
+import { StyleProp } from '../StyleProp';
 import { DocumentContentsView, ObserverJsxParser } from './DocumentContentsView';
 import { DocumentLinksButton } from './DocumentLinksButton';
 import './DocumentView.scss';
 import { FieldViewProps, FieldViewSharedProps } from './FieldView';
 import { FocusViewOptions } from './FocusViewOptions';
 import { KeyValueBox } from './KeyValueBox';
-import { LinkAnchorBox } from './LinkAnchorBox';
 import { OpenWhere } from './OpenWhere';
 import { FormattedTextBox } from './formattedText/FormattedTextBox';
 import { PresEffect, PresEffectDirection } from './trails';
@@ -90,6 +86,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
     // this makes mobx trace() statements more descriptive
     public get displayName()  { return 'DocumentViewInternal(' + this.Document.title + ')'; } // prettier-ignore
     public static SelectAfterContextMenu = true; //  whether a document should be selected after it's contextmenu is triggered.
+
     /**
      * This function is filled in by MainView to allow non-viewBox views to add Docs as tabs without
      * needing to know about/reference MainView
@@ -183,11 +180,10 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
     // anchors that are not rendered as DocumentViews (marked as 'layout_unrendered' with their 'annotationOn' set to this document).  e.g.,
     //     - PDF text regions are rendered as an Annotations without generating a DocumentView, '
     //     - RTF selections are rendered via Prosemirror and have a mark which contains the Document ID for the annotation link
-    //     - and links to PDF/Web docs at a certain scroll location never create an explicit view.
-    // For each of these, we create LinkAnchorBox's on the border of the DocumentView.
+    //     - and links to PDF/Web docs at a certain scroll location never create an explicit anchor view.
     @computed get directLinks() {
         TraceMobx();
-        return LinkManager.Instance.getAllRelatedLinks(this.Document).filter(
+        return Doc.Links(this.Document).filter(
             link =>
                 (link.link_matchEmbeddings ? link.link_anchor_1 === this.Document : Doc.AreProtosEqual(link.link_anchor_1 as Doc, this.Document)) ||
                 (link.link_matchEmbeddings ? link.link_anchor_2 === this.Document : Doc.AreProtosEqual(link.link_anchor_2 as Doc, this.Document)) ||
@@ -197,11 +193,11 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
     }
     @computed get _allLinks(): Doc[] {
         TraceMobx();
-        return LinkManager.Instance.getAllRelatedLinks(this.Document).filter(link => !link.link_matchEmbeddings || link.link_anchor_1 === this.Document || link.link_anchor_2 === this.Document);
+        return Doc.Links(this.Document).filter(link => !link.link_matchEmbeddings || link.link_anchor_1 === this.Document || link.link_anchor_2 === this.Document);
     }
 
     @computed get filteredLinks() {
-        return DocUtils.FilterDocs(this.directLinks, this._props.childFilters?.() ?? [], []).filter(d => d.link_displayLine || Doc.UserDoc().showLinkLines);
+        return DocUtils.FilterDocs(this.directLinks, this._props.childFilters?.() ?? [], []);
     }
 
     componentWillUnmount() {
@@ -259,7 +255,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
     startDragging(x: number, y: number, dropAction: dropActionType, hideSource = false) {
         const docView = this._docView;
         if (this._mainCont.current && docView) {
-            const views = SelectionManager.Views.filter(dv => dv.ContentDiv);
+            const views = DocumentView.Selected().filter(dv => dv.ContentDiv);
             const selected = views.length > 1 && views.some(dv => dv.Document === this.Document) ? views : [docView];
             const dragData = new DragManager.DocumentDragData(selected.map(dv => dv.Document));
             const screenXf = docView.screenToViewTransform();
@@ -289,8 +285,8 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
     };
     onBrowseClick = (e: React.MouseEvent) => {
         const browseTransitionTime = 500;
-        SelectionManager.DeselectAll();
-        DocumentManager.Instance.showDocument(this.Document, { zoomScale: 0.8, willZoomCentered: true }, (focused: boolean) => {
+        DocumentView.DeselectAll();
+        DocumentView.showDocument(this.Document, { zoomScale: 0.8, willZoomCentered: true }, (focused: boolean) => {
             const options: FocusViewOptions = { pointFocus: { X: e.clientX, Y: e.clientY }, zoomTime: browseTransitionTime };
             if (!focused && this._docView) {
                 this._docView
@@ -327,7 +323,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
                     UndoManager.RunInBatch(() => this.onDoubleClickHandler.script.run(scriptProps, console.log).result?.select && this._props.select(false), 'on double click: ' + this.Document.title);
                 } else if (!Doc.IsSystem(this.Document) && defaultDblclick !== 'ignore') {
                     UndoManager.RunInBatch(() => LightboxView.Instance.AddDocTab(this.Document, OpenWhere.lightbox), 'double tap');
-                    SelectionManager.DeselectAll();
+                    DocumentView.DeselectAll();
                     Doc.UnBrushDoc(this.Document);
                 } else {
                     this._singleClickFunc?.();
@@ -377,7 +373,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
         if (this._props.isGroupActive?.() === 'child' && !this._props.isDocumentActive?.()) return;
         // eslint-disable-next-line no-use-before-define
         this._longPressSelector = setTimeout(() => DocumentView.LongPress && this._props.select(false), 1000);
-        if (!GestureOverlay.DownDocView) GestureOverlay.DownDocView = this._docView;
+        if (!DocumentView.DownDocView) DocumentView.DownDocView = this._docView;
 
         this._downX = e.clientX;
         this._downY = e.clientY;
@@ -463,7 +459,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
     }, 'set toggle detail');
 
     drop = undoable((e: Event, de: DragManager.DropEvent) => {
-        if (this._props.dontRegisterView || this._props.LayoutTemplateString?.includes(LinkAnchorBox.name)) return false;
+        if (this._props.dontRegisterView) return false;
         if (this.Document === Doc.ActiveDashboard) {
             e.stopPropagation();
             e.preventDefault();
@@ -484,7 +480,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
                     if (linkDoc) {
                         de.complete.linkDocument = linkDoc;
                         linkDoc.layout_isSvg = true;
-                        DocumentManager.LinkCommonAncestor(linkDoc)?.ComponentView?.addDocument?.(linkDoc);
+                        DocumentView.linkCommonAncestor(linkDoc)?.ComponentView?.addDocument?.(linkDoc);
                     }
                 }
                 e.stopPropagation();
@@ -516,7 +512,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
         if (e && this.layoutDoc.layout_hideContextMenu && Doc.noviceMode) {
             e.preventDefault();
             e.stopPropagation();
-            // !this._props.isSelected(true) && SelectionManager.SelectView(this.DocumentView(), false);
+            // !this._props.isSelected(true) && DocumentView.SelectView(this.DocumentView(), false);
         }
         // the touch onContextMenu is button 0, the pointer onContextMenu is button 2
         if (e) {
@@ -576,8 +572,8 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
             if (this._props.bringToFront) {
                 const zorders = cm.findByDescription('ZOrder...');
                 const zorderItems: ContextMenuProps[] = zorders && 'subitems' in zorders ? zorders.subitems : [];
-                zorderItems.push({ description: 'Bring to Front', event: () => SelectionManager.Views.forEach(dv => dv._props.bringToFront?.(dv.Document, false)), icon: 'arrow-up' });
-                zorderItems.push({ description: 'Send to Back', event: () => SelectionManager.Views.forEach(dv => dv._props.bringToFront?.(dv.Document, true)), icon: 'arrow-down' });
+                zorderItems.push({ description: 'Bring to Front', event: () => DocumentView.Selected().forEach(dv => dv._props.bringToFront?.(dv.Document, false)), icon: 'arrow-up' });
+                zorderItems.push({ description: 'Send to Back', event: () => DocumentView.Selected().forEach(dv => dv._props.bringToFront?.(dv.Document, true)), icon: 'arrow-down' });
                 zorderItems.push({
                     description: !this.layoutDoc._keepZDragged ? 'Keep ZIndex when dragged' : 'Allow ZIndex to change when dragged',
                     event: undoBatch(
@@ -601,7 +597,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
                     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' });
                     !existingOnClick && cm.addItem({ description: 'OnClick...', noexpand: true, subitems: onClicks, icon: 'mouse-pointer' });
-                } else if (LinkManager.Links(this.Document).length) {
+                } else if (Doc.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' });
                     !existingOnClick && cm.addItem({ description: 'OnClick...', subitems: onClicks, icon: 'mouse-pointer' });
@@ -718,7 +714,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
             case StyleProp.PointerEvents: return 'none';
             case StyleProp.Highlighting:  return undefined;
             case StyleProp.Opacity:       {
-                const filtered = DocUtils.FilterDocs(this.directLinks, this._props.childFilters?.() ?? [], []).filter(d => d.link_displayLine || Doc.UserDoc().showLinkLines);
+                const filtered = DocUtils.FilterDocs(this.directLinks, this._props.childFilters?.() ?? [], []);
                 return filtered.some(link => link._link_displayArrow) ? 0 : undefined;
             }
             default:
@@ -726,34 +722,6 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
         return this._props.styleProvider?.(doc, props, property);
     };
 
-    // eslint-disable-next-line no-return-assign
-    removeLinkByHiding = (link: Doc) => () => link.link_displayLine = false; // prettier-ignore
-    @computed get allLinkEndpoints() {
-        // the small blue dots that mark the endpoints of links
-        if (this._componentView instanceof KeyValueBox || this._props.hideLinkAnchors || this.layoutDoc.layout_hideLinkAnchors || this._props.dontRegisterView || this.layoutDoc.layout_unrendered) return null;
-        return this.filteredLinks.map(link => (
-            <div className="documentView-anchorCont" key={link[Id]}>
-                <DocumentView
-                    {...this._props}
-                    isContentActive={returnFalse}
-                    Document={link}
-                    containerViewPath={this._props.docViewPath}
-                    PanelWidth={this.anchorPanelWidth}
-                    PanelHeight={this.anchorPanelHeight}
-                    dontRegisterView={false}
-                    layout_showTitle={returnEmptyString}
-                    hideCaptions
-                    hideLinkAnchors
-                    layout_fitWidth={returnTrue}
-                    removeDocument={this.removeLinkByHiding(link)}
-                    styleProvider={this.anchorStyleProvider}
-                    LayoutTemplate={undefined}
-                    LayoutTemplateString={LinkAnchorBox.LayoutString(`link_anchor_${LinkManager.anchorIndex(link, this.Document)}`)}
-                />
-            </div>
-        ));
-    }
-
     @computed get viewBoxContents() {
         TraceMobx();
         const isInk = this.layoutDoc._layout_isSvg && !this._props.LayoutTemplateString;
@@ -780,7 +748,6 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
                     setTitleFocus={this.setTitleFocus}
                     hideClickBehaviors={BoolCast(this.Document.hideClickBehaviors)}
                 />
-                {this.layoutDoc.layout_hideAllLinks ? null : this.allLinkEndpoints}
             </div>
         );
     }
@@ -1008,50 +975,45 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
             default:                    return renderDoc;
         }
     }
-    public static recordAudioAnnotation(dataDoc: Doc, field: string, onRecording?: (stop: () => void) => void, onEnd?: () => void) {
-        let gumStream: any;
-        let recorder: any;
-        navigator.mediaDevices.getUserMedia({ audio: true }).then(stream => {
-            let audioTextAnnos = Cast(dataDoc[field + '_audioAnnotations_text'], listSpec('string'), null);
-            if (audioTextAnnos) audioTextAnnos.push('');
-            else audioTextAnnos = dataDoc[field + '_audioAnnotations_text'] = new List<string>(['']);
-            DictationManager.Controls.listen({
-                interimHandler: value => { audioTextAnnos[audioTextAnnos.length - 1] = value; }, // prettier-ignore
-                continuous: { indefinite: false },
-            }).then(results => {
-                if (results && [DictationManager.Controls.Infringed].includes(results)) {
-                    DictationManager.Controls.stop();
-                }
-                onEnd?.();
-            });
-
-            gumStream = stream;
-            recorder = new MediaRecorder(stream);
-            recorder.ondataavailable = async (e: any) => {
-                const [{ result }] = await Networking.UploadFilesToServer({ file: e.data });
-                if (!(result instanceof Error)) {
-                    const audioField = new AudioField(result.accessPaths.agnostic.client);
-                    const audioAnnos = Cast(dataDoc[field + '_audioAnnotations'], listSpec(AudioField), null);
-                    if (audioAnnos) audioAnnos.push(audioField);
-                    else dataDoc[field + '_audioAnnotations'] = new List([audioField]);
-                }
-            };
-            recorder.start();
-            const stopFunc = () => {
-                recorder.stop();
-                DictationManager.Controls.stop(/* false */);
-                dataDoc.audioAnnoState = AudioAnnoState.stopped;
-                gumStream.getAudioTracks()[0].stop();
-            };
-            if (onRecording) onRecording(stopFunc);
-            else setTimeout(stopFunc, 5000);
-        });
-    }
 }
 
 @observer
 export class DocumentView extends DocComponent<DocumentViewProps>() {
     public static ROOT_DIV = 'documentView-effectsWrapper';
+    // LinkFollower
+    public static FollowLink: (linkDoc: Opt<Doc>, sourceDoc: Doc, altKey: boolean) => boolean;
+    // selection funcs
+    public static DeselectAll: (except?: Doc) => void | undefined;
+    public static DeselectView: (dv: DocumentView | undefined) => void | undefined;
+    public static SelectView: (dv: DocumentView | undefined, extendSelection: boolean) => void | undefined;
+    public static Selected: () => DocumentView[];
+    public static SelectedDocs: () => Doc[];
+    public static SelectSchemaDoc: (doc: Doc, deselectAllFirst?: boolean) => void;
+    public static SelectedSchemaDoc: () => Opt<Doc>;
+    // view mgr funcs
+    public static activateTabView: (tabDoc: Doc) => boolean;
+    public static allViews: () => DocumentView[];
+    public static addView: (dv: DocumentView) => void | undefined;
+    public static removeView: (dv: DocumentView) => void | undefined;
+    public static addViewRenderedCb: (doc: Opt<Doc>, func: (dv: DocumentView) => any) => boolean;
+    public static getFirstDocumentView: (toFind: Doc) => DocumentView | undefined;
+    public static getDocumentView: (target: Doc | undefined, preferredCollection?: DocumentView) => Opt<DocumentView>;
+    public static getContextPath: (doc: Opt<Doc>, includeExistingViews?: boolean) => Doc[];
+    public static getLightboxDocumentView: (toFind: Doc) => Opt<DocumentView>;
+    public static showDocumentView: (targetDocView: DocumentView, options: FocusViewOptions) => Promise<void>;
+    public static showDocument: (
+        targetDoc: Doc, // document to display
+        optionsIn: FocusViewOptions, // options for how to navigate to target
+        finished?: (changed: boolean) => void // func called after focusing on target with flag indicating whether anything needed to be done.
+    ) => Promise<void>;
+    public static linkCommonAncestor: (link: Doc) => DocumentView | undefined;
+    // pin func
+    public static PinDoc: (docIn: Doc | Doc[], pinProps: PinProps) => void;
+    // gesture
+    public static DownDocView: DocumentView | undefined; // the first DocView that receives a pointerdown event.  used by GestureOverlay to determine the doc a gesture should apply to.
+    // media playing
+    @observable public static CurrentlyPlaying: DocumentView[] = [];
+
     public get displayName() {  return 'DocumentView(' + (this.Document?.title??"") + ')';  } // prettier-ignore
     public ContentRef = React.createRef<HTMLDivElement>();
     private _htmlOverlayEffect: Opt<Doc>;
@@ -1138,14 +1100,14 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
     componentDidMount() {
         runInAction(() => this.Document[DocViews].add(this));
         this._disposers.onViewMounted = reaction(() => ScriptCast(this.Document.onViewMounted)?.script?.run({ this: this.Document }).result, emptyFunction);
-        !BoolCast(this.Document.dontRegisterView, this._props.dontRegisterView) && DocumentManager.Instance.AddView(this);
+        !BoolCast(this.Document.dontRegisterView, this._props.dontRegisterView) && DocumentView.addView(this);
     }
 
     componentWillUnmount() {
         this._viewTimer && clearTimeout(this._viewTimer);
         runInAction(() => this.Document[DocViews].delete(this));
         Object.values(this._disposers).forEach(disposer => disposer?.());
-        !BoolCast(this.Document.dontRegisterView, this._props.dontRegisterView) && DocumentManager.Instance.RemoveView(this);
+        !BoolCast(this.Document.dontRegisterView, this._props.dontRegisterView) && DocumentView.removeView(this);
     }
 
     public set IsSelected(val)  { runInAction(() => { this._selected = val; }); } // prettier-ignore
@@ -1176,10 +1138,6 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
         const xf = this.screenToContentsTransform().scale(this.nativeScaling).inverse();
         const [[left, top], [right, bottom]] = [xf.transformPoint(0, 0), xf.transformPoint(this.panelWidth, this.panelHeight)];
 
-        if (this._props.LayoutTemplateString?.includes(LinkAnchorBox.name)) {
-            const docuBox = this.ContentDiv.getElementsByClassName('linkAnchorBox-cont');
-            if (docuBox.length) return { ...docuBox[0].getBoundingClientRect(), transition: undefined };
-        }
         // transition is returned so that the bounds will 'update' at the end of an animated transition.  This is needed by xAnchor in LinkBox
         const transition = this.docViewPath().find((parent: DocumentView) => parent.DataTransition?.() || parent.ComponentView?.viewTransition?.());
         return { left, top, right, bottom, transition: transition?.DataTransition?.() || transition?.ComponentView?.viewTransition?.() };
@@ -1289,7 +1247,7 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
         if (checkResult) {
             return Doc.UserDoc().defaultTextLayout;
         }
-        const view = SelectionManager.Views[0]?._props.renderDepth > 0 ? SelectionManager.Views[0] : undefined;
+        const view = DocumentView.Selected()[0]?._props.renderDepth > 0 ? DocumentView.Selected()[0] : undefined;
         undoable(() => {
             let tempDoc: Opt<Doc>;
             if (view) {
@@ -1359,11 +1317,11 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
     screenToLocalScale = () => this._props.ScreenToLocalTransform().Scale;
     isSelected = () => this.IsSelected;
     select = (extendSelection: boolean, focusSelection?: boolean) => {
-        if (this.IsSelected && SelectionManager.Views.length > 1) SelectionManager.DeselectView(this);
+        if (this.IsSelected && DocumentView.Selected().length > 1) DocumentView.DeselectView(this);
         else {
-            SelectionManager.SelectView(this, extendSelection);
+            DocumentView.SelectView(this, extendSelection);
             if (focusSelection) {
-                DocumentManager.Instance.showDocument(this.Document, {
+                DocumentView.showDocument(this.Document, {
                     willZoomCentered: true,
                     zoomScale: 0.9,
                     zoomTime: 500,
@@ -1506,6 +1464,37 @@ export function returnEmptyDocViewList() {
     return emptyPath;
 }
 
+// eslint-disable-next-line default-param-last
+export function DocFocusOrOpen(docIn: Doc, optionsIn: FocusViewOptions = { willZoomCentered: true, zoomScale: 0, openLocation: OpenWhere.toggleRight }, containingDoc?: Doc) {
+    let doc = docIn;
+    const options = optionsIn;
+    const func = () => {
+        const cv = DocumentView.getDocumentView(containingDoc);
+        const dv = DocumentView.getDocumentView(doc, cv);
+        if (dv && (!containingDoc || dv.containerViewPath?.().lastElement()?.Document === containingDoc)) {
+            DocumentView.showDocumentView(dv, options).then(() => dv && Doc.linkFollowHighlight(dv.Document));
+        } else {
+            const container = DocCast(containingDoc ?? doc.embedContainer ?? Doc.BestEmbedding(doc));
+            const showDoc = !Doc.IsSystem(container) && !cv ? container : doc;
+            options.toggleTarget = undefined;
+            DocumentView.showDocument(showDoc, options, () => DocumentView.showDocument(doc, { ...options, openLocation: undefined })).then(() => {
+                const cvFound = DocumentView.getDocumentView(containingDoc);
+                const dvFound = DocumentView.getDocumentView(doc, cvFound);
+                dvFound && Doc.linkFollowHighlight(dvFound.Document);
+            });
+        }
+    };
+    if (Doc.IsDataProto(doc) && Doc.GetEmbeddings(doc).some(embed => embed.hidden && [AclAdmin, AclEdit].includes(GetEffectiveAcl(embed)))) {
+        doc = Doc.GetEmbeddings(doc).find(embed => embed.hidden && [AclAdmin, AclEdit].includes(GetEffectiveAcl(embed)))!;
+    }
+    if (doc.hidden) {
+        doc.hidden = false;
+        options.toggleTarget = false;
+        setTimeout(func);
+    } else func();
+}
+ScriptingGlobals.add(DocFocusOrOpen);
+
 // eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function deiconifyView(documentView: DocumentView) {
     documentView.iconify();
@@ -1527,9 +1516,9 @@ ScriptingGlobals.add(function updateLinkCollection(linkCollection: Doc, linkSour
     const collectedLinks = DocListCast(linkCollection[DocData].data);
     let wid = NumCast(linkSource._width);
     let embedding: Doc | undefined;
-    const links = LinkManager.Links(linkSource);
+    const links = Doc.Links(linkSource);
     links.forEach(link => {
-        const other = LinkManager.getOppositeAnchor(link, linkSource);
+        const other = Doc.getOppositeAnchor(link, linkSource);
         const otherdoc = DocCast(other?.annotationOn ?? other);
         if (otherdoc && !collectedLinks?.some(d => Doc.AreProtosEqual(d, otherdoc))) {
             embedding = Doc.MakeEmbedding(otherdoc);
diff --git a/src/client/views/nodes/FontIconBox/FontIconBox.tsx b/src/client/views/nodes/FontIconBox/FontIconBox.tsx
index 8d1617e66..5e3bb9fec 100644
--- a/src/client/views/nodes/FontIconBox/FontIconBox.tsx
+++ b/src/client/views/nodes/FontIconBox/FontIconBox.tsx
@@ -17,7 +17,7 @@ import { ContextMenu } from '../../ContextMenu';
 import { ViewBoxBaseComponent } from '../../DocComponent';
 import { EditableView } from '../../EditableView';
 import { SelectedDocView } from '../../selectedDoc';
-import { StyleProp } from '../../StyleProvider';
+import { StyleProp } from '../../StyleProp';
 import { FieldView, FieldViewProps } from '../FieldView';
 import { OpenWhere } from '../OpenWhere';
 import './FontIconBox.scss';
@@ -116,7 +116,7 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() {
             default:                               type = 'slider';
                 break;
         } // prettier-ignore
-        const numScript = (value?: number) => ScriptCast(this.Document.script).script.run({ this: this.Document, self: this.Document, value, _readOnly_: value === undefined });
+        const numScript = (value?: number) => ScriptCast(this.Document.script).script.run({ this: this.Document, value, _readOnly_: value === undefined });
         const color = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Color);
         // Script for checking the outcome of the toggle
         const checkResult = Number(Number(numScript().result ?? 0).toPrecision(NumCast(this.dataDoc.numPrecision, 3)));
@@ -142,7 +142,7 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() {
         setupMoveUpEvents(
             this,
             e,
-            () => ScriptCast(this.Document.onDragScript)?.script.run({ this: this.Document, self: this.Document, value: { doc: value, e } }).result,
+            () => ScriptCast(this.Document.onDragScript)?.script.run({ this: this.Document, value: { doc: value, e } }).result,
             emptyFunction,
             emptyFunction
         ); // prettier-ignore
@@ -162,7 +162,7 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() {
         const isViewDropdown = script?.script.originalScript.startsWith('{ return setView');
         if (isViewDropdown) {
             const selected = Array.from(script?.script.run({ _readOnly_: true }).result) as Doc[];
-            // const selected = SelectionManager.Docs;
+            // const selected = DocumentView.SelectedDocs();
             if (selected.lastElement()) {
                 if (StrCast(selected.lastElement().type) === DocumentType.COL) {
                     text = StrCast(selected.lastElement()._type_collection);
@@ -190,7 +190,7 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() {
             }
             noviceList = [CollectionViewType.Freeform, CollectionViewType.Schema, CollectionViewType.Carousel3D, CollectionViewType.Stacking, CollectionViewType.NoteTaking];
         } else {
-            text = script?.script.run({ this: this.Document, self: this.Document, value: '', _readOnly_: true }).result;
+            text = script?.script.run({ this: this.Document, value: '', _readOnly_: true }).result;
             // text = StrCast((RichTextMenu.Instance?.TextView?.EditorView ? RichTextMenu.Instance : Doc.UserDoc()).fontFamily);
             getStyle = (val: string) => ({ fontFamily: val });
         }
@@ -208,7 +208,7 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() {
         return (
             <Dropdown
                 selectedVal={text}
-                setSelectedVal={undoable(value => script.script.run({ this: this.Document, self: this.Document, value }), `dropdown select ${this.label}`)}
+                setSelectedVal={undoable(value => script.script.run({ this: this.Document, value }), `dropdown select ${this.label}`)}
                 color={SnappingManager.userColor}
                 background={SnappingManager.userVariantColor}
                 type={Type.TERT}
@@ -232,17 +232,17 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() {
      */
     @computed get colorButton() {
         const color = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Color);
-        const curColor = this.colorScript?.script.run({ this: this.Document, self: this.Document, value: undefined, _readOnly_: true }).result ?? 'transparent';
+        const curColor = this.colorScript?.script.run({ this: this.Document, value: undefined, _readOnly_: true }).result ?? 'transparent';
         const tooltip: string = StrCast(this.Document.toolTip);
 
         return (
             <ColorPicker
                 setSelectedColor={value => {
                     if (!this.colorBatch) this.colorBatch = UndoManager.StartBatch(`Set ${tooltip} color`);
-                    this.colorScript?.script.run({ this: this.Document, self: this.Document, value: value, _readOnly_: false });
+                    this.colorScript?.script.run({ this: this.Document, value: value, _readOnly_: false });
                 }}
                 setFinalColor={value => {
-                    this.colorScript?.script.run({ this: this.Document, self: this.Document, value: value, _readOnly_: false });
+                    this.colorScript?.script.run({ this: this.Document, value: value, _readOnly_: false });
                     this.colorBatch?.end();
                     this.colorBatch = undefined;
                 }}
@@ -262,7 +262,7 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() {
         const tooltip: string = StrCast(this.Document.toolTip);
 
         // const script = ScriptCast(this.Document.onClick);
-        // const toggleStatus = script ? script.script.run({ this: this.Document, self: this.Document, value: undefined, _readOnly_: true }).result : false;
+        // const toggleStatus = script ? script.script.run({ this: this.Document, value: undefined, _readOnly_: true }).result : false;
         // Colors
         const color = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Color);
         const items = DocListCast(this.dataDoc.data);
@@ -278,10 +278,10 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() {
                     tooltip: StrCast(item.toolTip),
                     val: StrCast(item.toolType),
                 }))}
-                selectedVal={StrCast(items.find(itemDoc => ScriptCast(itemDoc.onClick).script.run({ this: itemDoc, self: itemDoc, value: undefined, _readOnly_: true }).result)?.toolType)}
+                selectedVal={StrCast(items.find(itemDoc => ScriptCast(itemDoc.onClick).script.run({ this: itemDoc, value: undefined, _readOnly_: true }).result)?.toolType)}
                 setSelectedVal={(val: string | number) => {
                     const itemDoc = items.find(item => item.toolType === val);
-                    itemDoc && ScriptCast(itemDoc.onClick).script.run({ this: itemDoc, self: itemDoc, value: val, _readOnly_: false });
+                    itemDoc && ScriptCast(itemDoc.onClick).script.run({ this: itemDoc, value: val, _readOnly_: false });
                 }}
             />
         );
@@ -296,7 +296,7 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() {
 
         const script = ScriptCast(this.Document.onClick);
         const double = ScriptCast(this.Document.onDoubleClick);
-        const toggleStatus = script?.script.run({ this: this.Document, self: this.Document, value: undefined, _readOnly_: true }).result ?? false;
+        const toggleStatus = script?.script.run({ this: this.Document, value: undefined, _readOnly_: true }).result ?? false;
         // Colors
         const color = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Color);
         // const backgroundColor = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BackgroundColor);
@@ -319,8 +319,8 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() {
                         returnTrue,
                         emptyFunction,
                         action((clickEv, doubleTap) => {
-                            (!doubleTap || !double) && script?.script.run({ this: this.Document, self: this.Document, value: !toggleStatus, _readOnly_: false });
-                            doubleTap && double?.script.run({ this: this.Document, self: this.Document, value: !toggleStatus, _readOnly_: false });
+                            (!doubleTap || !double) && script?.script.run({ this: this.Document, value: !toggleStatus, _readOnly_: false });
+                            doubleTap && double?.script.run({ this: this.Document, value: !toggleStatus, _readOnly_: false });
                             this._hackToRecompute += 1;
                         })
                     )
@@ -341,15 +341,15 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() {
 
     @computed get editableText() {
         const script = ScriptCast(this.Document.script);
-        const checkResult = script?.script.run({ this: this.Document, self: this.Document, value: '', _readOnly_: true }).result;
+        const checkResult = script?.script.run({ this: this.Document, value: '', _readOnly_: true }).result;
 
-        const setValue = (value: string): boolean => script?.script.run({ this: this.Document, self: this.Document, value, _readOnly_: false }).result;
+        const setValue = (value: string): boolean => script?.script.run({ this: this.Document, value, _readOnly_: false }).result;
 
         return (
             <div className="menuButton editableText">
                 <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon="lock" />
                 <div style={{ width: 'calc(100% - .875em)', paddingLeft: '4px' }}>
-                    <EditableView GetValue={() => script?.script.run({ this: this.Document, self: this.Document, value: '', _readOnly_: true }).result} SetValue={setValue} oneLine contents={checkResult} />
+                    <EditableView GetValue={() => script?.script.run({ this: this.Document, value: '', _readOnly_: true }).result} SetValue={setValue} oneLine contents={checkResult} />
                 </div>
             </div>
         );
@@ -358,7 +358,7 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() {
     renderButton = () => {
         const color = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Color);
         const tooltip = StrCast(this.Document.toolTip);
-        const scriptFunc = () => ScriptCast(this.Document.onClick)?.script.run({ this: this.Document, self: this.Document, _readOnly_: false });
+        const scriptFunc = () => ScriptCast(this.Document.onClick)?.script.run({ this: this.Document, _readOnly_: false });
         const btnProps = { tooltip, icon: this.Icon(color)!, label: this.label };
         // prettier-ignore
         switch (this.type) {
diff --git a/src/client/views/nodes/FunctionPlotBox.tsx b/src/client/views/nodes/FunctionPlotBox.tsx
index f32d39aaf..3d1bd7563 100644
--- a/src/client/views/nodes/FunctionPlotBox.tsx
+++ b/src/client/views/nodes/FunctionPlotBox.tsx
@@ -2,7 +2,7 @@ import functionPlot from 'function-plot';
 import { computed, makeObservable, reaction } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
-import { DocListCast } from '../../../fields/Doc';
+import { Doc, DocListCast } from '../../../fields/Doc';
 import { List } from '../../../fields/List';
 import { listSpec } from '../../../fields/Schema';
 import { Cast, StrCast } from '../../../fields/Types';
@@ -11,7 +11,6 @@ import { DocUtils } from '../../documents/DocUtils';
 import { DocumentType } from '../../documents/DocumentTypes';
 import { Docs } from '../../documents/Documents';
 import { DragManager } from '../../util/DragManager';
-import { LinkManager } from '../../util/LinkManager';
 import { undoBatch } from '../../util/UndoManager';
 import { ViewBoxAnnotatableComponent } from '../DocComponent';
 import { PinDocView, PinProps } from '../PinFuncs';
@@ -49,8 +48,8 @@ export class FunctionPlotBox extends ViewBoxAnnotatableComponent<FieldViewProps>
         return anchor;
     };
     @computed get graphFuncs() {
-        const links = LinkManager.Instance.getAllRelatedLinks(this.Document)
-            .map(d => LinkManager.getOppositeAnchor(d, this.Document))
+        const links = Doc.Links(this.Document)
+            .map(d => Doc.getOppositeAnchor(d, this.Document))
             .filter(d => d)
             .map(d => d!);
         const funcs = links.concat(DocListCast(this.dataDoc[this.fieldKey])).map(doc =>
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index d317f46bb..3945da104 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -19,7 +19,6 @@ import { Docs } from '../../documents/Documents';
 import { DocumentType } from '../../documents/DocumentTypes';
 import { DocUtils } from '../../documents/DocUtils';
 import { Networking } from '../../Network';
-import { DocumentManager } from '../../util/DocumentManager';
 import { DragManager } from '../../util/DragManager';
 import { undoBatch } from '../../util/UndoManager';
 import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView';
@@ -30,7 +29,8 @@ import { MarqueeAnnotator } from '../MarqueeAnnotator';
 import { OverlayView } from '../OverlayView';
 import { AnchorMenu } from '../pdf/AnchorMenu';
 import { PinDocView, PinProps } from '../PinFuncs';
-import { StyleProp } from '../StyleProvider';
+import { StyleProp } from '../StyleProp';
+import { DocumentView } from './DocumentView';
 import { FieldView, FieldViewProps } from './FieldView';
 import { FocusViewOptions } from './FocusViewOptions';
 import './ImageBox.scss';
@@ -254,7 +254,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl
             cropping.y = NumCast(this.Document.y);
             this._props.addDocTab(cropping, OpenWhere.inParent);
         }
-        DocumentManager.Instance.AddViewRenderedCb(cropping, dv => setTimeout(() => (dv.ComponentView as ImageBox).setNativeSize(), 200));
+        DocumentView.addViewRenderedCb(cropping, dv => setTimeout(() => (dv.ComponentView as ImageBox).setNativeSize(), 200));
         this._props.bringToFront?.(cropping);
         return cropping;
     };
diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx
index 46bb16e50..cd591fa42 100644
--- a/src/client/views/nodes/KeyValueBox.tsx
+++ b/src/client/views/nodes/KeyValueBox.tsx
@@ -59,6 +59,8 @@ export class KeyValueBox extends ObservableReactComponent<FieldViewProps> {
     get fieldDocToLayout() {
         return DocCast(this._props.Document);
     }
+    isUnstyledView = returnTrue; // used by style provider via ViewBoxInterface
+    dontRegisterView = returnTrue; // used by ViewBoxInterface
 
     @action
     onEnterKey = (e: React.KeyboardEvent): void => {
@@ -111,7 +113,7 @@ export class KeyValueBox extends ObservableReactComponent<FieldViewProps> {
                     if (setResult) setResult?.(value);
                     else target[key] = field;
                 };
-                const res = script.run({ this: Doc.Layout(doc), self: doc, _setCacheResult_ }, console.log);
+                const res = script.run({ this: Doc.Layout(doc), _setCacheResult_ }, console.log);
                 if (!res.success) {
                     if (key) target[key] = script.originalScript;
                     return false;
diff --git a/src/client/views/nodes/LabelBox.tsx b/src/client/views/nodes/LabelBox.tsx
index 4dec3506c..f80ff5f94 100644
--- a/src/client/views/nodes/LabelBox.tsx
+++ b/src/client/views/nodes/LabelBox.tsx
@@ -13,7 +13,7 @@ import { ContextMenu } from '../ContextMenu';
 import { ContextMenuProps } from '../ContextMenuItem';
 import { ViewBoxBaseComponent } from '../DocComponent';
 import { PinDocView, PinProps } from '../PinFuncs';
-import { StyleProp } from '../StyleProvider';
+import { StyleProp } from '../StyleProp';
 import { FieldView, FieldViewProps } from './FieldView';
 import BigText from './LabelBigText';
 import './LabelBox.scss';
diff --git a/src/client/views/nodes/LinkAnchorBox.scss b/src/client/views/nodes/LinkAnchorBox.scss
deleted file mode 100644
index caff369df..000000000
--- a/src/client/views/nodes/LinkAnchorBox.scss
+++ /dev/null
@@ -1,34 +0,0 @@
-.linkAnchorBox-cont,
-.linkAnchorBox-cont-small {
-    cursor: default;
-    position: absolute;
-    width: 15;
-    height: 15;
-    border-radius: 20px;
-    user-select: none;
-    pointer-events: all;
-
-    .linkAnchorBox-linkCloser {
-        position: absolute;
-        width: 18;
-        height: 18;
-        background: rgb(219, 21, 21);
-        top: -1px;
-        left: -1px;
-        border-radius: 5px;
-        display: flex;
-        justify-content: center;
-        align-items: center;
-        padding-left: 2px;
-        padding-top: 1px;
-    }
-    .linkAnchorBox-button {
-        position: relative;
-        display: inline-block;
-    }
-}
-
-.linkAnchorBox-cont-small {
-    width: 5px;
-    height: 5px;
-}
\ No newline at end of file
diff --git a/src/client/views/nodes/LinkAnchorBox.tsx b/src/client/views/nodes/LinkAnchorBox.tsx
deleted file mode 100644
index d43241de0..000000000
--- a/src/client/views/nodes/LinkAnchorBox.tsx
+++ /dev/null
@@ -1,118 +0,0 @@
-import { action, computed, makeObservable } from 'mobx';
-import { observer } from 'mobx-react';
-import * as React from 'react';
-import { setupMoveUpEvents } from '../../../ClientUtils';
-import { Utils, emptyFunction } from '../../../Utils';
-import { Doc } from '../../../fields/Doc';
-import { NumCast, StrCast } from '../../../fields/Types';
-import { TraceMobx } from '../../../fields/util';
-import { DragManager } from '../../util/DragManager';
-import { dropActionType } from '../../util/DropActionTypes';
-import { LinkFollower } from '../../util/LinkFollower';
-import { SelectionManager } from '../../util/SelectionManager';
-import { ViewBoxBaseComponent } from '../DocComponent';
-import { StyleProp } from '../StyleProvider';
-import { FieldView, FieldViewProps } from './FieldView';
-import './LinkAnchorBox.scss';
-import { LinkInfo } from './LinkDocPreview';
-
-const { MEDIUM_GRAY } = require('../global/globalCssVariables.module.scss'); // prettier-ignore
-
-@observer
-export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps>() {
-    public static LayoutString(fieldKey: string) {
-        return FieldView.LayoutString(LinkAnchorBox, fieldKey);
-    }
-    _doubleTap = false;
-    _lastTap: number = 0;
-    _ref = React.createRef<HTMLDivElement>();
-    _isOpen = false;
-    _timeout: NodeJS.Timeout | undefined;
-
-    constructor(props: FieldViewProps) {
-        super(props);
-        makeObservable(this);
-    }
-
-    componentDidMount() {
-        this._props.setContentViewBox?.(this);
-    }
-
-    @computed get linkSource() {
-        return this.DocumentView?.().containerViewPath?.().lastElement().Document; // this._props.styleProvider?.(this.dataDoc, this._props, StyleProp.LinkSource);
-    }
-
-    onPointerDown = (e: React.PointerEvent) => {
-        const { linkSource } = this;
-        linkSource &&
-            setupMoveUpEvents(this, e, this.onPointerMove, emptyFunction, (clickEv, doubleTap) => {
-                if (doubleTap) LinkFollower.FollowLink(this.Document, linkSource, false);
-                else this._props.select(false);
-            });
-    };
-    onPointerMove = action((e: PointerEvent) => {
-        const cdiv = this._ref?.current?.parentElement;
-        if (!this._isOpen && cdiv) {
-            const bounds = cdiv.getBoundingClientRect();
-            const pt = Utils.getNearestPointInPerimeter(bounds.left, bounds.top, bounds.width, bounds.height, e.clientX, e.clientY);
-            const separation = Math.sqrt((pt[0] - e.clientX) * (pt[0] - e.clientX) + (pt[1] - e.clientY) * (pt[1] - e.clientY));
-            if (separation > 100) {
-                const dragData = new DragManager.DocumentDragData([this.Document]);
-                dragData.dropAction = dropActionType.embed;
-                dragData.dropPropertiesToRemove = ['link_anchor_1_x', 'link_anchor_1_y', 'link_anchor_2_x', 'link_anchor_2_y', 'onClick'];
-                DragManager.StartDocumentDrag([this._ref.current!], dragData, pt[0], pt[1]);
-                return true;
-            }
-            this.layoutDoc[this.fieldKey + '_x'] = ((pt[0] - bounds.left) / bounds.width) * 100;
-            this.layoutDoc[this.fieldKey + '_y'] = ((pt[1] - bounds.top) / bounds.height) * 100;
-            this.layoutDoc.link_autoMoveAnchors = false;
-        }
-        return false;
-    });
-
-    specificContextMenu = (): void => {};
-
-    render() {
-        TraceMobx();
-        const small = this._props.PanelWidth() <= 1; // this happens when rendered in a treeView
-        const x = NumCast(this.layoutDoc[this.fieldKey + '_x'], 100);
-        const y = NumCast(this.layoutDoc[this.fieldKey + '_y'], 100);
-        const background = this._props.styleProvider?.(this.dataDoc, this._props, StyleProp.BackgroundColor + ':anchor');
-        const anchor = this.fieldKey === 'link_anchor_1' ? 'link_anchor_2' : 'link_anchor_1';
-        const anchorScale = !this.dataDoc[this.fieldKey + '_useSmallAnchor'] && (x === 0 || x === 100 || y === 0 || y === 100) ? 1 : 0.25;
-        const targetTitle = StrCast((this.dataDoc[anchor] as Doc)?.title);
-        const selView = SelectionManager.Views.lastElement()?._props.LayoutTemplateString?.includes('link_anchor_1')
-            ? 'link_anchor_1'
-            : SelectionManager.Views.lastElement()?._props.LayoutTemplateString?.includes('link_anchor_2')
-              ? 'link_anchor_2'
-              : '';
-        return (
-            <div
-                ref={this._ref}
-                title={targetTitle}
-                className={`linkAnchorBox-cont${small ? '-small' : ''}`}
-                onPointerEnter={e =>
-                    LinkInfo.SetLinkInfo({
-                        DocumentView: this.DocumentView,
-                        styleProvider: this._props.styleProvider,
-                        linkSrc: this.linkSource,
-                        linkDoc: this.Document,
-                        showHeader: true,
-                        location: [e.clientX, e.clientY + 20],
-                        noPreview: false,
-                    })
-                }
-                onPointerDown={this.onPointerDown}
-                onContextMenu={this.specificContextMenu}
-                style={{
-                    border: selView && this.dataDoc[selView] === this.dataDoc[this.fieldKey] ? `solid ${MEDIUM_GRAY} 2px` : undefined,
-                    background,
-                    left: `calc(${x}% - ${small ? 2.5 : 7.5}px)`,
-                    top: `calc(${y}% - ${small ? 2.5 : 7.5}px)`,
-                    transform: `scale(${anchorScale})`,
-                    cursor: 'grab',
-                }}
-            />
-        );
-    }
-}
diff --git a/src/client/views/nodes/LinkBox.tsx b/src/client/views/nodes/LinkBox.tsx
index 559b1fcae..6caa38a7f 100644
--- a/src/client/views/nodes/LinkBox.tsx
+++ b/src/client/views/nodes/LinkBox.tsx
@@ -13,12 +13,11 @@ import { TraceMobx } from '../../../fields/util';
 import { emptyFunction } from '../../../Utils';
 import { Docs } from '../../documents/Documents';
 import { DocumentType } from '../../documents/DocumentTypes';
-import { DocumentManager } from '../../util/DocumentManager';
 import { SnappingManager } from '../../util/SnappingManager';
 import { ViewBoxBaseComponent } from '../DocComponent';
 import { EditableView } from '../EditableView';
 import { LightboxView } from '../LightboxView';
-import { StyleProp } from '../StyleProvider';
+import { StyleProp } from '../StyleProp';
 import { ComparisonBox } from './ComparisonBox';
 import { DocumentView } from './DocumentView';
 import { FieldView, FieldViewProps } from './FieldView';
@@ -43,7 +42,7 @@ export class LinkBox extends ViewBoxBaseComponent<FieldViewProps>() {
     anchor = (which: number) => {
         const anch = DocCast(this.dataDoc['link_anchor_' + which]);
         const anchor = anch?.layout_unrendered ? DocCast(anch.annotationOn) : anch;
-        return DocumentManager.Instance.getDocumentView(anchor, this.DocumentView?.().containerViewPath?.().lastElement());
+        return DocumentView.getDocumentView(anchor, this.DocumentView?.().containerViewPath?.().lastElement());
     };
     _hackToSeeIfDeleted: any;
     componentWillUnmount() {
diff --git a/src/client/views/nodes/LinkDescriptionPopup.tsx b/src/client/views/nodes/LinkDescriptionPopup.tsx
index 23cb25962..ff95f8547 100644
--- a/src/client/views/nodes/LinkDescriptionPopup.tsx
+++ b/src/client/views/nodes/LinkDescriptionPopup.tsx
@@ -2,16 +2,17 @@ import { action, makeObservable, observable, reaction } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
 import { DocData } from '../../../fields/DocSymbols';
+import { StrCast } from '../../../fields/Types';
 import { LinkManager } from '../../util/LinkManager';
 import './LinkDescriptionPopup.scss';
 import { TaskCompletionBox } from './TaskCompletedBox';
-import { StrCast } from '../../../fields/Types';
 
 @observer
 export class LinkDescriptionPopup extends React.Component<{}> {
     // eslint-disable-next-line no-use-before-define
     public static Instance: LinkDescriptionPopup;
     @observable public display: boolean = false;
+    // eslint-disable-next-line react/no-unused-class-component-methods
     @observable public showDescriptions: string = 'ON';
     @observable public popupX: number = 700;
     @observable public popupY: number = 350;
diff --git a/src/client/views/nodes/LinkDocPreview.tsx b/src/client/views/nodes/LinkDocPreview.tsx
index 0936acc15..508008569 100644
--- a/src/client/views/nodes/LinkDocPreview.tsx
+++ b/src/client/views/nodes/LinkDocPreview.tsx
@@ -11,12 +11,10 @@ import { Cast, DocCast, NumCast, PromiseValue, StrCast } from '../../../fields/T
 import { DocServer } from '../../DocServer';
 import { DocumentType } from '../../documents/DocumentTypes';
 import { Docs } from '../../documents/Documents';
-import { DocumentManager } from '../../util/DocumentManager';
 import { DragManager } from '../../util/DragManager';
-import { LinkFollower } from '../../util/LinkFollower';
 import { LinkManager } from '../../util/LinkManager';
 import { SearchUtil } from '../../util/SearchUtil';
-import { SettingsManager } from '../../util/SettingsManager';
+import { SnappingManager } from '../../util/SnappingManager';
 import { Transform } from '../../util/Transform';
 import { ObservableReactComponent } from '../ObservableReactComponent';
 import { DocumentView } from './DocumentView';
@@ -134,17 +132,17 @@ export class LinkDocPreview extends ObservableReactComponent<LinkDocPreviewProps
                 const anchorDoc = anchorDocId ? PromiseValue(DocCast(DocServer.GetCachedRefField(anchorDocId) ?? DocServer.GetRefField(anchorDocId))) : undefined;
                 anchorDoc?.then?.(
                     action(anchor => {
-                        if (anchor instanceof Doc && LinkManager.Links(anchor).length) {
-                            this._linkDoc = this._linkDoc ?? LinkManager.Links(anchor)[0];
+                        if (anchor instanceof Doc && Doc.Links(anchor).length) {
+                            this._linkDoc = this._linkDoc ?? Doc.Links(anchor)[0];
                             const automaticLink = this._linkDoc.link_relationship === LinkManager.AutoKeywords;
                             if (automaticLink) {
                                 // automatic links specify the target in the link info, not the source
                                 const linkTarget = anchor;
-                                this._linkSrc = LinkManager.getOppositeAnchor(this._linkDoc, linkTarget);
+                                this._linkSrc = Doc.getOppositeAnchor(this._linkDoc, linkTarget);
                                 this._markerTargetDoc = this._targetDoc = linkTarget;
                             } else {
                                 this._linkSrc = anchor;
-                                const linkTarget = LinkManager.getOppositeAnchor(this._linkDoc, this._linkSrc);
+                                const linkTarget = Doc.getOppositeAnchor(this._linkDoc, this._linkSrc);
                                 this._markerTargetDoc = linkTarget;
                                 this._targetDoc = /* linkTarget?.type === DocumentType.MARKER && */ linkTarget?.annotationOn ? Cast(linkTarget.annotationOn, Doc, null) ?? linkTarget : linkTarget;
                             }
@@ -169,8 +167,8 @@ export class LinkDocPreview extends ObservableReactComponent<LinkDocPreviewProps
                 LinkManager.Instance.currentLink = this._linkDoc;
                 LinkManager.Instance.currentLinkAnchor = this._linkSrc;
                 this._props.DocumentView?.().select(false);
-                if ((SettingsManager.Instance.propertiesWidth ?? 0) < 100) {
-                    SettingsManager.Instance.propertiesWidth = 250;
+                if ((SnappingManager.PropertiesWidth ?? 0) < 100) {
+                    SnappingManager.SetPropertiesWidth(250);
                 }
             })
         );
@@ -196,13 +194,13 @@ export class LinkDocPreview extends ObservableReactComponent<LinkDocPreviewProps
     followLink = () => {
         LinkInfo.Clear();
         if (this._linkDoc && this._linkSrc) {
-            LinkFollower.FollowLink(this._linkDoc, this._linkSrc, false);
+            DocumentView.FollowLink(this._linkDoc, this._linkSrc, false);
         } else if (this._props.hrefs?.length) {
             const webDoc =
                 Array.from(SearchUtil.SearchCollection(Doc.MyFilesystem, this._props.hrefs[0], false).keys())
                     .filter(doc => doc.type === DocumentType.WEB)
                     .lastElement() ?? Docs.Create.WebDocument(this._props.hrefs[0], { title: this._props.hrefs[0], _nativeWidth: 850, _width: 200, _height: 400, data_useCors: true });
-            DocumentManager.Instance.showDocument(webDoc, {
+            DocumentView.showDocument(webDoc, {
                 openLocation: OpenWhere.lightbox,
                 willPan: true,
                 zoomTime: 500,
@@ -229,7 +227,7 @@ export class LinkDocPreview extends ObservableReactComponent<LinkDocPreviewProps
     };
     @computed get previewHeader() {
         return !this._linkDoc || !this._markerTargetDoc || !this._targetDoc || !this._linkSrc ? null : (
-            <div className="linkDocPreview-info" style={{ background: SettingsManager.userBackgroundColor }}>
+            <div className="linkDocPreview-info" style={{ background: SnappingManager.userBackgroundColor }}>
                 <div className="linkDocPreview-buttonBar" style={{ float: 'left' }}>
                     <Tooltip title={<div className="dash-tooltip">Edit Link</div>} placement="top">
                         <div className="linkDocPreview-button" onPointerDown={this.editLink}>
@@ -282,7 +280,7 @@ export class LinkDocPreview extends ObservableReactComponent<LinkDocPreviewProps
                     ) : (
                         <DocumentView
                             ref={r => {
-                                const targetanchor = this._linkDoc && this._linkSrc && LinkManager.getOppositeAnchor(this._linkDoc, this._linkSrc);
+                                const targetanchor = this._linkDoc && this._linkSrc && Doc.getOppositeAnchor(this._linkDoc, this._linkSrc);
                                 targetanchor && this._targetDoc !== targetanchor && r?._props.focus?.(targetanchor, {});
                             }}
                             Document={this._targetDoc!}
@@ -325,7 +323,7 @@ export class LinkDocPreview extends ObservableReactComponent<LinkDocPreviewProps
                 className="linkDocPreview"
                 ref={this._linkDocRef}
                 onPointerDown={this.followLinkPointerDown}
-                style={{ borderColor: SettingsManager.userColor, left: this._props.location[0], top: this._props.location[1], width: this.width() + borders, height: this.height() + borders + (this._props.showHeader ? 37 : 0) }}>
+                style={{ borderColor: SnappingManager.userColor, left: this._props.location[0], top: this._props.location[1], width: this.width() + borders, height: this.height() + borders + (this._props.showHeader ? 37 : 0) }}>
                 {this.docPreview}
             </div>
         );
diff --git a/src/client/views/nodes/LoadingBox.tsx b/src/client/views/nodes/LoadingBox.tsx
index aa89398f3..5f343bdfe 100644
--- a/src/client/views/nodes/LoadingBox.tsx
+++ b/src/client/views/nodes/LoadingBox.tsx
@@ -6,12 +6,11 @@ import { Doc } from '../../../fields/Doc';
 import { Id } from '../../../fields/FieldSymbols';
 import { StrCast } from '../../../fields/Types';
 import { Networking } from '../../Network';
-import { DocumentManager } from '../../util/DocumentManager';
+import { DocumentType } from '../../documents/DocumentTypes';
+import { Docs } from '../../documents/Documents';
 import { ViewBoxAnnotatableComponent } from '../DocComponent';
 import { FieldView, FieldViewProps } from './FieldView';
 import './LoadingBox.scss';
-import { DocumentType } from '../../documents/DocumentTypes';
-import { Docs } from '../../documents/Documents';
 
 /**
  * LoadingBox Class represents a placeholder doc for documents that are currently
@@ -39,25 +38,11 @@ export class LoadingBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
     public static LayoutString(fieldKey: string) {
         return FieldView.LayoutString(LoadingBox, fieldKey);
     }
-    // removes from currently loading display
-    public static removeCurrentlyLoading(doc: Doc) {
-        if (DocumentManager.Instance.CurrentlyLoading) {
-            const index = DocumentManager.Instance.CurrentlyLoading.indexOf(doc);
-            runInAction(() => index !== -1 && DocumentManager.Instance.CurrentlyLoading.splice(index, 1));
-        }
-    }
-
-    // adds doc to currently loading display
-    public static addCurrentlyLoading(doc: Doc) {
-        if (DocumentManager.Instance.CurrentlyLoading.indexOf(doc) === -1) {
-            runInAction(() => DocumentManager.Instance.CurrentlyLoading.push(doc));
-        }
-    }
 
     _timer: any;
     @observable progress = '';
     componentDidMount() {
-        if (!DocumentManager.Instance.CurrentlyLoading?.includes(this.Document)) {
+        if (!Doc.CurrentlyLoading?.includes(this.Document)) {
             this.Document.loadingError = 'Upload interrupted, please try again';
         } else {
             const updateFunc = async () => {
diff --git a/src/client/views/nodes/MapBox/DirectionsAnchorMenu.tsx b/src/client/views/nodes/MapBox/DirectionsAnchorMenu.tsx
index f176317d2..b8fd8ac6a 100644
--- a/src/client/views/nodes/MapBox/DirectionsAnchorMenu.tsx
+++ b/src/client/views/nodes/MapBox/DirectionsAnchorMenu.tsx
@@ -8,9 +8,9 @@ import { returnFalse } from '../../../../ClientUtils';
 import { unimplementedFunction } from '../../../../Utils';
 import { Doc, Opt } from '../../../../fields/Doc';
 import { NumCast, StrCast } from '../../../../fields/Types';
-import { SelectionManager } from '../../../util/SelectionManager';
 import { SettingsManager } from '../../../util/SettingsManager';
 import { AntimodeMenu, AntimodeMenuProps } from '../../AntimodeMenu';
+import { DocumentView } from '../DocumentView';
 
 @observer
 export class DirectionsAnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
@@ -56,7 +56,7 @@ export class DirectionsAnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
 
     componentDidMount() {
         this._disposer = reaction(
-            () => SelectionManager.Views.slice(),
+            () => DocumentView.Selected().slice(),
             () => DirectionsAnchorMenu.Instance.fadeOut(true)
         );
     }
diff --git a/src/client/views/nodes/MapBox/MapAnchorMenu.tsx b/src/client/views/nodes/MapBox/MapAnchorMenu.tsx
index 174511e1a..103a35434 100644
--- a/src/client/views/nodes/MapBox/MapAnchorMenu.tsx
+++ b/src/client/views/nodes/MapBox/MapAnchorMenu.tsx
@@ -13,9 +13,9 @@ import { unimplementedFunction } from '../../../../Utils';
 import { Doc, Opt } from '../../../../fields/Doc';
 import { NumCast, StrCast } from '../../../../fields/Types';
 import { CalendarManager } from '../../../util/CalendarManager';
-import { SelectionManager } from '../../../util/SelectionManager';
 import { SettingsManager } from '../../../util/SettingsManager';
 import { AntimodeMenu, AntimodeMenuProps } from '../../AntimodeMenu';
+import { DocumentView } from '../DocumentView';
 import './MapAnchorMenu.scss';
 import { MapboxApiUtility, TransportationType } from './MapboxApiUtility';
 import { MarkerIcons } from './MarkerIcons';
@@ -126,7 +126,7 @@ export class MapAnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
 
     componentDidMount() {
         this._disposer = reaction(
-            () => SelectionManager.Views.slice(),
+            () => DocumentView.Selected().slice(),
             () => MapAnchorMenu.Instance.fadeOut(true)
         );
     }
diff --git a/src/client/views/nodes/MapBox/MapBox.tsx b/src/client/views/nodes/MapBox/MapBox.tsx
index 50831f8ea..ac8010f11 100644
--- a/src/client/views/nodes/MapBox/MapBox.tsx
+++ b/src/client/views/nodes/MapBox/MapBox.tsx
@@ -21,9 +21,7 @@ import { DocCast, NumCast, StrCast, toList } from '../../../../fields/Types';
 import { DocUtils } from '../../../documents/DocUtils';
 import { DocumentType } from '../../../documents/DocumentTypes';
 import { Docs } from '../../../documents/Documents';
-import { DocumentManager } from '../../../util/DocumentManager';
 import { DragManager } from '../../../util/DragManager';
-import { LinkManager } from '../../../util/LinkManager';
 import { UndoManager, undoable } from '../../../util/UndoManager';
 import { ViewBoxAnnotatableComponent, ViewBoxInterface } from '../../DocComponent';
 import { PinDocView, PinProps } from '../../PinFuncs';
@@ -249,7 +247,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
             if (existingPin) {
                 setTimeout(() => {
                     // we use a timeout in case this is called from the sidebar which may have just added a link that hasn't made its way into th elink manager yet
-                    if (!LinkManager.Instance.getAllRelatedLinks(doc).some(link => DocCast(link.link_anchor_1)?.mapPin === existingPin || DocCast(link.link_anchor_2)?.mapPin === existingPin)) {
+                    if (!Doc.Links(doc).some(link => DocCast(link.link_anchor_1)?.mapPin === existingPin || DocCast(link.link_anchor_2)?.mapPin === existingPin)) {
                         const anchor = this.getAnchor(true, undefined, existingPin);
                         anchor && DocUtils.MakeLink(anchor, doc, { link_relationship: 'link to map location' });
                         doc.latitude = existingPin?.latitude;
@@ -438,7 +436,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
             options.didMove = true;
         }
         return new Promise<Opt<DocumentView>>(res => {
-            DocumentManager.Instance.AddViewRenderedCb(doc, dv => res(dv));
+            DocumentView.addViewRenderedCb(doc, dv => res(dv));
         });
     };
 
diff --git a/src/client/views/nodes/MapBox/MapBox2.tsx b/src/client/views/nodes/MapBox/MapBox2.tsx
index 9825824bd..7697fd295 100644
--- a/src/client/views/nodes/MapBox/MapBox2.tsx
+++ b/src/client/views/nodes/MapBox/MapBox2.tsx
@@ -509,9 +509,9 @@
 
 //     // TODO: auto center on select a document in the sidebar
 //     private handleMapCenter = (map: google.maps.Map) => {
-//         // console.log("print the selected views in selectionManager:")
-//         // if (SelectionManager.Views.lastElement()) {
-//         //     console.log(SelectionManager.Views.lastElement());
+//         // console.log("print the selected views in Document.Selected:")
+//         // if (DocumentView.Selected().lastElement()) {
+//         //     console.log(DocumentView.Selected().lastElement());
 //         // }
 //     };
 
diff --git a/src/client/views/nodes/MapboxMapBox/MapboxContainer.tsx b/src/client/views/nodes/MapboxMapBox/MapboxContainer.tsx
index 3b4ffd4bd..bfd40692b 100644
--- a/src/client/views/nodes/MapboxMapBox/MapboxContainer.tsx
+++ b/src/client/views/nodes/MapboxMapBox/MapboxContainer.tsx
@@ -13,9 +13,7 @@ import { DocCast, NumCast, StrCast, toList } from '../../../../fields/Types';
 import { DocUtils } from '../../../documents/DocUtils';
 import { DocumentType } from '../../../documents/DocumentTypes';
 import { Docs } from '../../../documents/Documents';
-import { DocumentManager } from '../../../util/DocumentManager';
 import { DragManager } from '../../../util/DragManager';
-import { LinkManager } from '../../../util/LinkManager';
 import { Transform } from '../../../util/Transform';
 import { UndoManager, undoable } from '../../../util/UndoManager';
 import { ViewBoxAnnotatableComponent } from '../../DocComponent';
@@ -130,7 +128,7 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent<FieldViewProps>
             if (existingPin) {
                 setTimeout(() => {
                     // we use a timeout in case this is called from the sidebar which may have just added a link that hasn't made its way into th elink manager yet
-                    if (!LinkManager.Instance.getAllRelatedLinks(doc).some(link => DocCast(link.link_anchor_1)?.mapPin === existingPin || DocCast(link.link_anchor_2)?.mapPin === existingPin)) {
+                    if (!Doc.Links(doc).some(link => DocCast(link.link_anchor_1)?.mapPin === existingPin || DocCast(link.link_anchor_2)?.mapPin === existingPin)) {
                         const anchor = this.getAnchor(true, undefined, existingPin);
                         anchor && DocUtils.MakeLink(anchor, doc, { link_relationship: 'link to map location' });
                         doc.latitude = existingPin?.latitude;
@@ -390,7 +388,7 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent<FieldViewProps>
             options.didMove = true;
         }
         return new Promise<Opt<DocumentView>>(res => {
-            DocumentManager.Instance.AddViewRenderedCb(doc, dv => res(dv));
+            DocumentView.addViewRenderedCb(doc, dv => res(dv));
         });
     };
     /*
diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx
index 5d1874aca..9038ed27a 100644
--- a/src/client/views/nodes/PDFBox.tsx
+++ b/src/client/views/nodes/PDFBox.tsx
@@ -19,9 +19,7 @@ import { emptyFunction } from '../../../Utils';
 import { Docs } from '../../documents/Documents';
 import { CollectionViewType, DocumentType } from '../../documents/DocumentTypes';
 import { DocUtils } from '../../documents/DocUtils';
-import { DocumentManager } from '../../util/DocumentManager';
 import { KeyCodes } from '../../util/KeyCodes';
-import { SelectionManager } from '../../util/SelectionManager';
 import { undoBatch, UndoManager } from '../../util/UndoManager';
 import { CollectionFreeFormView } from '../collections/collectionFreeForm';
 import { CollectionStackingView } from '../collections/CollectionStackingView';
@@ -251,7 +249,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
             this.toggleSidebar(false);
         }
         return new Promise<Opt<DocumentView>>(res => {
-            DocumentManager.Instance.AddViewRenderedCb(doc, dv => res(dv));
+            DocumentView.addViewRenderedCb(doc, dv => res(dv));
         });
     };
 
@@ -561,7 +559,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
                     removeDocument={this.removeDocument}
                 />
             ) : (
-                <div onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, () => SelectionManager.SelectView(this.DocumentView?.()!, false), true)}>
+                <div onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, () => DocumentView.SelectView(this.DocumentView?.()!, false), true)}>
                     <ComponentTag
                         // eslint-disable-next-line react/jsx-props-no-spreading
                         {...this._props}
diff --git a/src/client/views/nodes/RecordingBox/RecordingBox.tsx b/src/client/views/nodes/RecordingBox/RecordingBox.tsx
index ae6caf9f4..e46e40bfe 100644
--- a/src/client/views/nodes/RecordingBox/RecordingBox.tsx
+++ b/src/client/views/nodes/RecordingBox/RecordingBox.tsx
@@ -3,6 +3,7 @@ import { observer } from 'mobx-react';
 import * as React from 'react';
 import { DateField } from '../../../../fields/DateField';
 import { Doc, DocListCast } from '../../../../fields/Doc';
+import { DocData } from '../../../../fields/DocSymbols';
 import { Id } from '../../../../fields/FieldSymbols';
 import { List } from '../../../../fields/List';
 import { BoolCast, DocCast } from '../../../../fields/Types';
@@ -10,19 +11,18 @@ import { VideoField } from '../../../../fields/URLField';
 import { Upload } from '../../../../server/SharedMediaTypes';
 import { DocumentType } from '../../../documents/DocumentTypes';
 import { Docs } from '../../../documents/Documents';
-import { DocumentManager } from '../../../util/DocumentManager';
 import { DragManager } from '../../../util/DragManager';
+import { dropActionType } from '../../../util/DropActionTypes';
 import { ScriptingGlobals } from '../../../util/ScriptingGlobals';
 import { Presentation } from '../../../util/TrackMovements';
 import { undoBatch } from '../../../util/UndoManager';
 import { ViewBoxBaseComponent } from '../../DocComponent';
 import { CollectionFreeFormView } from '../../collections/collectionFreeForm/CollectionFreeFormView';
 import { mediaState } from '../AudioBox';
+import { DocumentView } from '../DocumentView';
 import { FieldView, FieldViewProps } from '../FieldView';
 import { VideoBox } from '../VideoBox';
 import { RecordingView } from './RecordingView';
-import { DocData } from '../../../../fields/DocSymbols';
-import { dropActionType } from '../../../util/DropActionTypes';
 
 @observer
 export class RecordingBox extends ViewBoxBaseComponent<FieldViewProps>() {
@@ -102,7 +102,7 @@ export class RecordingBox extends ViewBoxBaseComponent<FieldViewProps>() {
         screengrabber.overlayY = 590; // was 0
         screengrabber[DocData][Doc.LayoutFieldKey(screengrabber) + '_trackScreen'] = true;
         Doc.AddToMyOverlay(screengrabber); // just adds doc to overlay
-        DocumentManager.Instance.AddViewRenderedCb(screengrabber, docView => {
+        DocumentView.addViewRenderedCb(screengrabber, docView => {
             RecordingBox.screengrabber = docView.ComponentView as RecordingBox;
             RecordingBox.screengrabber.Record?.();
         });
@@ -119,7 +119,7 @@ export class RecordingBox extends ViewBoxBaseComponent<FieldViewProps>() {
         value.overlayX = 70;
         value.overlayY = window.innerHeight - 180;
         Doc.AddToMyOverlay(value);
-        DocumentManager.Instance.AddViewRenderedCb(value, docView => {
+        DocumentView.addViewRenderedCb(value, docView => {
             Doc.UserDoc().currentRecording = docView.Document;
             docView.select(false);
             RecordingBox.resumeWorkspaceReplaying(value);
@@ -132,7 +132,7 @@ export class RecordingBox extends ViewBoxBaseComponent<FieldViewProps>() {
      */
     @undoBatch
     public static addRecToWorkspace(value: RecordingBox) {
-        const ffView = Array.from(DocumentManager.Instance.DocumentViews).find(view => view.ComponentView instanceof CollectionFreeFormView);
+        const ffView = DocumentView.allViews().find(view => view.ComponentView instanceof CollectionFreeFormView);
         (ffView?.ComponentView as CollectionFreeFormView)._props.addDocument?.(value.Document);
         Doc.RemoveDocFromList(Doc.UserDoc(), 'workspaceRecordings', value.Document);
         Doc.RemFromMyOverlay(value.Document);
@@ -142,7 +142,7 @@ export class RecordingBox extends ViewBoxBaseComponent<FieldViewProps>() {
     }
 
     public static resumeWorkspaceReplaying(doc: Doc) {
-        const docView = DocumentManager.Instance.getDocumentView(doc);
+        const docView = DocumentView.getDocumentView(doc);
         if (docView?.ComponentView instanceof VideoBox) {
             docView.ComponentView.Play();
         }
@@ -150,7 +150,7 @@ export class RecordingBox extends ViewBoxBaseComponent<FieldViewProps>() {
     }
 
     public static pauseWorkspaceReplaying(doc: Doc) {
-        const docView = DocumentManager.Instance.getDocumentView(doc);
+        const docView = DocumentView.getDocumentView(doc);
         const videoBox = docView?.ComponentView as VideoBox;
         if (videoBox) {
             videoBox.Pause();
diff --git a/src/client/views/nodes/ScreenshotBox.tsx b/src/client/views/nodes/ScreenshotBox.tsx
index 8eb55b2f8..3be50f5e6 100644
--- a/src/client/views/nodes/ScreenshotBox.tsx
+++ b/src/client/views/nodes/ScreenshotBox.tsx
@@ -24,6 +24,7 @@ import { SettingsManager } from '../../util/SettingsManager';
 import { TrackMovements } from '../../util/TrackMovements';
 import { ContextMenu } from '../ContextMenu';
 import { ViewBoxAnnotatableComponent } from '../DocComponent';
+import { DocViewUtils } from '../DocViewUtils';
 import { CollectionStackedTimeline } from '../collections/CollectionStackedTimeline';
 import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView';
 import { mediaState } from './AudioBox';
@@ -161,8 +162,8 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
         //     });
     }
     componentWillUnmount() {
-        const ind = DocUtils.ActiveRecordings.indexOf(this);
-        ind !== -1 && DocUtils.ActiveRecordings.splice(ind, 1);
+        const ind = DocViewUtils.ActiveRecordings.indexOf(this);
+        ind !== -1 && DocViewUtils.ActiveRecordings.splice(ind, 1);
     }
 
     specificContextMenu = (): void => {
@@ -265,7 +266,7 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
                 this._screenCapture = true;
                 this.dataDoc.mediaState = 'recording';
             });
-            DocUtils.ActiveRecordings.push(this);
+            DocViewUtils.ActiveRecordings.push(this);
         } else {
             this._audioRec?.stop();
             this._videoRec?.stop();
@@ -273,8 +274,8 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
                 this._screenCapture = false;
                 this.dataDoc.mediaState = 'paused';
             });
-            const ind = DocUtils.ActiveRecordings.indexOf(this);
-            ind !== -1 && DocUtils.ActiveRecordings.splice(ind, 1);
+            const ind = DocViewUtils.ActiveRecordings.indexOf(this);
+            ind !== -1 && DocViewUtils.ActiveRecordings.splice(ind, 1);
 
             CaptureManager.Instance.open(this.Document);
         }
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index f32d00386..afd73cfe8 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -16,9 +16,7 @@ import { emptyFunction, formatTime } from '../../../Utils';
 import { Docs } from '../../documents/Documents';
 import { DocumentType } from '../../documents/DocumentTypes';
 import { DocUtils, FollowLinkScript } from '../../documents/DocUtils';
-import { DocumentManager } from '../../util/DocumentManager';
 import { dropActionType } from '../../util/DropActionTypes';
-import { LinkManager } from '../../util/LinkManager';
 import { ReplayMovements } from '../../util/ReplayMovements';
 import { undoBatch } from '../../util/UndoManager';
 import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView';
@@ -26,16 +24,16 @@ import { CollectionStackedTimeline, TrimScope } from '../collections/CollectionS
 import { ContextMenu } from '../ContextMenu';
 import { ContextMenuProps } from '../ContextMenuItem';
 import { ViewBoxAnnotatableComponent, ViewBoxInterface } from '../DocComponent';
+import { VideoThumbnails } from '../global/globalEnums';
 import { MarqueeAnnotator } from '../MarqueeAnnotator';
 import { AnchorMenu } from '../pdf/AnchorMenu';
 import { PinDocView, PinProps } from '../PinFuncs';
-import { StyleProp } from '../StyleProvider';
+import { StyleProp } from '../StyleProp';
 import { DocumentView } from './DocumentView';
 import { FieldView, FieldViewProps } from './FieldView';
 import { FocusViewOptions } from './FocusViewOptions';
 import { RecordingBox } from './RecordingBox';
 import './VideoBox.scss';
-import { VideoThumbnails } from '../global/globalEnums';
 
 /**
  * VideoBox
@@ -88,7 +86,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl
     @observable _scrubbing: boolean = false;
 
     @computed get links() {
-        return LinkManager.Links(this.dataDoc);
+        return Doc.Links(this.dataDoc);
     }
     @computed get heightPercent() {
         return NumCast(this.layoutDoc._layout_timelineHeightPercent, 100);
@@ -347,7 +345,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl
         this._props.addDocument?.(imageSnapshot);
         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);  // 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));
+        setTimeout(() => downX !== undefined && downY !== undefined && DocumentView.getFirstDocumentView(imageSnapshot)?.startDragging(downX, downY, dropActionType.move, true));
     };
 
     getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => {
@@ -392,7 +390,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl
             return this._stackedTimeline.getView(doc, options);
         }
         return new Promise<Opt<DocumentView>>(res => {
-            DocumentManager.Instance.AddViewRenderedCb(doc, dv => res(dv));
+            DocumentView.addViewRenderedCb(doc, dv => res(dv));
         });
     };
 
@@ -446,7 +444,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl
                 { fireImmediately: true }
             );
 
-            (!this.dataDoc[this.fieldKey + '_thumbnails'] || StrListCast(this.dataDoc[this.fieldKey + '_thumbnails']).length !== VideoBox.numThumbnails) && this.getVideoThumbnails();
+            (!this.dataDoc[this.fieldKey + '_thumbnails'] || StrListCast(this.dataDoc[this.fieldKey + '_thumbnails']).length !== VideoThumbnails.DENSE) && this.getVideoThumbnails();
         }
     };
 
@@ -652,20 +650,20 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl
     @action
     removeCurrentlyPlaying = () => {
         const docView = this.DocumentView?.();
-        if (CollectionStackedTimeline.CurrentlyPlaying && docView) {
-            const index = CollectionStackedTimeline.CurrentlyPlaying.indexOf(docView);
-            index !== -1 && CollectionStackedTimeline.CurrentlyPlaying.splice(index, 1);
+        if (DocumentView.CurrentlyPlaying && docView) {
+            const index = DocumentView.CurrentlyPlaying.indexOf(docView);
+            index !== -1 && DocumentView.CurrentlyPlaying.splice(index, 1);
         }
     };
     // adds doc to currently playing display
     @action
     addCurrentlyPlaying = () => {
         const docView = this.DocumentView?.();
-        if (!CollectionStackedTimeline.CurrentlyPlaying) {
-            CollectionStackedTimeline.CurrentlyPlaying = [];
+        if (!DocumentView.CurrentlyPlaying) {
+            DocumentView.CurrentlyPlaying = [];
         }
-        if (docView && CollectionStackedTimeline.CurrentlyPlaying.indexOf(docView) === -1) {
-            CollectionStackedTimeline.CurrentlyPlaying.push(docView);
+        if (docView && DocumentView.CurrentlyPlaying.indexOf(docView) === -1) {
+            DocumentView.CurrentlyPlaying.push(docView);
         }
     };
 
diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx
index 54f246b20..1003bc473 100644
--- a/src/client/views/nodes/WebBox.tsx
+++ b/src/client/views/nodes/WebBox.tsx
@@ -21,7 +21,6 @@ import { emptyFunction, stringHash } from '../../../Utils';
 import { Docs } from '../../documents/Documents';
 import { DocumentType } from '../../documents/DocumentTypes';
 import { DocUtils } from '../../documents/DocUtils';
-import { DocumentManager } from '../../util/DocumentManager';
 import { ScriptingGlobals } from '../../util/ScriptingGlobals';
 import { SnappingManager } from '../../util/SnappingManager';
 import { undoBatch, UndoManager } from '../../util/UndoManager';
@@ -38,7 +37,7 @@ import { Annotation } from '../pdf/Annotation';
 import { GPTPopup } from '../pdf/GPTPopup/GPTPopup';
 import { PinDocView, PinProps } from '../PinFuncs';
 import { SidebarAnnos } from '../SidebarAnnos';
-import { StyleProp } from '../StyleProvider';
+import { StyleProp } from '../StyleProp';
 import { DocumentView } from './DocumentView';
 import { FieldView, FieldViewProps } from './FieldView';
 import { FocusViewOptions } from './FocusViewOptions';
@@ -324,7 +323,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
         if (this._url && webUrl && webUrl.href !== this._url) this.setData(webUrl.href);
         if (this._sidebarRef?.current?.makeDocUnfiltered(doc) && !this.SidebarShown) this.toggleSidebar(false);
         return new Promise<Opt<DocumentView>>(res => {
-            DocumentManager.Instance.AddViewRenderedCb(doc, dv => res(dv));
+            DocumentView.addViewRenderedCb(doc, dv => res(dv));
         });
     };
 
diff --git a/src/client/views/nodes/formattedText/DashFieldView.tsx b/src/client/views/nodes/formattedText/DashFieldView.tsx
index 1c5ea2dd4..5d53a2a5f 100644
--- a/src/client/views/nodes/formattedText/DashFieldView.tsx
+++ b/src/client/views/nodes/formattedText/DashFieldView.tsx
@@ -293,7 +293,6 @@ export class DashFieldView {
     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.getpos = getPos;
@@ -305,14 +304,14 @@ export class DashFieldView {
         this.dom.onkeypress = function (e: KeyboardEvent) {
             e.stopPropagation();
         };
-        this.dom.onkeydown = function (e: KeyboardEvent) {
+        this.dom.onkeydown = (e: KeyboardEvent) => {
             e.stopPropagation();
             if (e.key === 'Tab') {
                 e.preventDefault();
                 const editor = tbox.EditorView;
                 if (editor) {
                     const { state } = editor;
-                    for (let i = self.getpos() + 1; i < state.doc.content.size; i++) {
+                    for (let i = this.getpos() + 1; 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;
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index ad6629fc9..8a4a7718a 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -32,16 +32,13 @@ import { Docs } from '../../../documents/Documents';
 import { CollectionViewType, DocumentType } from '../../../documents/DocumentTypes';
 import { DocUtils } from '../../../documents/DocUtils';
 import { DictationManager } from '../../../util/DictationManager';
-import { DocumentManager } from '../../../util/DocumentManager';
 import { DragManager } from '../../../util/DragManager';
 import { dropActionType } from '../../../util/DropActionTypes';
 import { MakeTemplate } from '../../../util/DropConverter';
 import { LinkManager } from '../../../util/LinkManager';
 import { RTFMarkup } from '../../../util/RTFMarkup';
-import { SelectionManager } from '../../../util/SelectionManager';
 import { SnappingManager } from '../../../util/SnappingManager';
 import { undoable, undoBatch, UndoManager } from '../../../util/UndoManager';
-import { CollectionFreeFormView } from '../../collections/collectionFreeForm/CollectionFreeFormView';
 import { CollectionStackingView } from '../../collections/CollectionStackingView';
 import { CollectionTreeView } from '../../collections/CollectionTreeView';
 import { ContextMenu } from '../../ContextMenu';
@@ -53,9 +50,10 @@ import { AnchorMenu } from '../../pdf/AnchorMenu';
 import { GPTPopup } from '../../pdf/GPTPopup/GPTPopup';
 import { PinDocView, PinProps } from '../../PinFuncs';
 import { SidebarAnnos } from '../../SidebarAnnos';
-import { styleFromLayoutString, StyleProp } from '../../StyleProvider';
+import { StyleProp } from '../../StyleProp';
+import { styleFromLayoutString } from '../../StyleProvider';
 import { mediaState } from '../AudioBox';
-import { DocumentView, DocumentViewInternal } from '../DocumentView';
+import { DocumentView } from '../DocumentView';
 import { FieldView, FieldViewProps } from '../FieldView';
 import { FocusViewOptions } from '../FocusViewOptions';
 import { LinkInfo } from '../LinkDocPreview';
@@ -260,7 +258,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
                     let stopFunc: any;
                     const targetData = target[DocData];
                     targetData.mediaState = mediaState.Recording;
-                    DocumentViewInternal.recordAudioAnnotation(targetData, Doc.LayoutFieldKey(target), stop => { stopFunc = stop }); // prettier-ignore
+                    DictationManager.recordAudioAnnotation(targetData, Doc.LayoutFieldKey(target), stop => { stopFunc = stop }); // prettier-ignore
 
                     const reactionDisposer = reaction(
                         () => target.mediaState,
@@ -271,7 +269,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
                             }
                         }
                     );
-                    target.title = ComputedField.MakeFunction(`self["text_audioAnnotations_text"].lastElement()`);
+                    target.title = ComputedField.MakeFunction(`this.text_audioAnnotations_text.lastElement()`);
                 }
             });
         };
@@ -400,7 +398,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
     insertTime = () => {
         let linkTime;
         let linkAnchor;
-        LinkManager.Links(this.dataDoc).forEach(l => {
+        Doc.Links(this.dataDoc).forEach(l => {
             const anchor = DocCast(l.link_anchor_1)?.annotationOn ? DocCast(l.link_anchor_1) : DocCast(l.link_anchor_2)?.annotationOn ? DocCast(l.link_anchor_2) : undefined;
             if (anchor && (anchor.annotationOn as Doc).mediaState === mediaState.Recording) {
                 linkTime = NumCast(anchor._timecodeToShow /* audioStart */);
@@ -423,7 +421,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
 
     autoLink = () => {
         const newAutoLinks = new Set<Doc>();
-        const oldAutoLinks = LinkManager.Links(this.Document).filter(
+        const oldAutoLinks = Doc.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)) &&
@@ -442,7 +440,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
             tr = tr.setSelection(new TextSelection(tr.doc.resolve(from), tr.doc.resolve(to)));
             this._editorView?.dispatch(tr);
         }
-        oldAutoLinks.filter(oldLink => !newAutoLinks.has(oldLink) && oldLink.link_anchor_2 !== this.Document).forEach(LinkManager.Instance.deleteLink);
+        oldAutoLinks.filter(oldLink => !newAutoLinks.has(oldLink) && oldLink.link_anchor_2 !== this.Document).forEach(doc => Doc.DeleteLink?.(doc));
     };
 
     updateTitle = () => {
@@ -496,7 +494,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
                         if (node.firstChild === null && !node.marks.find((m: Mark) => m.type.name === schema.marks.noAutoLinkAnchor.name) && node.marks.find((m: Mark) => m.type.name === schema.marks.splitter.name)) {
                             alink =
                                 alink ??
-                                (LinkManager.Links(this.Document).find(
+                                (Doc.Links(this.Document).find(
                                     link =>
                                         Doc.AreProtosEqual(Cast(link.link_anchor_1, Doc, null), this.Document) && //
                                         Doc.AreProtosEqual(Cast(link.link_anchor_2, Doc, null), target)
@@ -787,7 +785,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
 
     deleteAnnotation = (anchor: Doc) => {
         const batch = UndoManager.StartBatch('delete link');
-        LinkManager.Instance.deleteLink(LinkManager.Links(anchor)[0]);
+        Doc.DeleteLink?.(Doc.Links(anchor)[0]);
         // const docAnnotations = DocListCast(this._props.dataDoc[this.fieldKey]);
         // this._props.dataDoc[this.fieldKey] = new List<Doc>(docAnnotations.filter(a => a !== this.annoTextRegion));
         // AnchorMenu.Instance.fadeOut(true);
@@ -837,7 +835,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
             anchorDoc &&
                 DocServer.GetRefField(anchorDoc).then(
                     action(anchor => {
-                        anchor && SelectionManager.SelectSchemaViewDoc(anchor as Doc);
+                        anchor && DocumentView.SelectSchemaDoc(anchor as Doc);
                         AnchorMenu.Instance.Status = 'annotation';
                         AnchorMenu.Instance.Delete = !anchor && editor.state.selection.empty ? returnFalse : !anchor ? deleteMarkups : () => this.deleteAnnotation(anchor as Doc);
                         AnchorMenu.Instance.Pinned = false;
@@ -1124,7 +1122,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
             setTimeout(() => this._sidebarRef?.current?.makeDocUnfiltered(doc));
         }
         return new Promise<Opt<DocumentView>>(res => {
-            DocumentManager.Instance.AddViewRenderedCb(doc, dv => res(dv));
+            DocumentView.addViewRenderedCb(doc, dv => res(dv));
         });
     };
     focus = (textAnchor: Doc, options: FocusViewOptions) => {
@@ -1206,7 +1204,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
     }
     componentDidMount() {
         !this._props.dontSelectOnLoad && this._props.setContentViewBox?.(this); // this tells the DocumentView that this AudioBox is the "content" of the document.  this allows the DocumentView to indirectly call getAnchor() on the AudioBox when making a link.
-        this._cachedLinks = LinkManager.Links(this.Document);
+        this._cachedLinks = Doc.Links(this.Document);
         this._disposers.breakupDictation = reaction(() => Doc.RecordingEvent, this.breakupDictation);
         this._disposers.layout_autoHeight = reaction(
             () => ({ autoHeight: this.layout_autoHeight, fontSize: this.fontSize, css: this.Document[DocCss] }),
@@ -1244,7 +1242,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
             { fireImmediately: !Array.from(FormattedTextBox._globalHighlights).includes('Bold Text') }
         );
         this._disposers.links = reaction(
-            () => LinkManager.Links(this.dataDoc), // if a link is deleted, then remove all hyperlinks that reference it from the text's marks
+            () => Doc.Links(this.dataDoc), // if a link is deleted, then remove all hyperlinks that reference it from the text's marks
             newLinks => {
                 this._cachedLinks.forEach(l => !newLinks.includes(l) && this.RemoveLinkFromDoc(l));
                 this._cachedLinks = newLinks;
@@ -1556,7 +1554,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
                     // const timecode = NumCast(anchor.timecodeToShow, 0);
                     const audiodoc = anchor.annotationOn as Doc;
                     const func = () => {
-                        const docView = DocumentManager.Instance.getDocumentView(audiodoc);
+                        const docView = DocumentView.getDocumentView(audiodoc);
                         if (!docView) {
                             this._props.addDocTab(audiodoc, OpenWhere.addBottom);
                             setTimeout(func);
@@ -1769,7 +1767,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
             case 'Escape':
                 this._editorView!.dispatch(state.tr.setSelection(TextSelection.create(state.doc, state.selection.from, state.selection.from)));
                 (document.activeElement as any).blur?.();
-                SelectionManager.DeselectAll();
+                DocumentView.DeselectAll();
                 RichTextMenu.Instance?.updateMenu(undefined, undefined, undefined, undefined);
                 return;
             case 'Enter':
@@ -1891,7 +1889,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
     }
     @computed get sidebarCollection() {
         const renderComponent = (tag: string) => {
-            const ComponentTag: any = tag === CollectionViewType.Freeform ? CollectionFreeFormView : tag === CollectionViewType.Tree ? CollectionTreeView : tag === 'translation' ? FormattedTextBox : CollectionStackingView;
+            const ComponentTag: any = tag === CollectionViewType.Tree ? CollectionTreeView : tag === 'translation' ? FormattedTextBox : CollectionStackingView;
             return ComponentTag === CollectionStackingView ? (
                 <SidebarAnnos
                     ref={this._sidebarRef}
@@ -1913,7 +1911,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
                     setHeight={this.setSidebarHeight}
                 />
             ) : (
-                <div onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, () => SelectionManager.SelectView(this.DocumentView?.()!, false), true)}>
+                <div onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, () => DocumentView.SelectView(this.DocumentView?.()!, false), true)}>
                     <ComponentTag
                         // eslint-disable-next-line react/jsx-props-no-spreading
                         {...this._props}
@@ -1998,11 +1996,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
     @observable _isHovering = false;
     onPassiveWheel = (e: WheelEvent) => {
         if (e.clientX > this.ProseRef!.getBoundingClientRect().right) {
-            if (this.dataDoc[this.SidebarKey + '_type_collection'] === CollectionViewType.Freeform) {
-                // if the scrolled freeform is a child of the sidebar component, we need to let the event go through
-                // so react can let the freeform view handle it. We prevent default to stop any containing views from scrolling
-                e.preventDefault();
-            }
             return;
         }
 
@@ -2037,7 +2030,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
     }
     render() {
         TraceMobx();
-        const scale = this._props.NativeDimScaling?.() || 1; // * NumCast(this.layoutDoc._freeform_scale, 1);
+        const scale = this._props.NativeDimScaling?.() || 1;
         const rounded = StrCast(this.layoutDoc.layout_borderRounding) === '100%' ? '-rounded' : '';
         setTimeout(() => !this._props.isContentActive() && FormattedTextBoxComment.textBox === this && FormattedTextBoxComment.Hide);
         const paddingX = NumCast(this.layoutDoc._xMargin, this._props.xPadding || 0);
diff --git a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts
index bc6454146..7a8b72be0 100644
--- a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts
+++ b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts
@@ -11,7 +11,7 @@ import { AclAdmin, AclAugment, AclEdit, DocData } from '../../../../fields/DocSy
 import { GetEffectiveAcl } from '../../../../fields/util';
 import { Docs } from '../../../documents/Documents';
 import { RTFMarkup } from '../../../util/RTFMarkup';
-import { SelectionManager } from '../../../util/SelectionManager';
+import { DocumentView } from '../DocumentView';
 import { OpenWhere } from '../OpenWhere';
 
 const mac = typeof navigator !== 'undefined' ? /Mac/.test(navigator.platform) : false;
@@ -172,7 +172,7 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any): KeyMa
     bind('Escape', (state: EditorState, dispatch: (tx: Transaction) => void) => {
         dispatch(state.tr.setSelection(TextSelection.create(state.doc, state.selection.from, state.selection.from)));
         (document.activeElement as any).blur?.();
-        SelectionManager.DeselectAll();
+        DocumentView.DeselectAll();
     });
 
     bind('Alt-Enter', () => onKey() || true);
diff --git a/src/client/views/nodes/formattedText/RichTextMenu.tsx b/src/client/views/nodes/formattedText/RichTextMenu.tsx
index 6c12b9991..a612f3c65 100644
--- a/src/client/views/nodes/formattedText/RichTextMenu.tsx
+++ b/src/client/views/nodes/formattedText/RichTextMenu.tsx
@@ -11,11 +11,10 @@ import * as React from 'react';
 import { Doc } from '../../../../fields/Doc';
 import { BoolCast, Cast, StrCast } from '../../../../fields/Types';
 import { DocServer } from '../../../DocServer';
-import { LinkManager } from '../../../util/LinkManager';
-import { SelectionManager } from '../../../util/SelectionManager';
 import { undoBatch, UndoManager } from '../../../util/UndoManager';
 import { AntimodeMenu, AntimodeMenuProps } from '../../AntimodeMenu';
 import { ObservableReactComponent } from '../../ObservableReactComponent';
+import { DocumentView } from '../DocumentView';
 import { EquationBox } from '../EquationBox';
 import { FieldViewProps } from '../FieldView';
 import { FormattedTextBox } from './FormattedTextBox';
@@ -116,7 +115,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
     _disposer: IReactionDisposer | undefined;
     componentDidMount() {
         this._disposer = reaction(
-            () => SelectionManager.Views.slice(),
+            () => DocumentView.Selected().slice(),
             () => this.updateMenu(undefined, undefined, undefined, undefined)
         );
     }
@@ -144,8 +143,8 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
         const { activeSizes } = active;
         const { activeColors } = active;
         const { activeHighlights } = active;
-        const refDoc = SelectionManager.Views.lastElement()?.layoutDoc ?? Doc.UserDoc();
-        const refField = (pfx => (pfx ? pfx + '_' : ''))(SelectionManager.Views.lastElement()?.LayoutFieldKey);
+        const refDoc = DocumentView.Selected().lastElement()?.layoutDoc ?? Doc.UserDoc();
+        const refField = (pfx => (pfx ? pfx + '_' : ''))(DocumentView.Selected().lastElement()?.LayoutFieldKey);
         const refVal = (field: string, dflt: string) => StrCast(refDoc[refField + field], StrCast(Doc.UserDoc()[field], dflt));
 
         this._activeListType = this.getActiveListStyle();
@@ -237,8 +236,8 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
                 m.type === state.schema.marks.pFontSize && activeSizes.add(m.attrs.fontSize);
                 m.type === state.schema.marks.pFontHighlight && activeHighlights.add(String(m.attrs.fontHighlight));
             });
-        } else if (SelectionManager.Views.some(dv => dv.ComponentView instanceof EquationBox)) {
-            SelectionManager.Views.forEach(dv => StrCast(dv.Document._text_fontSize) && activeSizes.add(StrCast(dv.Document._text_fontSize)));
+        } else if (DocumentView.Selected().some(dv => dv.ComponentView instanceof EquationBox)) {
+            DocumentView.Selected().forEach(dv => StrCast(dv.Document._text_fontSize) && activeSizes.add(StrCast(dv.Document._text_fontSize)));
         }
         return { activeFamilies: Array.from(activeFamilies), activeSizes: Array.from(activeSizes), activeColors: Array.from(activeColors), activeHighlights: Array.from(activeHighlights) };
     }
@@ -566,7 +565,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
                         if (linkDoc instanceof Doc) {
                             const linkAnchor1 = await Cast(linkDoc.link_anchor_1, Doc);
                             const linkAnchor2 = await Cast(linkDoc.link_anchor_2, Doc);
-                            const currentDoc = SelectionManager.Docs.lastElement();
+                            const currentDoc = DocumentView.Selected().lastElement().Document;
                             if (currentDoc && linkAnchor1 && linkAnchor2) {
                                 if (Doc.AreProtosEqual(currentDoc, linkAnchor1)) {
                                     return StrCast(linkAnchor2.title);
@@ -605,7 +604,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
                     .filter((aref: any) => aref?.href.indexOf(Doc.localServerPath()) === 0)
                     .forEach((aref: any) => {
                         const anchorId = aref.href.replace(Doc.localServerPath(), '').split('?')[0];
-                        anchorId && DocServer.GetRefField(anchorId).then(linkDoc => LinkManager.Instance.deleteLink(linkDoc as Doc));
+                        anchorId && DocServer.GetRefField(anchorId).then(linkDoc => Doc.DeleteLink?.(linkDoc as Doc));
                     });
             }
         }
diff --git a/src/client/views/nodes/formattedText/SummaryView.tsx b/src/client/views/nodes/formattedText/SummaryView.tsx
index 2c366b49b..238267f6e 100644
--- a/src/client/views/nodes/formattedText/SummaryView.tsx
+++ b/src/client/views/nodes/formattedText/SummaryView.tsx
@@ -21,11 +21,10 @@ export class SummaryView {
     root: any;
 
     constructor(node: any, view: any, getPos: any) {
-        const self = this;
         this.dom = document.createElement('span');
         this.dom.className = this.className(node.attrs.visibility);
-        this.dom.onpointerdown = function (e: any) {
-            self.onPointerDown(e, node, view, getPos);
+        this.dom.onpointerdown = (e: any) => {
+            this.onPointerDown(e, node, view, getPos);
         };
         this.dom.onkeypress = function (e: any) {
             e.stopPropagation();
diff --git a/src/client/views/nodes/generativeFill/GenerativeFill.tsx b/src/client/views/nodes/generativeFill/GenerativeFill.tsx
index d5fad296f..91b0ebd5c 100644
--- a/src/client/views/nodes/generativeFill/GenerativeFill.tsx
+++ b/src/client/views/nodes/generativeFill/GenerativeFill.tsx
@@ -16,7 +16,6 @@ import { NumCast } from '../../../../fields/Types';
 import { Networking } from '../../../Network';
 import { DocUtils } from '../../../documents/DocUtils';
 import { Docs } from '../../../documents/Documents';
-import { DocumentManager } from '../../../util/DocumentManager';
 import { CollectionDockingView } from '../../collections/CollectionDockingView';
 import { CollectionFreeFormView } from '../../collections/collectionFreeForm';
 import { ImageEditorData } from '../ImageBox';
@@ -28,6 +27,7 @@ import { APISuccess, ImageUtility } from './generativeFillUtils/ImageHandler';
 import { PointerHandler } from './generativeFillUtils/PointerHandler';
 import { activeColor, canvasSize, eraserColor, freeformRenderSize, newCollectionSize, offsetDistanceY, offsetX } from './generativeFillUtils/generativeFillConstants';
 import { CursorData, ImageDimensions, Point } from './generativeFillUtils/generativeFillInterfaces';
+import { DocumentView } from '../DocumentView';
 
 // enum BrushStyle {
 //     ADD,
@@ -295,7 +295,7 @@ const GenerativeFill = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addD
                         _height: newCollectionSize,
                         title: 'Image edit collection',
                     });
-                    DocUtils.MakeLink(imageRootDoc, newCollectionRef.current, { link_relationship: 'Image Edit Version History', link_displayLine: false });
+                    DocUtils.MakeLink(imageRootDoc, newCollectionRef.current, { link_relationship: 'Image Edit Version History' });
 
                     // opening new tab
                     CollectionDockingView.AddSplit(newCollectionRef.current, OpenWhereMod.right);
@@ -400,7 +400,7 @@ const GenerativeFill = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addD
             parentDoc.current.gen_fill_children = new List<Doc>([newImg]);
         }
 
-        DocUtils.MakeLink(parentDoc.current, newImg, { link_relationship: `Image edit; Prompt: ${input}`, link_displayLine: true });
+        DocUtils.MakeLink(parentDoc.current, newImg, { link_relationship: `Image edit; Prompt: ${input}` });
         adjustImgPositions();
 
         if (isNewCollection && newCollectionRef.current) {
@@ -430,7 +430,7 @@ const GenerativeFill = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addD
         ImageEditorData.Open = false;
         ImageEditorData.Source = '';
         if (newCollectionRef.current) {
-            DocumentManager.Instance.AddViewRenderedCb(newCollectionRef.current, dv => (dv.ComponentView as CollectionFreeFormView)?.fitContentOnce());
+            DocumentView.addViewRenderedCb(newCollectionRef.current, dv => (dv.ComponentView as CollectionFreeFormView)?.fitContentOnce());
         }
         setEdits([]);
     };
diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx
index 485ba7367..6b4f5e073 100644
--- a/src/client/views/nodes/trails/PresBox.tsx
+++ b/src/client/views/nodes/trails/PresBox.tsx
@@ -19,17 +19,14 @@ import { emptyFunction, emptyPath, stringHash } from '../../../../Utils';
 import { DocServer } from '../../../DocServer';
 import { Docs } from '../../../documents/Documents';
 import { CollectionViewType, DocumentType } from '../../../documents/DocumentTypes';
-import { DocumentManager } from '../../../util/DocumentManager';
 import { dropActionType } from '../../../util/DropActionTypes';
 import { ScriptingGlobals } from '../../../util/ScriptingGlobals';
-import { SelectionManager } from '../../../util/SelectionManager';
 import { SerializationHelper } from '../../../util/SerializationHelper';
-import { SettingsManager } from '../../../util/SettingsManager';
 import { SnappingManager } from '../../../util/SnappingManager';
 import { undoBatch, UndoManager } from '../../../util/UndoManager';
 import { CollectionDockingView } from '../../collections/CollectionDockingView';
 import { CollectionFreeFormView } from '../../collections/collectionFreeForm';
-import { CollectionStackedTimeline } from '../../collections/CollectionStackedTimeline';
+import { CollectionFreeFormPannableContents } from '../../collections/collectionFreeForm/CollectionFreeFormPannableContents';
 import { CollectionView } from '../../collections/CollectionView';
 import { TreeView } from '../../collections/TreeView';
 import { ViewBoxBaseComponent } from '../../DocComponent';
@@ -55,8 +52,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         super(props);
         makeObservable(this);
         if (!PresBox.navigateToDocScript) {
-            PresBox.navigateToDocScript = ScriptField.MakeFunction('navigateToDoc(this.presentation_targetDoc, self)')!;
+            PresBox.navigateToDocScript = ScriptField.MakeFunction('navigateToDoc(this.presentation_targetDoc, this)')!;
         }
+        CollectionFreeFormPannableContents.SetOverlayPlugin((fform: Doc) => PresBox.Instance.pathLines(fform));
     }
 
     private _disposers: { [name: string]: IReactionDisposer } = {};
@@ -122,8 +120,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         return false;
     }
     @computed get selectedDocumentView() {
-        if (SelectionManager.Views.length) return SelectionManager.Views[0];
-        if (this.selectedArray.size) return DocumentManager.Instance.getDocumentView(this.Document);
+        if (DocumentView.Selected().length) return DocumentView.Selected()[0];
+        if (this.selectedArray.size) return DocumentView.getDocumentView(this.Document);
         return undefined;
     }
     @computed get isPres() {
@@ -175,7 +173,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         this._unmounting = false;
         this.turnOffEdit(true);
         this._disposers.selection = reaction(
-            () => SelectionManager.Views.slice(),
+            () => DocumentView.Selected().slice(),
             views => (!PresBox.Instance || views.some(view => view.Document === this.Document)) && this.updateCurrentPresentation(),
             { fireImmediately: true }
         );
@@ -200,7 +198,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
     startTempMedia = (targetDoc: Doc, activeItem: Doc) => {
         const duration: number = NumCast(activeItem.config_clipEnd) - NumCast(activeItem.config_clipStart);
         if ([DocumentType.VID, DocumentType.AUDIO].includes(targetDoc.type as any)) {
-            const targMedia = DocumentManager.Instance.getDocumentView(targetDoc);
+            const targMedia = DocumentView.getDocumentView(targetDoc);
             targMedia?.ComponentView?.playFrom?.(NumCast(activeItem.config_clipStart), NumCast(activeItem.config_clipStart) + duration);
         }
     };
@@ -208,7 +206,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
     stopTempMedia = (targetDocField: FieldResult) => {
         const targetDoc = DocCast(DocCast(targetDocField).annotationOn) ?? DocCast(targetDocField);
         if ([DocumentType.VID, DocumentType.AUDIO].includes(targetDoc.type as any)) {
-            const targMedia = DocumentManager.Instance.getDocumentView(targetDoc);
+            const targMedia = DocumentView.getDocumentView(targetDoc);
             targMedia?.ComponentView?.Pause?.();
         }
     };
@@ -260,7 +258,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
     // go to documents chain
     runSubroutines = (childrenToRun: Opt<Doc[]>, normallyNextSlide: Doc) => {
         if (childrenToRun && childrenToRun[0] !== normallyNextSlide) {
-            childrenToRun.forEach(child => DocumentManager.Instance.showDocument(child, {}));
+            childrenToRun.forEach(child => DocumentView.showDocument(child, {}));
         }
     };
 
@@ -280,7 +278,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                 if (listItems && presIndexed < listItems.length) {
                     if (!first) {
                         const listItemDoc = listItems[presIndexed];
-                        const targetView = listItems && DocumentManager.Instance.getFirstDocumentView(listItemDoc);
+                        const targetView = listItems && DocumentView.getFirstDocumentView(listItemDoc);
                         Doc.linkFollowUnhighlight();
                         Doc.HighlightDoc(listItemDoc);
                         listItemDoc.presentation_effect = this.activeItem.presBulletEffect;
@@ -425,7 +423,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
             const acontext = activeItem.config_activeFrame !== undefined ? DocCast(DocCast(activeItem.presentation_targetDoc).embedContainer) : DocCast(activeItem.presentation_targetDoc);
             const context = DocCast(acontext)?.annotationOn ? DocCast(DocCast(acontext).annotationOn) : acontext;
             if (context) {
-                const ffview = CollectionFreeFormView.from(DocumentManager.Instance.getFirstDocumentView(context));
+                const ffview = CollectionFreeFormView.from(DocumentView.getFirstDocumentView(context));
                 if (ffview?.childDocs) {
                     PresBox.Instance._keyTimer = CollectionFreeFormView.gotoKeyframe(PresBox.Instance._keyTimer, ffview.childDocs, frameTime);
                     ffview.layoutDoc._currentFrame = NumCast(activeFrame);
@@ -605,7 +603,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                 const viewport = { panX: (contentBounds[0] + contentBounds[2]) / 2, panY: (contentBounds[1] + contentBounds[3]) / 2, width: contentBounds[2] - contentBounds[0], height: contentBounds[3] - contentBounds[1] };
                 bestTarget._freeform_panX = viewport.panX;
                 bestTarget._freeform_panY = viewport.panY;
-                const dv = DocumentManager.Instance.getDocumentView(bestTarget);
+                const dv = DocumentView.getDocumentView(bestTarget);
                 if (dv) {
                     changed = true;
                     const computedScale = NumCast(activeItem.config_zoom, 1) * Math.min(dv._props.PanelWidth() / viewport.width, dv._props.PanelHeight() / viewport.height);
@@ -644,8 +642,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         const eleViewCache = Array.from(this._eleArray);
         const resetSelection = action(() => {
             if (!this._props.isSelected()) {
-                const presDocView = DocumentManager.Instance.getDocumentView(this.Document);
-                if (presDocView) SelectionManager.SelectView(presDocView, false);
+                const presDocView = DocumentView.getDocumentView(this.Document);
+                if (presDocView) DocumentView.SelectView(presDocView, false);
                 this.clearSelectedArray();
                 selViewCache.forEach(doc => this.addToSelectedArray(doc));
                 this._dragArray.splice(0, this._dragArray.length, ...dragViewCache);
@@ -660,7 +658,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
 
     static NavigateToTarget(targetDoc: Doc, activeItem: Doc, finished?: () => void) {
         if (activeItem.presentation_movement === PresMovement.None && targetDoc.type === DocumentType.SCRIPTING) {
-            (DocumentManager.Instance.getFirstDocumentView(targetDoc)?.ComponentView as ScriptingBox)?.onRun?.();
+            (DocumentView.getFirstDocumentView(targetDoc)?.ComponentView as ScriptingBox)?.onRun?.();
             return;
         }
         const effect = activeItem.presentation_effect && activeItem.presentation_effect !== PresEffect.None ? activeItem.presentation_effect : undefined;
@@ -680,19 +678,19 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         };
         if (activeItem.presentation_openInLightbox) {
             const context = DocCast(targetDoc.annotationOn) ?? targetDoc;
-            if (!DocumentManager.Instance.getLightboxDocumentView(context)) {
+            if (!DocumentView.getLightboxDocumentView(context)) {
                 LightboxView.Instance.SetLightboxDoc(context);
             }
         }
         if (targetDoc) {
             if (activeItem.presentation_targetDoc instanceof Doc) activeItem.presentation_targetDoc[Animation] = undefined;
 
-            DocumentManager.Instance.AddViewRenderedCb(LightboxView.LightboxDoc, () => {
+            DocumentView.addViewRenderedCb(LightboxView.LightboxDoc, () => {
                 // if target or the doc it annotates is not in the lightbox, then close the lightbox
-                if (!DocumentManager.Instance.getLightboxDocumentView(DocCast(targetDoc.annotationOn) ?? targetDoc)) {
+                if (!DocumentView.getLightboxDocumentView(DocCast(targetDoc.annotationOn) ?? targetDoc)) {
                     LightboxView.Instance.SetLightboxDoc(undefined);
                 }
-                DocumentManager.Instance.showDocument(targetDoc, options, finished);
+                DocumentView.showDocument(targetDoc, options, finished);
             });
         } else finished?.();
     }
@@ -1032,8 +1030,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
 
     @action
     selectPres = () => {
-        const presDocView = DocumentManager.Instance.getDocumentView(this.Document);
-        presDocView && SelectionManager.SelectView(presDocView, false);
+        const presDocView = DocumentView.getDocumentView(this.Document);
+        presDocView && DocumentView.SelectView(presDocView, false);
     };
 
     focusElement = (doc: Doc) => {
@@ -1044,7 +1042,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
     // Regular click
     @action
     selectElement = (doc: Doc, noNav = false) => {
-        CollectionStackedTimeline.CurrentlyPlaying?.map(clip => clip?.ComponentView?.Pause?.());
+        DocumentView.CurrentlyPlaying?.map(clip => clip?.ComponentView?.Pause?.());
         if (noNav) {
             const index = this.childDocs.indexOf(doc);
             if (index >= 0 && index < this.childDocs.length) {
@@ -1200,7 +1198,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         const order: JSX.Element[] = [];
         const docs = new Set<Doc>();
         const presCollection = collection;
-        const dv = DocumentManager.Instance.getDocumentView(presCollection);
+        const dv = DocumentView.getDocumentView(presCollection);
         this.childDocs.forEach((doc, index) => {
             const tagDoc = PresBox.targetRenderedDoc(doc);
             const srcContext = Cast(tagDoc.embedContainer, Doc, null);
@@ -2206,7 +2204,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         if (doc) {
             const tabMap = CollectionDockingView.Instance?.tabMap;
             const docTab = tabMap && Array.from(tabMap).find(tab => tab.DashDoc.type === DocumentType.COL)?.DashDoc;
-            const presCollection = DocumentManager.GetContextPath(this.activeItem).reverse().lastElement().presentation_targetDoc ?? docTab;
+            const presCollection = DocumentView.getContextPath(this.activeItem).reverse().lastElement().presentation_targetDoc ?? docTab;
             const data = Cast(presCollection?.data, listSpec(Doc));
             const configData = Cast(this.Document.data, listSpec(Doc));
             if (data && configData) {
@@ -2282,12 +2280,12 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
 
     @action
     toggleProperties = () => {
-        SettingsManager.Instance.propertiesWidth = SettingsManager.Instance.propertiesWidth > 0 ? 0 : 250;
+        SnappingManager.SetPropertiesWidth(SnappingManager.PropertiesWidth > 0 ? 0 : 250);
     };
 
     @computed get toolbar() {
-        const propIcon = SettingsManager.Instance.propertiesWidth > 0 ? 'angle-double-right' : 'angle-double-left';
-        const propTitle = SettingsManager.Instance.propertiesWidth > 0 ? 'Close Presentation Panel' : 'Open Presentation Panel';
+        const propIcon = SnappingManager.PropertiesWidth > 0 ? 'angle-double-right' : 'angle-double-left';
+        const propTitle = SnappingManager.PropertiesWidth > 0 ? 'Close Presentation Panel' : 'Open Presentation Panel';
         const mode = StrCast(this.Document._type_collection) as CollectionViewType;
         const isMini: boolean = this.toolbarWidth <= 100;
         const activeColor = SnappingManager.userVariantColor;
@@ -2316,7 +2314,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                         </Tooltip>
                         <Tooltip title={<div className="dash-tooltip">{propTitle}</div>}>
                             <div className="toolbar-button" style={{ position: 'absolute', right: 4, fontSize: 16 }} onClick={this.toggleProperties}>
-                                <FontAwesomeIcon className="toolbar-thumbtack" icon={propIcon} style={{ color: SettingsManager.Instance.propertiesWidth > 0 ? activeColor : inactiveColor }} />
+                                <FontAwesomeIcon className="toolbar-thumbtack" icon={propIcon} style={{ color: SnappingManager.PropertiesWidth > 0 ? activeColor : inactiveColor }} />
                             </div>
                         </Tooltip>
                     </>
diff --git a/src/client/views/nodes/trails/PresElementBox.tsx b/src/client/views/nodes/trails/PresElementBox.tsx
index af0ab3b53..306b98190 100644
--- a/src/client/views/nodes/trails/PresElementBox.tsx
+++ b/src/client/views/nodes/trails/PresElementBox.tsx
@@ -13,9 +13,8 @@ import { BoolCast, Cast, DocCast, NumCast, StrCast } from '../../../../fields/Ty
 import { emptyFunction } from '../../../../Utils';
 import { Docs } from '../../../documents/Documents';
 import { CollectionViewType, DocumentType } from '../../../documents/DocumentTypes';
-import { DocumentManager } from '../../../util/DocumentManager';
 import { DragManager } from '../../../util/DragManager';
-import { SettingsManager } from '../../../util/SettingsManager';
+import { SnappingManager } from '../../../util/SnappingManager';
 import { Transform } from '../../../util/Transform';
 import { undoable, undoBatch } from '../../../util/UndoManager';
 import { TreeView } from '../../collections/TreeView';
@@ -23,7 +22,7 @@ import { ViewBoxBaseComponent } from '../../DocComponent';
 import { EditableView } from '../../EditableView';
 import { Colors } from '../../global/globalEnums';
 import { PinDocView } from '../../PinFuncs';
-import { StyleProp } from '../../StyleProvider';
+import { StyleProp } from '../../StyleProp';
 import { DocumentView } from '../DocumentView';
 import { FieldView, FieldViewProps } from '../FieldView';
 import { PresBox } from './PresBox';
@@ -282,8 +281,8 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
 
     @action
     toggleProperties = () => {
-        if (SettingsManager.Instance.propertiesWidth < 5) {
-            SettingsManager.Instance.propertiesWidth = 250;
+        if (SnappingManager.PropertiesWidth < 5) {
+            SnappingManager.SetPropertiesWidth(250);
         }
     };
 
@@ -417,7 +416,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
 
     @computed
     get toolbarWidth(): number {
-        const presBoxDocView = DocumentManager.Instance.getDocumentView(this.presBox);
+        const presBoxDocView = DocumentView.getDocumentView(this.presBox);
         const width = NumCast(this.presBox?._width);
         return presBoxDocView ? presBoxDocView._props.PanelWidth() : width || 300;
     }
diff --git a/src/client/views/pdf/AnchorMenu.tsx b/src/client/views/pdf/AnchorMenu.tsx
index f02c56471..a837969aa 100644
--- a/src/client/views/pdf/AnchorMenu.tsx
+++ b/src/client/views/pdf/AnchorMenu.tsx
@@ -8,12 +8,12 @@ import { ClientUtils, returnFalse, setupMoveUpEvents } from '../../../ClientUtil
 import { emptyFunction, unimplementedFunction } from '../../../Utils';
 import { Doc, Opt } from '../../../fields/Doc';
 import { DocumentType } from '../../documents/DocumentTypes';
-import { SelectionManager } from '../../util/SelectionManager';
 import { SettingsManager } from '../../util/SettingsManager';
 import { AntimodeMenu, AntimodeMenuProps } from '../AntimodeMenu';
 import { LinkPopup } from '../linking/LinkPopup';
 import './AnchorMenu.scss';
 import { GPTPopup } from './GPTPopup/GPTPopup';
+import { DocumentView } from '../nodes/DocumentView';
 
 @observer
 export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
@@ -66,7 +66,7 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
 
     componentDidMount() {
         this._disposer = reaction(
-            () => SelectionManager.Views.slice(),
+            () => DocumentView.Selected().slice(),
             () => AnchorMenu.Instance.fadeOut(true)
         );
     }
@@ -145,7 +145,7 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
      * all selected text available to summarize but its only supported for pdf and web ATM.
      * @returns Whether the GPT icon for summarization should appear
      */
-    canSummarize = () => SelectionManager.Docs.some(doc => [DocumentType.PDF, DocumentType.WEB].includes(doc.type as any));
+    canSummarize = () => DocumentView.SelectedDocs().some(doc => [DocumentType.PDF, DocumentType.WEB].includes(doc.type as any));
 
     render() {
         const buttons =
diff --git a/src/client/views/pdf/Annotation.tsx b/src/client/views/pdf/Annotation.tsx
index 8b2b179d3..7dd4047c1 100644
--- a/src/client/views/pdf/Annotation.tsx
+++ b/src/client/views/pdf/Annotation.tsx
@@ -5,10 +5,10 @@ import { Doc, DocListCast, Opt, StrListCast } from '../../../fields/Doc';
 import { Highlight } from '../../../fields/DocSymbols';
 import { List } from '../../../fields/List';
 import { BoolCast, DocCast, NumCast, StrCast } from '../../../fields/Types';
-import { LinkFollower } from '../../util/LinkFollower';
 import { LinkManager } from '../../util/LinkManager';
 import { undoable } from '../../util/UndoManager';
 import { ObservableReactComponent } from '../ObservableReactComponent';
+import { DocumentView } from '../nodes/DocumentView';
 import { FieldViewProps } from '../nodes/FieldView';
 import { OpenWhere } from '../nodes/OpenWhere';
 import { AnchorMenu } from './AnchorMenu';
@@ -56,7 +56,7 @@ export class Annotation extends ObservableReactComponent<IAnnotationProps> {
 
     @computed get linkHighlighted() {
         const found = LinkManager.Instance.getAllDirectLinks(this._props.annoDoc).find(link => {
-            const a1 = LinkManager.getOppositeAnchor(link, this._props.annoDoc);
+            const a1 = Doc.getOppositeAnchor(link, this._props.annoDoc);
             return a1 && Doc.GetBrushStatus(DocCast(a1.annotationOn, a1));
         });
         return found;
@@ -103,7 +103,7 @@ export class Annotation extends ObservableReactComponent<IAnnotationProps> {
             e.preventDefault();
         } else if (e.button === 0) {
             e.stopPropagation();
-            LinkFollower.FollowLink(undefined, this._props.annoDoc, false);
+            DocumentView.FollowLink(undefined, this._props.annoDoc, false);
         }
     };
     brushed = () => this._props.annoDoc && Doc.GetBrushHighlightStatus(this._props.annoDoc);
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index 15ac73f78..45b1d727e 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -15,17 +15,17 @@ import { Cast, NumCast, StrCast } from '../../../fields/Types';
 import { TraceMobx } from '../../../fields/util';
 import { emptyFunction } from '../../../Utils';
 import { DocUtils } from '../../documents/DocUtils';
-import { SelectionManager } from '../../util/SelectionManager';
 import { SnappingManager } from '../../util/SnappingManager';
 import { MarqueeOptionsMenu } from '../collections/collectionFreeForm';
 import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView';
 import { MarqueeAnnotator } from '../MarqueeAnnotator';
+import { DocumentView } from '../nodes/DocumentView';
 import { FieldViewProps } from '../nodes/FieldView';
 import { FocusViewOptions } from '../nodes/FocusViewOptions';
 import { LinkInfo } from '../nodes/LinkDocPreview';
 import { PDFBox } from '../nodes/PDFBox';
 import { ObservableReactComponent } from '../ObservableReactComponent';
-import { StyleProp } from '../StyleProvider';
+import { StyleProp } from '../StyleProp';
 import { AnchorMenu } from './AnchorMenu';
 import { Annotation } from './Annotation';
 import { GPTPopup } from './GPTPopup/GPTPopup';
@@ -122,7 +122,7 @@ export class PDFViewer extends ObservableReactComponent<IViewerProps> {
 
         this._disposers.selected = reaction(
             () => this._props.isSelected(),
-            () => SelectionManager.Views.length === 1 && this.setupPdfJsViewer(),
+            () => DocumentView.Selected().length === 1 && this.setupPdfJsViewer(),
             { fireImmediately: true }
         );
         this._disposers.curPage = reaction(
diff --git a/src/client/views/search/SearchBox.tsx b/src/client/views/search/SearchBox.tsx
index 5df934231..56552c952 100644
--- a/src/client/views/search/SearchBox.tsx
+++ b/src/client/views/search/SearchBox.tsx
@@ -12,16 +12,15 @@ import { DocCast, StrCast } from '../../../fields/Types';
 import { DocUtils } from '../../documents/DocUtils';
 import { DocumentType } from '../../documents/DocumentTypes';
 import { Docs } from '../../documents/Documents';
-import { DocumentManager } from '../../util/DocumentManager';
-import { LinkManager } from '../../util/LinkManager';
 import { SearchUtil } from '../../util/SearchUtil';
-import { SettingsManager } from '../../util/SettingsManager';
+import { SnappingManager } from '../../util/SnappingManager';
 import { undoBatch } from '../../util/UndoManager';
 import { ViewBoxBaseComponent } from '../DocComponent';
 import { ObservableReactComponent } from '../ObservableReactComponent';
 import { CollectionDockingView } from '../collections/CollectionDockingView';
 import { IRecommendation, Recommendation } from '../newlightbox/components';
 import { fetchRecommendations } from '../newlightbox/utils';
+import { DocumentView } from '../nodes/DocumentView';
 import { FieldView, FieldViewProps } from '../nodes/FieldView';
 import './SearchBox.scss';
 
@@ -54,7 +53,7 @@ export class SearchBoxItem extends ObservableReactComponent<SearchBoxItemProps>
      * or opening it in a new tab.
      */
     selectElement = async (doc: Doc, finishFunc: () => void) => {
-        await DocumentManager.Instance.showDocument(doc, { willZoomCentered: true }, finishFunc);
+        await DocumentView.showDocument(doc, { willZoomCentered: true }, finishFunc);
     };
 
     /**
@@ -65,12 +64,12 @@ export class SearchBoxItem extends ObservableReactComponent<SearchBoxItemProps>
      */
     onResultClick = action(async (doc: Doc) => {
         this._props.selectItem(doc);
-        this.selectElement(doc, () => DocumentManager.Instance.getFirstDocumentView(doc)?.ComponentView?.search?.(this._props.searchString, undefined, false));
+        this.selectElement(doc, () => DocumentView.getFirstDocumentView(doc)?.ComponentView?.search?.(this._props.searchString, undefined, false));
     });
 
     componentWillUnmount(): void {
         const doc = this._props.Document;
-        DocumentManager.Instance.getFirstDocumentView(doc)?.ComponentView?.search?.('', undefined, true);
+        DocumentView.getFirstDocumentView(doc)?.ComponentView?.search?.('', undefined, true);
     }
 
     @undoBatch
@@ -99,20 +98,18 @@ export class SearchBoxItem extends ObservableReactComponent<SearchBoxItemProps>
                               }
                     }
                     style={{
-                        fontWeight: LinkManager.Links(this._props.linkFrom).find(
-                            link =>
-                                Doc.AreProtosEqual(LinkManager.getOppositeAnchor(link, this._props.linkFrom!), this._props.Document) ||
-                                Doc.AreProtosEqual(DocCast(LinkManager.getOppositeAnchor(link, this._props.linkFrom!)?.annotationOn), this._props.Document)
+                        fontWeight: Doc.Links(this._props.linkFrom).find(
+                            link => Doc.AreProtosEqual(Doc.getOppositeAnchor(link, this._props.linkFrom!), this._props.Document) || Doc.AreProtosEqual(DocCast(Doc.getOppositeAnchor(link, this._props.linkFrom!)?.annotationOn), this._props.Document)
                         )
                             ? 'bold'
                             : '',
                     }}
                     className={this._props.className}>
                     <div className="searchBox-result-title">{title as string}</div>
-                    <div className="searchBox-result-type" style={{ color: SettingsManager.userVariantColor }}>
+                    <div className="searchBox-result-type" style={{ color: SnappingManager.userVariantColor }}>
                         {formattedType}
                     </div>
-                    <div className="searchBox-result-keys" style={{ color: SettingsManager.userVariantColor }}>
+                    <div className="searchBox-result-keys" style={{ color: SnappingManager.userVariantColor }}>
                         {this._props.matchedKeys.join(', ')}
                     </div>
                 </div>
@@ -373,7 +370,7 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() {
 
         const query = StrCast(this._searchString);
         Doc.SetSearchQuery(query);
-        if (!this._props.linkSearch) Array.from(this._results.keys()).forEach(doc => DocumentManager.Instance.getFirstDocumentView(doc)?.ComponentView?.search?.(this._searchString, undefined, true));
+        if (!this._props.linkSearch) Array.from(this._results.keys()).forEach(doc => DocumentView.getFirstDocumentView(doc)?.ComponentView?.search?.(this._searchString, undefined, true));
         this._results.clear();
 
         if (query) {
@@ -412,7 +409,7 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() {
         this._timeout && clearTimeout(this._timeout);
         this._timeout = undefined;
         this._results.forEach((_, doc) => {
-            DocumentManager.Instance.getFirstDocumentView(doc)?.ComponentView?.search?.('', undefined, true);
+            DocumentView.getFirstDocumentView(doc)?.ComponentView?.search?.('', undefined, true);
             Doc.UnBrushDoc(doc);
             Doc.UnHighlightDoc(doc);
             Doc.ClearSearchMatches();
@@ -476,7 +473,7 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() {
         const recommendationsJSX: JSX.Element[] = this._recommendations.map(props => <Recommendation {...props} />);
 
         return (
-            <div className="searchBox-container" style={{ pointerEvents: 'all', color: SettingsManager.userColor, background: SettingsManager.userBackgroundColor }}>
+            <div className="searchBox-container" style={{ pointerEvents: 'all', color: SnappingManager.userColor, background: SnappingManager.userBackgroundColor }}>
                 <div className="searchBox-bar">
                     {isLinkSearch ? null : (
                         <select name="type" id="searchBox-type" className="searchBox-type" onChange={this.onSelectChange}>
@@ -501,7 +498,7 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() {
                 </div>
                 {resultsJSX.length > 0 && (
                     <div className="searchBox-results-container">
-                        <div className="section-header" style={{ background: SettingsManager.userVariantColor }}>
+                        <div className="section-header" style={{ background: SnappingManager.userVariantColor }}>
                             <div className="section-title">Results</div>
                             <div className="section-subtitle">{`${validResults} result` + (validResults === 1 ? '' : 's')}</div>
                         </div>
@@ -510,7 +507,7 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() {
                 )}
                 {recommendationsJSX.length > 0 && (
                     <div className="searchBox-recommendations-container">
-                        <div className="section-header" style={{ background: SettingsManager.userVariantColor }}>
+                        <div className="section-header" style={{ background: SnappingManager.userVariantColor }}>
                             <div className="section-title">Recommendations</div>
                             <div className="section-subtitle">{`${validResults} result` + (validResults === 1 ? '' : 's')}</div>
                         </div>
diff --git a/src/client/views/selectedDoc/SelectedDocView.tsx b/src/client/views/selectedDoc/SelectedDocView.tsx
index 6ed3b240d..78a1a92f7 100644
--- a/src/client/views/selectedDoc/SelectedDocView.tsx
+++ b/src/client/views/selectedDoc/SelectedDocView.tsx
@@ -6,8 +6,8 @@ import * as React from 'react';
 import { emptyFunction } from '../../../Utils';
 import { Doc } from '../../../fields/Doc';
 import { StrCast } from '../../../fields/Types';
-import { DocumentManager } from '../../util/DocumentManager';
-import { SettingsManager } from '../../util/SettingsManager';
+import { SnappingManager } from '../../util/SnappingManager';
+import { DocumentView } from '../nodes/DocumentView';
 import { FocusViewOptions } from '../nodes/FocusViewOptions';
 
 export interface SelectedDocViewProps {
@@ -33,14 +33,14 @@ export class SelectedDocView extends React.Component<SelectedDocViewProps> {
                         return {
                             text: StrCast(doc.title),
                             val: StrCast(doc._id),
-                            color: SettingsManager.userColor,
-                            background: SettingsManager.userBackgroundColor,
+                            color: SnappingManager.userColor,
+                            background: SnappingManager.userBackgroundColor,
                             icon: <FontAwesomeIcon size="1x" icon={Doc.toIcon(doc)} />,
-                            onClick: () => DocumentManager.Instance.showDocument(doc, options, emptyFunction),
+                            onClick: () => DocumentView.showDocument(doc, options, emptyFunction),
                         };
                     })}
-                    color={SettingsManager.userColor}
-                    background={SettingsManager.userBackgroundColor}
+                    color={SnappingManager.userColor}
+                    background={SnappingManager.userBackgroundColor}
                 />
             </div>
         );
diff --git a/src/client/views/topbar/TopBar.tsx b/src/client/views/topbar/TopBar.tsx
index f82ae34c5..9f97efcce 100644
--- a/src/client/views/topbar/TopBar.tsx
+++ b/src/client/views/topbar/TopBar.tsx
@@ -11,8 +11,6 @@ import { AclAdmin, DashVersion } from '../../../fields/DocSymbols';
 import { StrCast } from '../../../fields/Types';
 import { GetEffectiveAcl } from '../../../fields/util';
 import { emptyFunction } from '../../../Utils';
-import { CurrentUserUtils } from '../../util/CurrentUserUtils';
-import { DocumentManager } from '../../util/DocumentManager';
 import { dropActionType } from '../../util/DropActionTypes';
 import { PingManager } from '../../util/PingManager';
 import { ReportManager } from '../../util/reportManager/ReportManager';
@@ -25,7 +23,7 @@ import { CollectionDockingView } from '../collections/CollectionDockingView';
 import { CollectionLinearView } from '../collections/collectionLinear';
 import { DashboardView } from '../DashboardView';
 import { Colors } from '../global/globalEnums';
-import { DocumentViewInternal, returnEmptyDocViewList } from '../nodes/DocumentView';
+import { DocumentView, DocumentViewInternal, returnEmptyDocViewList } from '../nodes/DocumentView';
 import { ObservableReactComponent } from '../ObservableReactComponent';
 import { DefaultStyleProvider } from '../StyleProvider';
 import './TopBar.scss';
@@ -170,7 +168,7 @@ export class TopBar extends ObservableReactComponent<{}> {
                     color={this.color}
                     style={{ fontWeight: 700, fontSize: '1rem' }}
                     onClick={(e: React.MouseEvent) => {
-                        const dashView = Doc.ActiveDashboard && DocumentManager.Instance.getDocumentView(Doc.ActiveDashboard);
+                        const dashView = Doc.ActiveDashboard && DocumentView.getDocumentView(Doc.ActiveDashboard);
                         dashView?.showContextMenu(e.clientX + 20, e.clientY + 30);
                     }}
                 />
@@ -184,7 +182,7 @@ export class TopBar extends ObservableReactComponent<{}> {
      * and allows the user to access their account settings etc.
      */
     @computed get topbarRight() {
-        const upToDate = DashVersion === CurrentUserUtils.ServerVersion;
+        const upToDate = DashVersion === SnappingManager.ServerVersion;
         return (
             <div className="topbar-right">
                 {Doc.ActiveDashboard ? (
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index 5fd053eef..bb6995398 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -224,8 +224,25 @@ export class Doc extends RefField {
     @observable public static GuestDashboard: Doc | undefined = undefined;
     @observable public static GuestTarget: Doc | undefined = undefined;
     @observable public static GuestMobile: Doc | undefined = undefined;
+    @observable.shallow public static CurrentlyLoading: Doc[] = observable([]);
+    // removes from currently loading display
+    public static removeCurrentlyLoading(doc: Doc) {
+        if (Doc.CurrentlyLoading) {
+            const index = Doc.CurrentlyLoading.indexOf(doc);
+            runInAction(() => index !== -1 && Doc.CurrentlyLoading.splice(index, 1));
+        }
+    }
+    // adds doc to currently loading display
+    public static addCurrentlyLoading(doc: Doc) {
+        if (Doc.CurrentlyLoading.indexOf(doc) === -1) {
+            runInAction(() => Doc.CurrentlyLoading.push(doc));
+        }
+    }
 
-    public static AddLink: undefined | ((link: Doc, checkExists?: boolean) => void);
+    public static AddLink: (link: Doc, checkExists?: boolean) => void;
+    public static DeleteLink: (link: Doc) => void;
+    public static Links: (link: Doc | undefined) => Doc[];
+    public static getOppositeAnchor: (linkDoc: Doc, anchor: Doc) => Doc | undefined;
 
     public static get MySharedDocs()          { return DocCast(Doc.UserDoc().mySharedDocs); } // prettier-ignore
     public static get MyUserDocView()         { return DocCast(Doc.UserDoc().myUserDocView); } // prettier-ignore
diff --git a/src/fields/ScriptField.ts b/src/fields/ScriptField.ts
index 8a3787768..8fe365ac2 100644
--- a/src/fields/ScriptField.ts
+++ b/src/fields/ScriptField.ts
@@ -142,7 +142,6 @@ export class ScriptField extends ObjectField {
         return CompileScript(script, {
             params: {
                 this: Doc?.name || 'Doc', // this is the doc that executes the script
-                self: Doc?.name || 'Doc', // self is the root doc of the doc that executes the script
                 documentView: 'any',
                 _last_: 'any', // _last_ is the previous value of a computed field when it is being triggered to re-run.
                 _setCacheResult_: 'any', // set the cached value of the function
diff --git a/src/fields/documentSchemas.ts b/src/fields/documentSchemas.ts
index c73689e1f..335683270 100644
--- a/src/fields/documentSchemas.ts
+++ b/src/fields/documentSchemas.ts
@@ -86,7 +86,6 @@ export const documentSchema = createSchema({
     followLinkLocation: 'string', // flag for where to place content when following a click interaction (e.g., add:right, lightbox, default, )
     hideLinkButton: 'boolean', // whether the blue link counter button should be hidden
     layout_hideAllLinks: 'boolean', // whether all individual blue anchor dots should be hidden
-    link_displayLine: 'boolean', // whether a link connection should be shown between link anchor endpoints.
     isLightbox: 'boolean', // whether the marked object will display addDocTab() calls that target "lightbox" destinations
     layers: listSpec('string'), // which layers the document is part of
     _lockedPosition: 'boolean', // whether the document can be moved (dragged)
diff --git a/src/mobile/MobileInterface.tsx b/src/mobile/MobileInterface.tsx
index 55a85ed1b..7e67690d3 100644
--- a/src/mobile/MobileInterface.tsx
+++ b/src/mobile/MobileInterface.tsx
@@ -92,7 +92,6 @@ import {
     faTrash,
     faTrashAlt,
     faTree,
-    faRoute,
     faTv,
     faUndoAlt,
     faVideo,
@@ -104,7 +103,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { action, computed, observable, runInAction } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
-import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue } from '../ClientUtils';
+import { returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue } from '../ClientUtils';
 import { CollectionViewType, DocumentType } from '../client/documents/DocumentTypes';
 import { Docs, DocumentOptions } from '../client/documents/Documents';
 import { CurrentUserUtils } from '../client/util/CurrentUserUtils';
@@ -112,8 +111,8 @@ import { ScriptingGlobals } from '../client/util/ScriptingGlobals';
 import { SettingsManager } from '../client/util/SettingsManager';
 import { Transform } from '../client/util/Transform';
 import { UndoManager } from '../client/util/UndoManager';
+import { DashboardView } from '../client/views/DashboardView';
 import { GestureOverlay } from '../client/views/GestureOverlay';
-import { TabDocView } from '../client/views/collections/TabDocView';
 import { AudioBox } from '../client/views/nodes/AudioBox';
 import { DocumentView } from '../client/views/nodes/DocumentView';
 import { RadialMenu } from '../client/views/nodes/RadialMenu';
@@ -127,7 +126,7 @@ import './AudioUpload.scss';
 import { Uploader } from './ImageUpload';
 import './ImageUpload.scss';
 import './MobileInterface.scss';
-import { DashboardView } from '../client/views/DashboardView';
+import { emptyFunction } from '../Utils';
 
 library.add(
     ...[
@@ -691,7 +690,7 @@ export class MobileInterface extends React.Component {
         // Only making button available if it is an image
         if (!(this._activeDoc.type === 'collection' || this._activeDoc.type === 'presentation')) {
             return (
-                <div className="docButton" title={'Pin to presentation'} style={{ backgroundColor: 'white', color: 'black' }} onClick={e => TabDocView.PinDoc(this._activeDoc, {})}>
+                <div className="docButton" title={'Pin to presentation'} style={{ backgroundColor: 'white', color: 'black' }} onClick={e => DocumentView.PinDoc(this._activeDoc, {})}>
                     <FontAwesomeIcon className="documentdecorations-icon" size="sm" icon="map-pin" />
                 </div>
             );
diff --git a/src/server/SharedMediaTypes.ts b/src/server/SharedMediaTypes.ts
index 0a961f570..8ae13454e 100644
--- a/src/server/SharedMediaTypes.ts
+++ b/src/server/SharedMediaTypes.ts
@@ -13,6 +13,11 @@ export namespace AcceptableMedia {
     export const audioFormats = ['.wav', '.mp3', '.mpeg', '.flac', '.au', '.aiff', '.m4a', '.webm'];
 }
 
+export enum AudioAnnoState {
+    stopped = 'stopped',
+    playing = 'playing',
+}
+
 export namespace Upload {
     export function isImageInformation(uploadResponse: Upload.FileInformation): uploadResponse is Upload.ImageInformation {
         return 'nativeWidth' in uploadResponse;
-- 
cgit v1.2.3-70-g09d2


From 9b424c94d7a89950e9cf3f72e684bd15a61e87ae Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Thu, 2 May 2024 11:19:37 -0400
Subject: another push to remove cycles by pushing things onto Doc  and
 DocumentView

---
 src/client/DocServer.ts                            | 16 +++-----
 src/client/util/SharingManager.tsx                 |  1 +
 src/client/views/DashboardView.tsx                 |  6 +--
 src/client/views/DocComponent.tsx                  | 20 ++++------
 src/client/views/DocumentDecorations.tsx           |  3 +-
 src/client/views/InkingStroke.tsx                  | 10 ++---
 src/client/views/Main.tsx                          |  5 ++-
 src/client/views/PropertiesView.tsx                |  9 ++---
 .../views/collections/CollectionDockingView.tsx    | 14 ++++---
 src/client/views/collections/TreeView.tsx          |  5 +--
 .../collectionSchema/CollectionSchemaView.tsx      |  8 ++--
 .../collectionSchema/SchemaTableCell.tsx           |  9 ++---
 src/client/views/nodes/ComparisonBox.tsx           |  7 ++--
 src/client/views/nodes/DocumentView.tsx            | 26 ++++++------
 src/client/views/nodes/FieldView.tsx               |  2 +-
 src/client/views/nodes/KeyValueBox.tsx             | 46 +++++++++++-----------
 src/client/views/nodes/KeyValuePair.tsx            |  3 +-
 src/client/views/nodes/PDFBox.tsx                  |  6 +--
 src/client/views/nodes/WebBox.tsx                  |  6 +--
 .../views/nodes/formattedText/FormattedTextBox.tsx |  6 +--
 .../views/nodes/formattedText/RichTextRules.ts     | 14 +++----
 src/fields/Doc.ts                                  | 18 +++++++--
 22 files changed, 122 insertions(+), 118 deletions(-)

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

diff --git a/src/client/DocServer.ts b/src/client/DocServer.ts
index cf7a61d24..ac865382d 100644
--- a/src/client/DocServer.ts
+++ b/src/client/DocServer.ts
@@ -26,7 +26,10 @@ import { SerializationHelper } from './util/SerializationHelper';
  */
 export namespace DocServer {
     // eslint-disable-next-line import/no-mutable-exports
-    export let _cache: { [id: string]: RefField | Promise<Opt<RefField>> } = {};
+    let _cache: { [id: string]: RefField | Promise<Opt<RefField>> } = {};
+    export function Cache() {
+        return _cache;
+    }
 
     function errorFunc(): never {
         throw new Error("Can't use DocServer without calling init first");
@@ -54,15 +57,6 @@ export namespace DocServer {
         }
     }
 
-    export function FindDocByTitle(title: string) {
-        const foundDocId =
-            title &&
-            Array.from(Object.keys(_cache))
-                .filter(key => _cache[key] instanceof Doc)
-                .find(key => (_cache[key] as Doc).title === title);
-
-        return foundDocId ? (_cache[foundDocId] as Doc) : undefined;
-    }
     let _socket: Socket;
     // this client's distinct GUID created at initialization
     let USER_ID: string;
@@ -228,7 +222,7 @@ export namespace DocServer {
             const deserializeField = getSerializedField.then(async fieldJson => {
                 // deserialize
                 const field = await SerializationHelper.Deserialize(fieldJson);
-                if (force && field instanceof Doc && cached instanceof Doc) {
+                if (force && field && cached instanceof Doc) {
                     cached[UpdatingFromServer] = true;
                     Array.from(Object.keys(field)).forEach(key => {
                         const fieldval = field[key];
diff --git a/src/client/util/SharingManager.tsx b/src/client/util/SharingManager.tsx
index 31a99896f..c2a52cae9 100644
--- a/src/client/util/SharingManager.tsx
+++ b/src/client/util/SharingManager.tsx
@@ -94,6 +94,7 @@ export class SharingManager extends React.Component<{}> {
         super(props);
         makeObservable(this);
         SharingManager.Instance = this;
+        DocumentView.ShareOpen = this.open;
     }
 
     /**
diff --git a/src/client/views/DashboardView.tsx b/src/client/views/DashboardView.tsx
index d92a73b63..fc6a330f7 100644
--- a/src/client/views/DashboardView.tsx
+++ b/src/client/views/DashboardView.tsx
@@ -22,7 +22,6 @@ import { Docs, DocumentOptions } from '../documents/Documents';
 import { dropActionType } from '../util/DropActionTypes';
 import { HistoryUtil } from '../util/History';
 import { ScriptingGlobals } from '../util/ScriptingGlobals';
-import { SharingManager } from '../util/SharingManager';
 import { SnappingManager } from '../util/SnappingManager';
 import { undoBatch, undoable } from '../util/UndoManager';
 import { ContextMenu } from './ContextMenu';
@@ -30,6 +29,7 @@ import './DashboardView.scss';
 import { MainViewModal } from './MainViewModal';
 import { ObservableReactComponent } from './ObservableReactComponent';
 import { Colors } from './global/globalEnums';
+import { DocumentView } from './nodes/DocumentView';
 import { ButtonType } from './nodes/FontIconBox/FontIconBox';
 
 enum DashboardGroup {
@@ -170,7 +170,7 @@ export class DashboardView extends ObservableReactComponent<{}> {
 
             ContextMenu.Instance.addItem({
                 description: `Share Dashboard`,
-                event: () => SharingManager.Instance.open(undefined, dashboard),
+                event: () => DocumentView.ShareOpen(undefined, dashboard),
                 icon: 'edit',
             });
             ContextMenu.Instance.addItem({
@@ -535,7 +535,7 @@ ScriptingGlobals.add(function createNewDashboard() {
 }, 'creates a new dashboard when called');
 // eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function shareDashboard(dashboard: Doc) {
-    SharingManager.Instance.open(undefined, dashboard);
+    DocumentView.ShareOpen(undefined, dashboard);
 }, 'opens sharing dialog for Dashboard');
 // eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function removeDashboard(dashboard: Doc) {
diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx
index 97ff346e4..70b20aec7 100644
--- a/src/client/views/DocComponent.tsx
+++ b/src/client/views/DocComponent.tsx
@@ -23,9 +23,7 @@ import { OpenWhere } from './nodes/OpenWhere';
  * Many of these methods only make sense for specific viewBox'es, but they should be written to
  * be as general as possible
  */
-export interface ViewBoxInterface {
-    fieldKey?: string;
-    annotationKey?: string;
+export class ViewBoxInterface<P> extends ObservableReactComponent<React.PropsWithChildren<P>> {
     promoteCollection?: () => void; // moves contents of collection to parent
     updateIcon?: () => void; // updates the icon representation of the document
     getAnchor?: (addAsAnnotation: boolean, pinData?: PinProps) => Doc; // returns an Anchor Doc that represents the current state of the doc's componentview (e.g., the current playhead location of a an audio/video box)
@@ -61,7 +59,7 @@ export interface ViewBoxInterface {
     snapPt?: (pt: { X: number; Y: number }, excludeSegs?: number[]) => { nearestPt: { X: number; Y: number }; distance: number };
     search?: (str: string, bwd?: boolean, clear?: boolean) => boolean;
     dontRegisterView?: () => boolean; // KeyValueBox's don't want to register their views
-    isUnstyledView?: () => boolean; // SchemaView and KeyValue are untyled -- not tiles, no opacity
+    isUnstyledView?: () => boolean; // SchemaView and KeyValue are unstyled -- not titles, no opacity, no animations
 }
 /**
  * DocComponent returns a React base class used by Doc views with accessors for unpacking the Document,layoutDoc, and dataDoc's
@@ -113,7 +111,7 @@ export function DocComponent<P extends DocComponentProps>() {
  *   Example views include: InkingStroke, FontIconBox, EquationBox, etc
  */
 export function ViewBoxBaseComponent<P extends FieldViewProps>() {
-    class Component extends ObservableReactComponent<React.PropsWithChildren<P>> {
+    class Component extends ViewBoxInterface<P> {
         constructor(props: P) {
             super(props);
             makeObservable(this);
@@ -168,7 +166,7 @@ export function ViewBoxBaseComponent<P extends FieldViewProps>() {
  * Example views include: PDFBox, ImageBox, MapBox, etc
  */
 export function ViewBoxAnnotatableComponent<P extends FieldViewProps>() {
-    class Component extends ObservableReactComponent<React.PropsWithChildren<P>> {
+    class Component extends ViewBoxInterface<P> {
         @observable _annotationKeySuffix = () => 'annotations';
         @observable _isAnyChildContentActive = false;
 
@@ -219,8 +217,7 @@ export function ViewBoxAnnotatableComponent<P extends FieldViewProps>() {
             return this.fieldKey + (this._annotationKeySuffix() ? '_' + this._annotationKeySuffix() : '');
         }
 
-        @action.bound
-        removeDocument(docIn: Doc | Doc[], annotationKey?: string, leavePushpin?: boolean, dontAddToRemoved?: boolean): boolean {
+        override removeDocument = (docIn: Doc | Doc[], annotationKey?: string, leavePushpin?: boolean, dontAddToRemoved?: boolean): boolean => {
             const effectiveAcl = GetEffectiveAcl(this.dataDoc);
             const docs = toList(docIn).filter(fdoc => [AclEdit, AclAdmin].includes(effectiveAcl) || GetEffectiveAcl(fdoc) === AclAdmin);
 
@@ -249,12 +246,11 @@ export function ViewBoxAnnotatableComponent<P extends FieldViewProps>() {
             }
 
             return false;
-        }
+        };
         // this is called with the document that was dragged and the collection to move it into.
         // if the target collection is the same as this collection, then the move will be allowed.
         // otherwise, the document being moved must be able to be removed from its container before
         // moving it into the target.
-        @action.bound
         moveDocument = (docs: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (doc: Doc | Doc[], annotationKey?: string) => boolean, annotationKey?: string): boolean => {
             if (Doc.AreProtosEqual(this._props.Document, targetCollection)) {
                 return true;
@@ -265,8 +261,8 @@ export function ViewBoxAnnotatableComponent<P extends FieldViewProps>() {
             }
             return false;
         };
-        @action.bound
-        addDocument = (docIn: Doc | Doc[], annotationKey?: string): boolean => {
+
+        override addDocument = (docIn: Doc | Doc[], annotationKey?: string): boolean => {
             const docs = toList(docIn);
             if (this._props.filterAddDocument?.(docs) === false || docs.find(fdoc => Doc.AreProtosEqual(fdoc, this.Document) && Doc.LayoutField(fdoc) === Doc.LayoutField(this.Document))) {
                 return false;
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 432b02782..4262c2d57 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -33,7 +33,6 @@ import { Colors } from './global/globalEnums';
 import { CollectionFreeFormDocumentView } from './nodes/CollectionFreeFormDocumentView';
 import { DocumentView } from './nodes/DocumentView';
 import { ImageBox } from './nodes/ImageBox';
-import { KeyValueBox } from './nodes/KeyValueBox';
 import { OpenWhereMod } from './nodes/OpenWhere';
 import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox';
 
@@ -137,7 +136,7 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
                         if (titleFieldKey === 'title') {
                             dv.dataDoc.title_custom = !this._accumulatedTitle.startsWith('-');
                         }
-                        KeyValueBox.SetField(dv.Document, titleFieldKey, this._accumulatedTitle);
+                        Doc.SetField(dv.Document, titleFieldKey, this._accumulatedTitle);
                     }),
                 'edit title'
             );
diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx
index 62fc73c78..f497ca447 100644
--- a/src/client/views/InkingStroke.tsx
+++ b/src/client/views/InkingStroke.tsx
@@ -42,14 +42,14 @@ import './InkStroke.scss';
 import { InkStrokeProperties } from './InkStrokeProperties';
 import { InkTangentHandles } from './InkTangentHandles';
 import { FieldView, FieldViewProps } from './nodes/FieldView';
-import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox';
+import { FormattedTextBox, FormattedTextBoxProps } from './nodes/formattedText/FormattedTextBox';
 import { PinDocView, PinProps } from './PinFuncs';
 import { StyleProp } from './StyleProp';
 
 const { INK_MASK_SIZE } = require('./global/globalCssVariables.module.scss'); // prettier-ignore
 
 @observer
-export class InkingStroke extends ViewBoxAnnotatableComponent<FieldViewProps>() implements ViewBoxInterface {
+export class InkingStroke extends ViewBoxAnnotatableComponent<FieldViewProps>() {
     static readonly MaskDim = INK_MASK_SIZE; // choose a really big number to make sure mask fits over container (which in theory can be arbitrarily big)
     public static LayoutString(fieldStr: string) {
         return FieldView.LayoutString(InkingStroke, fieldStr);
@@ -337,9 +337,9 @@ export class InkingStroke extends ViewBoxAnnotatableComponent<FieldViewProps>()
         );
     };
 
-    _subContentView: ViewBoxInterface | undefined;
-    setSubContentView = (doc: ViewBoxInterface) => {
-        this._subContentView = doc;
+    _subContentView: ViewBoxInterface<FormattedTextBoxProps> | undefined;
+    setSubContentView = (box: ViewBoxInterface<FormattedTextBoxProps>) => {
+        this._subContentView = box;
     };
     @computed get fillColor(): string {
         const isInkMask = BoolCast(this.layoutDoc.stroke_isInkMask);
diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx
index b6dfed687..259ffbbc5 100644
--- a/src/client/views/Main.tsx
+++ b/src/client/views/Main.tsx
@@ -7,9 +7,11 @@ import * as dotenv from 'dotenv'; // see https://github.com/motdotla/dotenv#how-
 import * as React from 'react';
 import * as ReactDOM from 'react-dom/client';
 import { AssignAllExtensions } from '../../extensions/Extensions';
+import { Doc } from '../../fields/Doc';
 import { FieldLoader } from '../../fields/FieldLoader';
 import { BranchingTrailManager } from '../util/BranchingTrailManager';
 import { CurrentUserUtils } from '../util/CurrentUserUtils';
+import { LinkFollower } from '../util/LinkFollower';
 import { PingManager } from '../util/PingManager';
 import { ReplayMovements } from '../util/ReplayMovements';
 import { TrackMovements } from '../util/TrackMovements';
@@ -18,7 +20,7 @@ import { MainView } from './MainView';
 import { CollectionView } from './collections/CollectionView';
 import { CollectionFreeFormInfoUI } from './collections/collectionFreeForm/CollectionFreeFormInfoUI';
 import './global/globalScripts';
-import { LinkFollower } from '../util/LinkFollower';
+import { KeyValueBox } from './nodes/KeyValueBox';
 
 dotenv.config();
 
@@ -66,6 +68,7 @@ FieldLoader.ServerLoadStatus = { requested: 0, retrieved: 0, message: 'cache' };
         // iniitialize plugin apis
         CollectionFreeFormInfoUI.Init();
         LinkFollower.Init();
+        KeyValueBox.Init();
         root.render(<MainView />);
     }, 0);
 })();
diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx
index 7c16e0ddb..b03c1a64e 100644
--- a/src/client/views/PropertiesView.tsx
+++ b/src/client/views/PropertiesView.tsx
@@ -41,7 +41,6 @@ import './PropertiesView.scss';
 import { DefaultStyleProvider, SetFilterOpener as SetPropertiesFilterOpener } from './StyleProvider';
 import { DocumentView } from './nodes/DocumentView';
 import { StyleProviderFuncType } from './nodes/FieldView';
-import { KeyValueBox } from './nodes/KeyValueBox';
 import { OpenWhere } from './nodes/OpenWhere';
 import { PresBox, PresEffect, PresEffectDirection } from './nodes/trails';
 
@@ -208,7 +207,7 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
                             fontSize={10}
                             GetValue={() => editableContents}
                             SetValue={(value: string) => {
-                                value !== '-multiple-' && docs.map(doc => KeyValueBox.SetField(doc, key, value, true));
+                                value !== '-multiple-' && docs.map(doc => Doc.SetField(doc, key, value, true));
                                 return true;
                             }}
                         />
@@ -252,10 +251,10 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
             if (value.indexOf(':') !== -1) {
                 const newVal = value[0].toUpperCase() + value.substring(1, value.length);
                 const splits = newVal.split(':');
-                KeyValueBox.SetField(doc, splits[0], splits[1], true);
+                Doc.SetField(doc, splits[0], splits[1], true);
                 const tags = StrCast(doc.tags, ':');
                 if (tags.includes(`${splits[0]}:`) && splits[1] === 'undefined') {
-                    KeyValueBox.SetField(doc, 'tags', `"${tags.replace(splits[0] + ':', '')}"`, true);
+                    Doc.SetField(doc, 'tags', `"${tags.replace(splits[0] + ':', '')}"`, true);
                 }
                 return true;
             }
@@ -645,7 +644,7 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
             DocumentView.Selected().map(dv => Doc.SetInPlace(dv.Document, 'title', value, true));
         } else if (this.dataDoc) {
             if (this.selectedDoc) Doc.SetInPlace(this.selectedDoc, 'title', value, true);
-            else KeyValueBox.SetField(this.dataDoc, 'title', value as string, true);
+            else Doc.SetField(this.dataDoc, 'title', value as string, true);
         }
     };
 
diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index fc9e2e39b..0ee3575f3 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -2,7 +2,7 @@ import { action, IReactionDisposer, makeObservable, observable, reaction } from
 import { observer } from 'mobx-react';
 import * as React from 'react';
 import * as ReactDOM from 'react-dom/client';
-import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, DivHeight, DivWidth, incrementTitleCopy, UpdateIcon } from '../../../ClientUtils';
+import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, DivHeight, DivWidth, incrementTitleCopy, returnTrue, UpdateIcon } from '../../../ClientUtils';
 import { Doc, DocListCast, Field, Opt } from '../../../fields/Doc';
 import { AclAdmin, AclEdit, DocData } from '../../../fields/DocSymbols';
 import { Id } from '../../../fields/FieldSymbols';
@@ -49,7 +49,8 @@ export class CollectionDockingView extends CollectionSubView() {
     private _reactionDisposer?: IReactionDisposer;
     private _lightboxReactionDisposer?: IReactionDisposer;
     private _containerRef = React.createRef<HTMLDivElement>();
-    public _flush: UndoManager.Batch | undefined;
+    private _flush: UndoManager.Batch | undefined;
+    private _unmounting = false;
     private _ignoreStateChange = '';
     public tabMap: Set<any> = new Set();
     public get HasFullScreen() {
@@ -330,6 +331,7 @@ export class CollectionDockingView extends CollectionSubView() {
     };
 
     componentDidMount: () => void = async () => {
+        this._props.setContentViewBox?.(this);
         this._unmounting = false;
         SetPropSetterCb('title', this.titleChanged); // this overrides any previously assigned callback for the property
         if (this._containerRef.current) {
@@ -366,7 +368,6 @@ export class CollectionDockingView extends CollectionSubView() {
         }
     };
 
-    _unmounting = false;
     componentWillUnmount: () => void = () => {
         this._unmounting = true;
         try {
@@ -383,6 +384,9 @@ export class CollectionDockingView extends CollectionSubView() {
         this._lightboxReactionDisposer?.();
     };
 
+    // ViewBoxInterface overrides
+    override isUnstyledView = returnTrue;
+
     @action
     onResize = () => {
         const cur = this._containerRef.current;
@@ -437,8 +441,8 @@ export class CollectionDockingView extends CollectionSubView() {
                 } else {
                     const tabTarget = (e.target as HTMLElement)?.parentElement?.className.includes('lm_tab') ? (e.target as HTMLElement).parentElement : (e.target as HTMLElement);
                     const map = Array.from(this.tabMap).find(tab => tab.element[0] === tabTarget);
-                    if (map?.DashDoc && DocumentView.getFirstDocumentView(map.DashDoc)) {
-                        DocumentView.SelectView(DocumentView.getFirstDocumentView(map.DashDoc), false);
+                    if (map?.DashDoc && DocumentView.getDocumentView(map.DashDoc, this.DocumentView?.())) {
+                        DocumentView.SelectView(DocumentView.getDocumentView(map.DashDoc, this.DocumentView?.()), false);
                     }
                 }
             }
diff --git a/src/client/views/collections/TreeView.tsx b/src/client/views/collections/TreeView.tsx
index 969b98d91..6ea6bbfbd 100644
--- a/src/client/views/collections/TreeView.tsx
+++ b/src/client/views/collections/TreeView.tsx
@@ -32,7 +32,6 @@ import { ObservableReactComponent } from '../ObservableReactComponent';
 import { StyleProp } from '../StyleProp';
 import { DocumentView, DocumentViewInternal } from '../nodes/DocumentView';
 import { FieldViewProps, StyleProviderFuncType } from '../nodes/FieldView';
-import { KeyValueBox } from '../nodes/KeyValueBox';
 import { OpenWhere } from '../nodes/OpenWhere';
 import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox';
 import { RichTextMenu } from '../nodes/formattedText/RichTextMenu';
@@ -568,7 +567,7 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> {
                         height={13}
                         fontSize={12}
                         GetValue={() => Field.toKeyValueString(doc, key)}
-                        SetValue={(value: string) => KeyValueBox.SetField(doc, key, value, true)}
+                        SetValue={(value: string) => Doc.SetField(doc, key, value, true)}
                     />
                 );
             }
@@ -600,7 +599,7 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> {
                             const key = match[1];
                             const assign = match[2];
                             const val = match[3];
-                            KeyValueBox.SetField(doc, key, assign + val, false);
+                            Doc.SetField(doc, key, assign + val, false);
                             return true;
                         }
                         return false;
diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx
index b30954ffd..77247e675 100644
--- a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx
+++ b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx
@@ -28,7 +28,6 @@ import { Colors } from '../../global/globalEnums';
 import { DocumentView } from '../../nodes/DocumentView';
 import { FieldViewProps } from '../../nodes/FieldView';
 import { FocusViewOptions } from '../../nodes/FocusViewOptions';
-import { KeyValueBox } from '../../nodes/KeyValueBox';
 import { CollectionSubView } from '../CollectionSubView';
 import './CollectionSchemaView.scss';
 import { SchemaColumnHeader } from './SchemaColumnHeader';
@@ -174,7 +173,9 @@ export class CollectionSchemaView extends CollectionSubView() {
         document.removeEventListener('keydown', this.onKeyDown);
     }
 
-    isUnstyledView = returnTrue; // used by style provide via ViewBoxInterface
+    // ViewBoxInterface overrides
+    override isUnstyledView = returnTrue; // used by style provider : turns off  opacity, animation effects, scaling
+
     rowIndex = (doc: Doc) => this.sortedDocs.docs.indexOf(doc);
 
     @action
@@ -606,7 +607,7 @@ export class CollectionSchemaView extends CollectionSubView() {
     };
 
     setColumnValues = (key: string, value: string) => {
-        this.childDocs.forEach(doc => KeyValueBox.SetField(doc, key, value));
+        this.childDocs.forEach(doc => Doc.SetField(doc, key, value));
         return true;
     };
 
@@ -804,7 +805,6 @@ export class CollectionSchemaView extends CollectionSubView() {
         );
     }
     get renderKeysMenu() {
-        console.log('RNDERMENUT:' + this._columnMenuIndex);
         return (
             <div className="schema-column-menu" style={{ left: 0, minWidth: CollectionSchemaView._minColWidth }}>
                 <input className="schema-key-search-input" type="text" onKeyDown={this.onSearchKeyDown} onChange={this.updateKeySearch} onPointerDown={e => e.stopPropagation()} />
diff --git a/src/client/views/collections/collectionSchema/SchemaTableCell.tsx b/src/client/views/collections/collectionSchema/SchemaTableCell.tsx
index ee6987e89..3df7ecdbe 100644
--- a/src/client/views/collections/collectionSchema/SchemaTableCell.tsx
+++ b/src/client/views/collections/collectionSchema/SchemaTableCell.tsx
@@ -29,7 +29,6 @@ import { DefaultStyleProvider } from '../../StyleProvider';
 import { Colors } from '../../global/globalEnums';
 import { DocFocusOrOpen, returnEmptyDocViewList } from '../../nodes/DocumentView';
 import { FieldViewProps } from '../../nodes/FieldView';
-import { KeyValueBox } from '../../nodes/KeyValueBox';
 import { FormattedTextBox } from '../../nodes/formattedText/FormattedTextBox';
 import { FInfotoColType } from './CollectionSchemaView';
 import './CollectionSchemaView.scss';
@@ -144,7 +143,7 @@ export class SchemaTableCell extends ObservableReactComponent<SchemaTableCellPro
                             this._props.finishEdit?.();
                             return true;
                         }
-                        const ret = KeyValueBox.SetField(fieldProps.Document, this._props.fieldKey.replace(/^_/, ''), value, Doc.IsDataProto(fieldProps.Document) ? true : undefined);
+                        const ret = Doc.SetField(fieldProps.Document, this._props.fieldKey.replace(/^_/, ''), value, Doc.IsDataProto(fieldProps.Document) ? true : undefined);
                         this._props.finishEdit?.();
                         return ret;
                     }, 'edit schema cell')}
@@ -357,7 +356,7 @@ export class SchemaBoolCell extends ObservableReactComponent<SchemaTableCellProp
                     onChange={undoBatch((value: React.ChangeEvent<HTMLInputElement> | undefined) => {
                         if ((value?.nativeEvent as any).shiftKey) {
                             this._props.setColumnValues(this._props.fieldKey.replace(/^_/, ''), (color === 'black' ? '=' : '') + (value?.target?.checked.toString() ?? ''));
-                        } else KeyValueBox.SetField(this._props.Document, this._props.fieldKey.replace(/^_/, ''), (color === 'black' ? '=' : '') + (value?.target?.checked.toString() ?? ''));
+                        } else Doc.SetField(this._props.Document, this._props.fieldKey.replace(/^_/, ''), (color === 'black' ? '=' : '') + (value?.target?.checked.toString() ?? ''));
                     })}
                 />
                 <EditableView
@@ -371,7 +370,7 @@ export class SchemaBoolCell extends ObservableReactComponent<SchemaTableCellProp
                             this._props.finishEdit?.();
                             return true;
                         }
-                        const set = KeyValueBox.SetField(this._props.Document, this._props.fieldKey.replace(/^_/, ''), value, Doc.IsDataProto(this._props.Document) ? true : undefined);
+                        const set = Doc.SetField(this._props.Document, this._props.fieldKey.replace(/^_/, ''), value, Doc.IsDataProto(this._props.Document) ? true : undefined);
                         this._props.finishEdit?.();
                         return set;
                     })}
@@ -437,7 +436,7 @@ export class SchemaEnumerationCell extends ObservableReactComponent<SchemaTableC
                         placeholder={StrCast(this._props.Document[this._props.fieldKey], 'select...')}
                         options={options}
                         isMulti={false}
-                        onChange={val => KeyValueBox.SetField(this._props.Document, this._props.fieldKey.replace(/^_/, ''), `"${val?.value ?? ''}"`)}
+                        onChange={val => Doc.SetField(this._props.Document, this._props.fieldKey.replace(/^_/, ''), `"${val?.value ?? ''}"`)}
                     />
                 </div>
             </div>
diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx
index 6ae6bb228..474d54119 100644
--- a/src/client/views/nodes/ComparisonBox.tsx
+++ b/src/client/views/nodes/ComparisonBox.tsx
@@ -19,11 +19,10 @@ import { StyleProp } from '../StyleProp';
 import './ComparisonBox.scss';
 import { DocumentView } from './DocumentView';
 import { FieldView, FieldViewProps } from './FieldView';
-import { KeyValueBox } from './KeyValueBox';
 import { FormattedTextBox } from './formattedText/FormattedTextBox';
 
 @observer
-export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implements ViewBoxInterface {
+export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
     public static LayoutString(fieldKey: string) {
         return FieldView.LayoutString(ComparisonBox, fieldKey);
     }
@@ -188,9 +187,9 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
         //     where (this) is replaced by the text in the fieldKey slot abd this.excludeWords is repalced by the conetnts of the excludeWords field
         // The GPT call will put the "answer"  in the second slot of the comparison (eg., text_2)
         if (whichSlot.endsWith('2') && !layoutTemplateString?.includes(whichSlot)) {
-            const queryText = altText?.replace('(this)', subjectText); // TODO: this should be done in KeyValueBox.setField but it doesn't know about the fieldKey ...
+            const queryText = altText?.replace('(this)', subjectText); // TODO: this should be done in Doc.setField but it doesn't know about the fieldKey ...
             if (queryText?.match(/\(\(.*\)\)/)) {
-                KeyValueBox.SetField(this.Document, whichSlot, ':=' + queryText, false); // make the second slot be a computed field on the data doc that calls ChatGpt
+                Doc.SetField(this.Document, whichSlot, ':=' + queryText, false); // make the second slot be a computed field on the data doc that calls ChatGpt
             }
         }
         return layoutTemplateString;
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index f8ce50c98..4a5c32b4c 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -31,7 +31,6 @@ import { MakeTemplate, makeUserTemplateButton } from '../../util/DropConverter';
 import { UPDATE_SERVER_CACHE } from '../../util/LinkManager';
 import { ScriptingGlobals } from '../../util/ScriptingGlobals';
 import { SearchUtil } from '../../util/SearchUtil';
-import { SharingManager } from '../../util/SharingManager';
 import { SnappingManager } from '../../util/SnappingManager';
 import { UndoManager, undoBatch, undoable } from '../../util/UndoManager';
 import { ContextMenu } from '../ContextMenu';
@@ -47,7 +46,6 @@ import { DocumentLinksButton } from './DocumentLinksButton';
 import './DocumentView.scss';
 import { FieldViewProps, FieldViewSharedProps } from './FieldView';
 import { FocusViewOptions } from './FocusViewOptions';
-import { KeyValueBox } from './KeyValueBox';
 import { OpenWhere } from './OpenWhere';
 import { FormattedTextBox } from './formattedText/FormattedTextBox';
 import { PresEffect, PresEffectDirection } from './trails';
@@ -115,7 +113,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
     @observable _mounted = false; // turn off all pointer events if component isn't yet mounted (enables nested Docs in alternate UI textboxes that appear on hover which otherwise would grab focus from the text box, reverting to the original UI )
     @observable _isContentActive: boolean | undefined = undefined;
     @observable _pointerEvents: 'none' | 'all' | 'visiblePainted' | undefined = undefined;
-    @observable _componentView: Opt<ViewBoxInterface> = undefined; // needs to be accessed from DocumentView wrapper class
+    @observable _componentView: Opt<ViewBoxInterface<FieldViewProps>> = undefined; // needs to be accessed from DocumentView wrapper class
     @observable _animateScaleTime: Opt<number> = undefined; // milliseconds for animating between views.  defaults to 300 if not uset
     @observable _animateScalingTo = 0;
 
@@ -627,7 +625,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
         const constantItems: ContextMenuProps[] = [];
         if (!Doc.IsSystem(this.Document) && this.Document._type_collection !== CollectionViewType.Docking) {
             constantItems.push({ description: 'Zip Export', icon: 'download', event: async () => DocUtils.Zip(this.Document) });
-            (this.Document._type_collection !== CollectionViewType.Docking || !Doc.noviceMode) && constantItems.push({ description: 'Share', event: () => SharingManager.Instance.open(this._docView), icon: 'users' });
+            constantItems.push({ description: 'Share', event: () => DocumentView.ShareOpen(this._docView), icon: 'users' });
             if (this._props.removeDocument && Doc.ActiveDashboard !== this.Document) {
                 // need option to gray out menu items ... preferably with a '?' that explains why they're grayed out (eg., no permissions)
                 constantItems.push({ description: 'Close', event: this.deleteClicked, icon: 'times' });
@@ -699,7 +697,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
     screenToLocalContent = () => this._props.ScreenToLocalTransform().translate(0, -this.headerMargin);
     onClickFunc = this.disableClickScriptFunc ? undefined : () => this.onClickHandler;
     setHeight = (height: number) => { !this._props.suppressSetHeight && (this.layoutDoc._height = Math.min(NumCast(this.layoutDoc._maxHeight, Number.MAX_SAFE_INTEGER), height)); } // prettier-ignore
-    setContentView = action((view: ViewBoxInterface) => { this._componentView = view; }); // prettier-ignore
+    setContentView = action((view: ViewBoxInterface<FieldViewProps>) => { this._componentView = view; }); // prettier-ignore
     isContentActive = (): boolean | undefined => this._isContentActive;
     childFilters = () => [...this._props.childFilters(), ...StrListCast(this.layoutDoc.childFilters)];
 
@@ -725,7 +723,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
     @computed get viewBoxContents() {
         TraceMobx();
         const isInk = this.layoutDoc._layout_isSvg && !this._props.LayoutTemplateString;
-        const noBackground = this.Document.isGroup && !this._props.LayoutTemplateString?.includes(KeyValueBox.name) && (!this.layoutDoc.backgroundColor || this.layoutDoc.backgroundColor === 'transparent');
+        const noBackground = this.Document.isGroup && !this._componentView?.isUnstyledView?.() && (!this.layoutDoc.backgroundColor || this.layoutDoc.backgroundColor === 'transparent');
         return (
             <div
                 className="documentView-contentsView"
@@ -784,7 +782,9 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
         const targetDoc = showTitle?.startsWith('_') ? this.layoutDoc : this.Document;
         const background = StrCast(
             this.layoutDoc.layout_headingColor,
-            StrCast(SharingManager.Instance.users.find(u => u.user.email === this.dataDoc.author)?.sharingDoc.headingColor, StrCast(Doc.SharingDoc().headingColor, SnappingManager.userBackgroundColor))
+            // StrCast(SharingManager.Instance.users.find(u => u.user.email === this.dataDoc.author)?.sharingDoc.headingColor,
+            StrCast(Doc.SharingDoc().headingColor, SnappingManager.userBackgroundColor)
+            // )
         );
         const dropdownWidth = this._titleRef.current?._editing || this._changingTitleField ? Math.max(10, (this._titleDropDownInnerWidth * this.titleHeight) / 30) : 0;
         const sidebarWidthPercent = +StrCast(this.layoutDoc.layout_sidebarWidthPercent).replace('%', '');
@@ -839,7 +839,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
                                     Doc.UserDoc().layout_showTitle = input?.substring(1) ? input.substring(1) : 'title';
                                 }
                             } else if (showTitle && !showTitle.includes(';') && !showTitle.includes('Date') && showTitle !== 'author') {
-                                KeyValueBox.SetField(targetDoc, showTitle, input);
+                                Doc.SetField(targetDoc, showTitle, input);
                             }
                             return true;
                         })}
@@ -940,7 +940,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
                     borderRadius: this.borderRounding,
                     pointerEvents: this._pointerEvents === 'visiblePainted' ? 'none' : this._pointerEvents, // visible painted means that the underlying doc contents are irregular and will process their own pointer events (otherwise, the contents are expected to fill the entire doc view box so we can handle pointer events here)
                 }}>
-                {this._componentView instanceof KeyValueBox ? renderDoc : DocumentViewInternal.AnimationEffect(renderDoc, this.Document[Animation])}
+                {this._componentView?.isUnstyledView?.() ? renderDoc : DocumentViewInternal.AnimationEffect(renderDoc, this.Document[Animation])}
                 {borderPath?.jsx}
             </div>
         );
@@ -980,6 +980,8 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
 @observer
 export class DocumentView extends DocComponent<DocumentViewProps>() {
     public static ROOT_DIV = 'documentView-effectsWrapper';
+    // Sharing Manager
+    public static ShareOpen: (target?: DocumentView, targetDoc?: Doc) => void;
     // LinkFollower
     public static FollowLink: (linkDoc: Opt<Doc>, sourceDoc: Doc, altKey: boolean) => boolean;
     // selection funcs
@@ -1049,7 +1051,7 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
     @observable public static LongPress = false;
 
     @computed private get shouldNotScale() {
-        return (this.layout_fitWidth && !this.nativeWidth) || this._props.LayoutTemplateString?.includes(KeyValueBox.name) || [CollectionViewType.Docking].includes(this.Document._type_collection as any);
+        return (this.layout_fitWidth && !this.nativeWidth) || this.ComponentView?.isUnstyledView?.();
     }
     @computed private get effectiveNativeWidth() {
         return this.shouldNotScale ? 0 : this.nativeWidth || NumCast(this.layoutDoc.width);
@@ -1144,10 +1146,10 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
     }
 
     @computed get nativeWidth() {
-        return this._props.LayoutTemplateString?.includes(KeyValueBox.name) ? 0 : returnVal(this._props.NativeWidth?.(), Doc.NativeWidth(this.layoutDoc, this._props.TemplateDataDocument, !this.layout_fitWidth));
+        return returnVal(this._props.NativeWidth?.(), Doc.NativeWidth(this.layoutDoc, this._props.TemplateDataDocument, !this.layout_fitWidth));
     }
     @computed get nativeHeight() {
-        return this._props.LayoutTemplateString?.includes(KeyValueBox.name) ? 0 : returnVal(this._props.NativeHeight?.(), Doc.NativeHeight(this.layoutDoc, this._props.TemplateDataDocument, !this.layout_fitWidth));
+        return returnVal(this._props.NativeHeight?.(), Doc.NativeHeight(this.layoutDoc, this._props.TemplateDataDocument, !this.layout_fitWidth));
     }
     @computed public get centeringX() { return this._props.dontCenter?.includes('x') ? 0 : this.Xshift; } // prettier-ignore
     @computed public get centeringY() { return this._props.dontCenter?.includes('y') ? 0 : this.Yshift; } // prettier-ignore
diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx
index c6c77d8d2..8a37000f7 100644
--- a/src/client/views/nodes/FieldView.tsx
+++ b/src/client/views/nodes/FieldView.tsx
@@ -45,7 +45,7 @@ export interface FieldViewSharedProps {
     containerViewPath?: () => DocumentView[];
     fitContentsToBox?: () => boolean; // used by freeformview to fit its contents to its panel. corresponds to _freeform_fitContentsToBox property on a Document
     isGroupActive?: () => string | undefined; // is this document part of a group that is active
-    setContentViewBox?: (view: ViewBoxInterface) => any; // called by rendered field's viewBox so that DocumentView can make direct calls to the viewBox
+    setContentViewBox?: (view: ViewBoxInterface<any>) => any; // called by rendered field's viewBox so that DocumentView can make direct calls to the viewBox
     PanelWidth: () => number;
     PanelHeight: () => number;
     isDocumentActive?: () => boolean | undefined; // whether a document should handle pointer events
diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx
index cd591fa42..66e210c03 100644
--- a/src/client/views/nodes/KeyValueBox.tsx
+++ b/src/client/views/nodes/KeyValueBox.tsx
@@ -13,10 +13,10 @@ import { DocumentType } from '../../documents/DocumentTypes';
 import { Docs } from '../../documents/Documents';
 import { SetupDrag } from '../../util/DragManager';
 import { CompiledScript } from '../../util/Scripting';
-import { undoBatch } from '../../util/UndoManager';
+import { undoable } from '../../util/UndoManager';
 import { ContextMenu } from '../ContextMenu';
 import { ContextMenuProps } from '../ContextMenuItem';
-import { ObservableReactComponent } from '../ObservableReactComponent';
+import { ViewBoxBaseComponent } from '../DocComponent';
 import { DocumentIconContainer } from './DocumentIcon';
 import { FieldView, FieldViewProps } from './FieldView';
 import { ImageBox } from './ImageBox';
@@ -31,7 +31,7 @@ export type KVPScript = {
     onDelegate: boolean;
 };
 @observer
-export class KeyValueBox extends ObservableReactComponent<FieldViewProps> {
+export class KeyValueBox extends ViewBoxBaseComponent<FieldViewProps>() {
     public static LayoutString() {
         return FieldView.LayoutString(KeyValueBox, 'data');
     }
@@ -48,19 +48,17 @@ export class KeyValueBox extends ObservableReactComponent<FieldViewProps> {
     componentDidMount() {
         this._props.setContentViewBox?.(this);
     }
-    isKeyValueBox = returnTrue;
-    able = returnAlways;
-    layout_fitWidth = returnTrue;
-    onClickScriptDisable = returnAlways;
+    // ViewBoxInterface overrides
+    override isUnstyledView = returnTrue; // used by style provider via ViewBoxInterface - ignore opacity, anim effects, titles
+    override dontRegisterView = returnTrue; // don't want to follow links to this view
+    override onClickScriptDisable = returnAlways;
 
     @observable private rows: KeyValuePair[] = [];
     @observable _splitPercentage = 50;
 
     get fieldDocToLayout() {
-        return DocCast(this._props.Document);
+        return DocCast(this.Document);
     }
-    isUnstyledView = returnTrue; // used by style provider via ViewBoxInterface
-    dontRegisterView = returnTrue; // used by ViewBoxInterface
 
     @action
     onEnterKey = (e: React.KeyboardEvent): void => {
@@ -83,7 +81,7 @@ export class KeyValueBox extends ObservableReactComponent<FieldViewProps> {
      * @param value
      * @returns
      */
-    public static CompileKVPScript(rawvalueIn: string): KVPScript | undefined {
+    public static CompileKVPScript = (rawvalueIn: string): KVPScript | undefined => {
         let rawvalue = rawvalueIn;
         const onDelegate = rawvalue.startsWith('=');
         rawvalue = onDelegate ? rawvalue.substring(1) : rawvalue;
@@ -97,9 +95,9 @@ export class KeyValueBox extends ObservableReactComponent<FieldViewProps> {
             script = ScriptField.CompileScript(value, {}, true, undefined, DocumentIconContainer.getTransformer());
         }
         return !script.compiled ? undefined : { script, type, onDelegate };
-    }
+    };
 
-    public static ApplyKVPScript(doc: Doc, key: string, kvpScript: KVPScript, forceOnDelegate?: boolean, setResult?: (value: FieldResult) => void) {
+    public static ApplyKVPScript = (doc: Doc, key: string, kvpScript: KVPScript, forceOnDelegate?: boolean, setResult?: (value: FieldResult) => void) => {
         const { script, type, onDelegate } = kvpScript;
         // const target = onDelegate ? Doc.Layout(doc.layout) : Doc.GetProto(doc); // bcz: TODO need to be able to set fields on layout templates
         const target = forceOnDelegate || onDelegate || key.startsWith('_') ? doc : DocCast(doc.proto, doc);
@@ -127,14 +125,13 @@ export class KeyValueBox extends ObservableReactComponent<FieldViewProps> {
             return true;
         }
         return false;
-    }
+    };
 
-    @undoBatch
-    public static SetField(doc: Doc, key: string, value: string, forceOnDelegate?: boolean, setResult?: (value: FieldResult) => void) {
-        const script = this.CompileKVPScript(value);
+    public static SetField = undoable((doc: Doc, key: string, value: string, forceOnDelegate?: boolean, setResult?: (value: FieldResult) => void) => {
+        const script = KeyValueBox.CompileKVPScript(value);
         if (!script) return false;
-        return this.ApplyKVPScript(doc, key, script, forceOnDelegate, setResult);
-    }
+        return KeyValueBox.ApplyKVPScript(doc, key, script, forceOnDelegate, setResult);
+    }, 'Set Doc Field');
 
     onPointerDown = (e: React.PointerEvent): void => {
         if (e.buttons === 1 && this._props.isSelected()) {
@@ -249,15 +246,15 @@ export class KeyValueBox extends ObservableReactComponent<FieldViewProps> {
     getFieldView = () => {
         const rows = this.rows.filter(row => row.isChecked);
         if (rows.length > 1) {
-            const parent = Docs.Create.StackingDocument([], { _layout_autoHeight: true, _width: 300, title: `field views for ${DocCast(this._props.Document).title}`, _chromeHidden: true });
+            const parent = Docs.Create.StackingDocument([], { _layout_autoHeight: true, _width: 300, title: `field views for ${DocCast(this.Document).title}`, _chromeHidden: true });
             rows.forEach(row => {
-                const field = this.createFieldView(DocCast(this._props.Document), row);
+                const field = this.createFieldView(DocCast(this.Document), row);
                 field && Doc.AddDocToList(parent, 'data', field);
                 row.uncheck();
             });
             return parent;
         }
-        return rows.length ? this.createFieldView(DocCast(this._props.Document), rows.lastElement()) : undefined;
+        return rows.length ? this.createFieldView(DocCast(this.Document), rows.lastElement()) : undefined;
     };
 
     createFieldView = (templateDoc: Doc, row: KeyValuePair) => {
@@ -305,7 +302,7 @@ export class KeyValueBox extends ObservableReactComponent<FieldViewProps> {
         openItems.push({
             description: 'Default Perspective',
             event: () => {
-                this._props.addDocTab(this._props.Document, OpenWhere.close);
+                this._props.addDocTab(this.Document, OpenWhere.close);
                 this._props.addDocTab(this.fieldDocToLayout, OpenWhere.addRight);
             },
             icon: 'image',
@@ -341,6 +338,9 @@ export class KeyValueBox extends ObservableReactComponent<FieldViewProps> {
             </div>
         );
     }
+    public static Init() {
+        Doc.SetField = KeyValueBox.SetField;
+    }
 }
 
 Docs.Prototypes.TemplateMap.set(DocumentType.KVP, {
diff --git a/src/client/views/nodes/KeyValuePair.tsx b/src/client/views/nodes/KeyValuePair.tsx
index 878b0e54c..397cc15ed 100644
--- a/src/client/views/nodes/KeyValuePair.tsx
+++ b/src/client/views/nodes/KeyValuePair.tsx
@@ -15,7 +15,6 @@ import { EditableView } from '../EditableView';
 import { ObservableReactComponent } from '../ObservableReactComponent';
 import { DefaultStyleProvider } from '../StyleProvider';
 import { returnEmptyDocViewList } from './DocumentView';
-import { KeyValueBox } from './KeyValueBox';
 import './KeyValueBox.scss';
 import './KeyValuePair.scss';
 import { OpenWhere } from './OpenWhere';
@@ -136,7 +135,7 @@ export class KeyValuePair extends ObservableReactComponent<KeyValuePairProps> {
                                 pinToPres: returnZero,
                             }}
                             GetValue={() => Field.toKeyValueString(this._props.doc, this._props.keyName)}
-                            SetValue={(value: string) => KeyValueBox.SetField(this._props.doc, this._props.keyName, value)}
+                            SetValue={(value: string) => Doc.SetField(this._props.doc, this._props.keyName, value)}
                         />
                     </div>
                 </td>
diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx
index 9038ed27a..b8b9f63a9 100644
--- a/src/client/views/nodes/PDFBox.tsx
+++ b/src/client/views/nodes/PDFBox.tsx
@@ -25,7 +25,7 @@ import { CollectionFreeFormView } from '../collections/collectionFreeForm';
 import { CollectionStackingView } from '../collections/CollectionStackingView';
 import { ContextMenu } from '../ContextMenu';
 import { ContextMenuProps } from '../ContextMenuItem';
-import { ViewBoxAnnotatableComponent, ViewBoxInterface } from '../DocComponent';
+import { ViewBoxAnnotatableComponent } from '../DocComponent';
 import { Colors } from '../global/globalEnums';
 import { PDFViewer } from '../pdf/PDFViewer';
 import { PinDocView, PinProps } from '../PinFuncs';
@@ -39,7 +39,7 @@ import './PDFBox.scss';
 import { CreateImage } from './WebBoxRenderer';
 
 @observer
-export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implements ViewBoxInterface {
+export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
     public static LayoutString(fieldKey: string) {
         return FieldView.LayoutString(PDFBox, fieldKey);
     }
@@ -283,7 +283,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
         !this.Document._layout_fitWidth && (this.Document._height = NumCast(this.Document._width) * (nh / nw));
     };
 
-    public search = action((searchString: string, bwd?: boolean, clear: boolean = false) => {
+    override search = action((searchString: string, bwd?: boolean, clear: boolean = false) => {
         if (!this._searching && !clear) {
             this._searching = true;
             setTimeout(() => {
diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx
index 1003bc473..198652310 100644
--- a/src/client/views/nodes/WebBox.tsx
+++ b/src/client/views/nodes/WebBox.tsx
@@ -48,7 +48,7 @@ import './WebBox.scss';
 const { CreateImage } = require('./WebBoxRenderer');
 
 @observer
-export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implements ViewBoxInterface {
+export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
     public static LayoutString(fieldKey: string) {
         return FieldView.LayoutString(WebBox, fieldKey);
     }
@@ -112,7 +112,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
     }
 
     @action
-    search = (searchString: string, bwd?: boolean, clear: boolean = false) => {
+    override search = (searchString: string, bwd?: boolean, clear: boolean = false) => {
         if (!this._searching && !clear) {
             this._searching = true;
             setTimeout(() => {
@@ -1000,7 +1000,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
     };
     _innerCollectionView: CollectionFreeFormView | undefined;
     zoomScaling = () => this._innerCollectionView?.zoomScaling() ?? 1;
-    setInnerContent = (component: ViewBoxInterface) => {
+    setInnerContent = (component: ViewBoxInterface<any>) => {
         this._innerCollectionView = component as CollectionFreeFormView;
     };
 
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 8a4a7718a..321fdbb91 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -43,7 +43,7 @@ import { CollectionStackingView } from '../../collections/CollectionStackingView
 import { CollectionTreeView } from '../../collections/CollectionTreeView';
 import { ContextMenu } from '../../ContextMenu';
 import { ContextMenuProps } from '../../ContextMenuItem';
-import { ViewBoxAnnotatableComponent, ViewBoxInterface } from '../../DocComponent';
+import { ViewBoxAnnotatableComponent } from '../../DocComponent';
 import { Colors } from '../../global/globalEnums';
 import { LightboxView } from '../../LightboxView';
 import { AnchorMenu } from '../../pdf/AnchorMenu';
@@ -73,12 +73,12 @@ import { schema } from './schema_rts';
 import { SummaryView } from './SummaryView';
 // import * as applyDevTools from 'prosemirror-dev-tools';
 
-interface FormattedTextBoxProps extends FieldViewProps {
+export interface FormattedTextBoxProps extends FieldViewProps {
     onBlur?: () => void; // callback when text loses focus
     autoFocus?: boolean; // whether text should get input focus  when created
 }
 @observer
-export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextBoxProps>() implements ViewBoxInterface {
+export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextBoxProps>() {
     public static LayoutString(fieldStr: string) {
         return FieldView.LayoutString(FormattedTextBox, fieldStr);
     }
diff --git a/src/client/views/nodes/formattedText/RichTextRules.ts b/src/client/views/nodes/formattedText/RichTextRules.ts
index 1ff862859..bf11dfe62 100644
--- a/src/client/views/nodes/formattedText/RichTextRules.ts
+++ b/src/client/views/nodes/formattedText/RichTextRules.ts
@@ -7,13 +7,11 @@ import { Id } from '../../../../fields/FieldSymbols';
 import { List } from '../../../../fields/List';
 import { NumCast, StrCast } from '../../../../fields/Types';
 import { Utils } from '../../../../Utils';
-import { DocServer } from '../../../DocServer';
 import { Docs } from '../../../documents/Documents';
-import { DocUtils } from '../../../documents/DocUtils';
 import { CollectionViewType } from '../../../documents/DocumentTypes';
+import { DocUtils } from '../../../documents/DocUtils';
 import { CollectionView } from '../../collections/CollectionView';
 import { ContextMenu } from '../../ContextMenu';
-import { KeyValueBox } from '../KeyValueBox';
 import { FormattedTextBox } from './FormattedTextBox';
 import { wrappingInputRule } from './prosemirrorPatches';
 import { RichTextMenu } from './RichTextMenu';
@@ -311,10 +309,10 @@ export class RichTextRules {
                         }
                     };
                     const getTitledDoc = (title: string) => {
-                        if (!DocServer.FindDocByTitle(title)) {
+                        if (!Doc.FindDocByTitle(title)) {
                             Docs.Create.TextDocument('', { title: title, _width: 400, _layout_fitWidth: true, _layout_autoHeight: true });
                         }
-                        const titledDoc = DocServer.FindDocByTitle(title);
+                        const titledDoc = Doc.FindDocByTitle(title);
                         return titledDoc ? Doc.BestEmbedding(titledDoc) : titledDoc;
                     };
                     const target = getTitledDoc(docTitle);
@@ -337,14 +335,14 @@ export class RichTextRules {
                     const assign = match[4] === ':' ? (match[4] = '') : match[4];
                     const value = match[5];
                     const dataDoc = value === undefined ? !fieldKey.startsWith('_') : !assign?.startsWith('=');
-                    const getTitledDoc = (title: string) => DocServer.FindDocByTitle(title);
+                    const getTitledDoc = (title: string) => Doc.FindDocByTitle(title);
                     // if the value has commas assume its an array (unless it's part of a chat gpt call indicated by '((' )
                     if (value?.includes(',') && !value.startsWith('((')) {
                         const values = value.split(',');
                         const strs = values.some(v => !v.match(/^[-]?[0-9.]$/));
                         this.Document[DocData][fieldKey] = strs ? new List<string>(values) : new List<number>(values.map(v => Number(v)));
                     } else if (value) {
-                        KeyValueBox.SetField(
+                        Doc.SetField(
                             this.Document,
                             fieldKey,
                             assign + value,
@@ -367,7 +365,7 @@ export class RichTextRules {
             // pass the contents between '((' and '))' to chatGPT and append the result
             new InputRule(/(^|[^=])(\(\(.*\)\))$/, (state, match, start, end) => {
                 let count = 0; // ignore first return value which will be the notation that chat is pending a result
-                KeyValueBox.SetField(this.Document, '', match[2], false, (gptval: FieldResult) => {
+                Doc.SetField(this.Document, '', match[2], false, (gptval: FieldResult) => {
                     if (count) {
                         const tr = this.TextBox.EditorView?.state.tr.insertText(' ' + (gptval as string));
                         tr && this.TextBox.EditorView?.dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(end + 2), tr.doc.resolve(end + 2 + (gptval as string).length))));
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index bb6995398..725221a66 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -225,7 +225,17 @@ export class Doc extends RefField {
     @observable public static GuestTarget: Doc | undefined = undefined;
     @observable public static GuestMobile: Doc | undefined = undefined;
     @observable.shallow public static CurrentlyLoading: Doc[] = observable([]);
-    // removes from currently loading display
+    // DocServer api
+    public static FindDocByTitle(title: string) {
+        const foundDocId =
+            title &&
+            Array.from(Object.keys(DocServer.Cache()))
+                .filter(key => DocServer.Cache()[key] instanceof Doc)
+                .find(key => (DocServer.Cache()[key] as Doc).title === title);
+
+        return foundDocId ? (DocServer.Cache()[foundDocId] as Doc) : undefined;
+    }
+    // removes from currently loading doc set
     public static removeCurrentlyLoading(doc: Doc) {
         if (Doc.CurrentlyLoading) {
             const index = Doc.CurrentlyLoading.indexOf(doc);
@@ -238,12 +248,14 @@ export class Doc extends RefField {
             runInAction(() => Doc.CurrentlyLoading.push(doc));
         }
     }
-
+    // LinkManager api
     public static AddLink: (link: Doc, checkExists?: boolean) => void;
     public static DeleteLink: (link: Doc) => void;
     public static Links: (link: Doc | undefined) => Doc[];
     public static getOppositeAnchor: (linkDoc: Doc, anchor: Doc) => Doc | undefined;
-
+    // KeyValue SetField
+    public static SetField: (doc: Doc, key: string, value: string, forceOnDelegate?: boolean, setResult?: (value: FieldResult) => void) => boolean;
+    // UserDoc "API"
     public static get MySharedDocs()          { return DocCast(Doc.UserDoc().mySharedDocs); } // prettier-ignore
     public static get MyUserDocView()         { return DocCast(Doc.UserDoc().myUserDocView); } // prettier-ignore
     public static get MyDockedBtns()          { return DocCast(Doc.UserDoc().myDockedBtns); } // prettier-ignore
-- 
cgit v1.2.3-70-g09d2


From 93ab35c251b399f3e44ab2fa016e2af13df5a53b Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Fri, 3 May 2024 15:28:53 -0400
Subject: fixed not having docs being dragged show up in schema view unless it
 is active.

---
 src/client/views/DocComponent.tsx                  |   1 +
 src/client/views/collections/TabDocView.tsx        |  13 +-
 .../collectionFreeForm/CollectionFreeFormView.tsx  | 148 ++++++++++-----------
 .../collectionSchema/CollectionSchemaView.tsx      |   5 +-
 4 files changed, 77 insertions(+), 90 deletions(-)

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

diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx
index 70b20aec7..f01666d62 100644
--- a/src/client/views/DocComponent.tsx
+++ b/src/client/views/DocComponent.tsx
@@ -53,6 +53,7 @@ export class ViewBoxInterface<P> extends ObservableReactComponent<React.PropsWit
     dragConfig?: (dragData: DragManager.DocumentDragData) => void; // function to setup dragData in custom way (see TreeViews which add a tree view flag)
     incrementalRendering?: () => void;
     infoUI?: () => JSX.Element | null;
+    contentBounds?: () => undefined | { bounds: { x: number; y: number; r: number; b: number }; cx: number; cy: number; width: number; height: number }; // bounds of contents in collection coordinate space (used by TabDocViewThumb)
     screenBounds?: () => Opt<{ left: number; top: number; right: number; bottom: number; transition?: string }>;
     ptToScreen?: (pt: { X: number; Y: number }) => { X: number; Y: number };
     ptFromScreen?: (pt: { X: number; Y: number }) => { X: number; Y: number };
diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx
index 2d8b2564d..008ef6ab4 100644
--- a/src/client/views/collections/TabDocView.tsx
+++ b/src/client/views/collections/TabDocView.tsx
@@ -93,13 +93,10 @@ export class TabMinimapView extends ObservableReactComponent<TabMinimapViewProps
     };
 
     @computed get renderBounds() {
-        const compView = this._props.tabView()?.ComponentView as CollectionFreeFormView;
-        const bounds = compView?.freeformData?.(true)?.bounds;
-        if (!bounds) return undefined;
-        const xbounds = bounds.r - bounds.x;
-        const ybounds = bounds.b - bounds.y;
-        const dim = Math.max(xbounds, ybounds);
-        return { l: bounds.x + xbounds / 2 - dim / 2, t: bounds.y + ybounds / 2 - dim / 2, cx: bounds.x + xbounds / 2, cy: bounds.y + ybounds / 2, dim };
+        const cbounds = this._props.tabView()?.ComponentView?.contentBounds?.();
+        const { width, height, bounds, cx, cy } = cbounds ?? { bounds: undefined, width: 0, height: 0 };
+        const dim = Math.max(width, height);
+        return bounds === undefined ? bounds : { l: bounds.x + width / 2 - dim / 2, t: bounds.y + height / 2 - dim / 2, cx, cy, dim };
     }
     @computed get xPadding() {
         return !this.renderBounds ? 0 : Math.max(0, this._props.PanelWidth() / NumCast(this._props.document._freeform_scale, 1) - 2 * (this.renderBounds.cx - this.renderBounds.l));
@@ -127,8 +124,8 @@ export class TabMinimapView extends ObservableReactComponent<TabMinimapViewProps
             );
     };
     popup = () => {
-        if (!this.renderBounds) return <div />;
         const { renderBounds } = this;
+        if (!renderBounds) return <div />;
         const miniWidth = () => (this._props.PanelWidth() / NumCast(this._props.document._freeform_scale, 1) / renderBounds.dim) * 100;
         const miniHeight = () => (this._props.PanelHeight() / NumCast(this._props.document._freeform_scale, 1) / renderBounds.dim) * 100;
         const miniLeft = () => 50 + ((NumCast(this._props.document._freeform_panX) - renderBounds.cx) / renderBounds.dim) * 100 - miniWidth() / 2;
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 64398a60a..a36c36261 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -88,13 +88,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         return parent instanceof CollectionFreeFormView ? parent : undefined;
     }
 
-    _oldWheel: any;
-    _clusters = new CollectionFreeFormClusters(this);
-    constructor(props: any) {
-        super(props);
-        makeObservable(this);
-    }
-
+    private _clusters = new CollectionFreeFormClusters(this);
+    private _oldWheel: any;
     private _panZoomTransitionTimer: any;
     private _lastX: number = 0;
     private _lastY: number = 0;
@@ -129,16 +124,20 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     @observable _paintedId = 'id' + Utils.GenerateGuid().replace(/-/g, '');
     @observable _keyframeEditing = false;
 
+    constructor(props: any) {
+        super(props);
+        makeObservable(this);
+    }
+
     @computed get layoutEngine() {
         return this._props.layoutEngine?.() || StrCast(this.layoutDoc._layoutEngine);
     }
     @computed get childPointerEvents() {
-        const engine = this._props.layoutEngine?.() || StrCast(this.Document._layoutEngine);
         return SnappingManager.IsResizing
             ? 'none'
             : this._props.childPointerEvents?.() ??
                   (this._props.viewDefDivClick || //
-                  (engine === computePassLayout.name && !this._props.isSelected()) ||
+                  (this.layoutEngine === computePassLayout.name && !this._props.isSelected()) ||
                   this.isContentActive() === false
                       ? 'none'
                       : this._props.pointerEvents?.());
@@ -149,27 +148,9 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         if (viewsMask.length) renderableEles.push(<div className={`collectionfreeformview-mask${this._layoutElements.some(ele => (ele.inkMask ?? 0) > 0) ? '' : '-empty'}`}>{viewsMask}</div>);
         return renderableEles;
     }
-    @computed get fitToContentVals() {
-        const hgt = this.contentBounds.b - this.contentBounds.y;
-        const wid = this.contentBounds.r - this.contentBounds.x;
-        return {
-            bounds: { ...this.contentBounds, cx: this.contentBounds.x + wid / 2, cy: this.contentBounds.y + hgt / 2 },
-            scale:
-                (!this.childDocs.length || !Number.isFinite(hgt) || !Number.isFinite(wid)
-                    ? 1 //
-                    : Math.min(this._props.PanelHeight() / hgt, this._props.PanelWidth() / wid)) / (this._props.NativeDimScaling?.() || 1),
-        };
-    }
     @computed get fitContentsToBox() {
         return (this._props.fitContentsToBox?.() || this.Document._freeform_fitContentsToBox) && !this.isAnnotationOverlay;
     }
-    @computed get contentBounds() {
-        return aggregateBounds(
-            this._layoutElements.filter(e => e.bounds?.width && !e.bounds.z).map(e => e.bounds!),
-            NumCast(this.layoutDoc._xPadding, this._props.xPadding ?? 10),
-            NumCast(this.layoutDoc._yPadding, this._props.yPadding ?? 10)
-        );
-    }
     @computed get nativeWidth() {
         return this._props.NativeWidth?.() || Doc.NativeWidth(this.Document, Cast(this.Document.resolvedDataDoc, Doc, null));
     }
@@ -177,14 +158,13 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         return this._props.NativeHeight?.() || Doc.NativeHeight(this.Document, Cast(this.Document.resolvedDataDoc, Doc, null));
     }
     @computed get centeringShiftX(): number {
-        const scaling = this.nativeDimScaling;
-        return this._props.isAnnotationOverlay || this._props.originTopLeft ? 0 : this._props.PanelWidth() / 2 / scaling; // shift so pan position is at center of window for non-overlay collections
+        return this._props.isAnnotationOverlay || this._props.originTopLeft ? 0 : this._props.PanelWidth() / 2 / this.nativeDimScaling; // shift so pan position is at center of window for non-overlay collections
     }
     @computed get centeringShiftY(): number {
         const panLocAtCenter = !(this._props.isAnnotationOverlay || this._props.originTopLeft);
         if (!panLocAtCenter) return 0;
         const dv = this.DocumentView?.();
-        const aspect = !(this._props.layout_fitWidth?.(this.Document) ?? dv?.layoutDoc.layout_fitWidth) && dv?.nativeWidth && dv?.nativeHeight;
+        const aspect = !this.fitWidth && dv?.nativeWidth && dv?.nativeHeight;
         const scaling = this.nativeDimScaling;
         // if freeform has a native aspect, then the panel height needs to be adjusted to match it
         const height = aspect ? (dv.nativeHeight / dv.nativeWidth) * this._props.PanelWidth() : this._props.PanelHeight();
@@ -202,14 +182,16 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     @computed get backgroundColor() {
         return this._props.styleProvider?.(this.Document, this._props, StyleProp.BackgroundColor);
     }
+    @computed get fitWidth() {
+        return this._props.layout_fitWidth?.(this.Document) ?? this.layoutDoc.layout_fitWidth;
+    }
     @computed get nativeDimScaling() {
         if (this._firstRender || (this._props.isAnnotationOverlay && !this._props.annotationLayerHostsContent)) return 1;
-        const nw = this.nativeWidth;
-        const nh = this.nativeHeight;
-        const hscale = nh ? this._props.PanelHeight() / nh : 1;
-        const wscale = nw ? this._props.PanelWidth() / nw : 1;
-        return wscale < hscale || (this._props.layout_fitWidth?.(this.Document) ?? this.layoutDoc.layout_fitWidth) ? wscale : hscale;
+        const hscale = this._props.PanelHeight() / (this.nativeHeight || this._props.PanelHeight());
+        const wscale = this._props.PanelWidth() / (this.nativeWidth || this._props.PanelWidth());
+        return wscale < hscale || this.fitWidth ? wscale : hscale;
     }
+    @computed get fitContentBounds() { return !this._firstRender && this.fitContentsToBox ? this.contentBounds() : undefined; } // prettier-ignore
     @computed get paintFunc() {
         const field = this.dataDoc[this.fieldKey];
         const paintFunc = StrCast(Field.toJavascriptString(Cast(field, RichTextField, null)?.Text as FieldType)).trim();
@@ -244,23 +226,40 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     };
     getKeyFrameEditing = () => this._keyframeEditing;
 
+    override contentBounds = () => {
+        const { x, y, r, b } = aggregateBounds(
+            this._layoutElements.filter(e => e.bounds?.width && !e.bounds.z).map(e => e.bounds!),
+            NumCast(this.layoutDoc._xPadding, this._props.xPadding ?? 0),
+            NumCast(this.layoutDoc._yPadding, this._props.yPadding ?? 0)
+        );
+        const [width, height] = [r - x, b - y];
+        return {
+            width,
+            height,
+            cx: x + width / 2,
+            cy: y + height / 2,
+            bounds: { x, y, r, b },
+            scale: (!this.childDocs.length || !Number.isFinite(height) || !Number.isFinite(width)
+                ? 1 //
+                : Math.min(this._props.PanelHeight() / height,this._props.PanelWidth() / width )) / (this._props.NativeDimScaling?.() || 1),
+        }; // prettier-ignore
+    };
     onChildClickHandler = () => this._props.childClickScript || ScriptCast(this.Document.onChildClick);
     onChildDoubleClickHandler = () => this._props.childDoubleClickScript || ScriptCast(this.Document.onChildDoubleClick);
     elementFunc = () => this._layoutElements;
     viewTransition = () => (this._panZoomTransition ? '' + this._panZoomTransition : undefined);
     panZoomTransition = () => (this._panZoomTransition ? `transform ${this._panZoomTransition}ms` : Cast(this.layoutDoc._viewTransition, 'string', Cast(this.Document._viewTransition, 'string', null)));
     fitContentOnce = () => {
-        const vals = this.fitToContentVals;
-        this.layoutDoc._freeform_panX = vals.bounds.cx;
-        this.layoutDoc._freeform_panY = vals.bounds.cy;
-        this.layoutDoc._freeform_scale = vals.scale;
+        const { cx, cy, scale } = this.contentBounds(); // prettier-ignore
+        this.layoutDoc._freeform_panX = cx;
+        this.layoutDoc._freeform_panY = cy;
+        this.layoutDoc._freeform_scale = scale;
     };
-    freeformData = (force?: boolean) => (!this._firstRender && (this.fitContentsToBox || force) ? this.fitToContentVals : undefined);
     // freeform_panx, freeform_pany, freeform_scale all attempt to get values first from the layout controller, then from the layout/dataDoc (or template layout doc), and finally from the resolved template data document.
     // this search order, for example, allows icons of cropped images to find the panx/pany/zoom on the cropped image's data doc instead of the usual layout doc because the zoom/panX/panY define the cropped image
-    panX = () => this.freeformData()?.bounds.cx ?? NumCast(this.Document[this.panXFieldKey], NumCast(Cast(this.Document.resolvedDataDoc, Doc, null)?.freeform_panX, 1));
-    panY = () => this.freeformData()?.bounds.cy ?? NumCast(this.Document[this.panYFieldKey], NumCast(Cast(this.Document.resolvedDataDoc, Doc, null)?.freeform_panY, 1));
-    zoomScaling = () => this.freeformData()?.scale ?? NumCast(Doc.Layout(this.Document)[this.scaleFieldKey], 1); // , NumCast(DocCast(this.Document.resolvedDataDoc)?.[this.scaleFieldKey], 1));
+    panX = () => this.fitContentBounds?.cx ?? NumCast(this.Document[this.panXFieldKey], NumCast(Cast(this.Document.resolvedDataDoc, Doc, null)?.freeform_panX, 1));
+    panY = () => this.fitContentBounds?.cy ?? NumCast(this.Document[this.panYFieldKey], NumCast(Cast(this.Document.resolvedDataDoc, Doc, null)?.freeform_panY, 1));
+    zoomScaling = () => this.fitContentBounds?.scale ?? NumCast(Doc.Layout(this.Document)[this.scaleFieldKey], 1); // , NumCast(DocCast(this.Document.resolvedDataDoc)?.[this.scaleFieldKey], 1));
     PanZoomCenterXf = () => (this._props.isAnnotationOverlay && this.zoomScaling() === 1 ? `` : `translate(${this.centeringShiftX}px, ${this.centeringShiftY}px) scale(${this.zoomScaling()}) translate(${-this.panX()}px, ${-this.panY()}px)`);
     ScreenToContentsXf = () => this.screenToFreeformContentsXf.copy();
     getActiveDocuments = () => this.childLayoutPairs.filter(pair => this.isCurrent(pair.layout)).map(pair => pair.layout);
@@ -274,19 +273,16 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         docs.map(doc => DocumentView.getDocumentView(doc, this.DocumentView?.())).forEach(dv => dv && DocumentView.SelectView(dv, true));
     };
     addDocument = (newBox: Doc | Doc[]) => {
-        let retVal = false;
-        if (newBox instanceof Doc) {
-            retVal = this._props.addDocument?.(newBox) || false;
-            if (retVal) {
-                this.bringToFront(newBox);
-                this._clusters.addDocument(newBox);
+        const newBoxes = toList(newBox);
+        const retVal = newBoxes.every(doc => {
+            const added = this._props.addDocument?.(doc);
+            if (added) {
+                this.bringToFront(doc);
+                this._clusters.addDocument(doc);
             }
-        } else {
-            retVal = this._props.addDocument?.(newBox) || false;
-            // bcz: deal with clusters
-        }
+            return added;
+        });
         if (retVal) {
-            const newBoxes = toList(newBox);
             newBoxes.forEach(box => {
                 if (box.activeFrame !== undefined) {
                     const vals = CollectionFreeFormDocumentView.animFields.map(field => box[field.key]);
@@ -856,33 +852,25 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         if (!this.isAnnotationOverlay && clamp) {
             // this section wraps the pan position, horizontally and/or vertically whenever the content is panned out of the viewing bounds
             const docs = this.childLayoutPairs.map(pair => pair.layout).filter(doc => doc instanceof Doc && doc.type !== DocumentType.LINK);
-            const measuredDocs = docs
-                .map(doc => ({ pos: { x: NumCast(doc.x), y: NumCast(doc.y) }, size: { width: NumCast(doc._width), height: NumCast(doc._height) } }))
-                .filter(({ pos, size }) => pos && size)
-                .map(({ pos, size }) => ({ pos: pos!, size: size! }));
+            const measuredDocs = docs.map(doc => ({ x: NumCast(doc.x), y: NumCast(doc.y), width: NumCast(doc._width), height: NumCast(doc._height) }));
             if (measuredDocs.length) {
-                const ranges = measuredDocs.reduce(
-                    (
-                        { xrange, yrange },
-                        { pos, size } // computes range of content
-                    ) => ({
-                        xrange: { min: Math.min(xrange.min, pos.x), max: Math.max(xrange.max, pos.x + (size.width || 0)) },
-                        yrange: { min: Math.min(yrange.min, pos.y), max: Math.max(yrange.max, pos.y + (size.height || 0)) },
-                    }),
-                    {
-                        xrange: { min: this._props.originTopLeft ? 0 : Number.MAX_VALUE, max: -Number.MAX_VALUE },
-                        yrange: { min: this._props.originTopLeft ? 0 : Number.MAX_VALUE, max: -Number.MAX_VALUE },
-                    }
-                );
-                const scaling = this.zoomScaling() * (this._props.NativeDimScaling?.() || 1);
-                const panelWidMax = (this._props.PanelWidth() / scaling) * (this._props.originTopLeft ? 2 / this.nativeDimScaling : 1);
-                const panelWidMin = (this._props.PanelWidth() / scaling) * (this._props.originTopLeft ? 0 : 1);
-                const panelHgtMax = (this._props.PanelHeight() / scaling) * (this._props.originTopLeft ? 2 / this.nativeDimScaling : 1);
-                const panelHgtMin = (this._props.PanelHeight() / scaling) * (this._props.originTopLeft ? 0 : 1);
-                if (ranges.xrange.min >= panX + panelWidMax / 2) panX = ranges.xrange.max + (this._props.originTopLeft ? 0 : panelWidMax / 2);
-                else if (ranges.xrange.max <= panX - panelWidMin / 2) panX = ranges.xrange.min - (this._props.originTopLeft ? panelWidMax / 2 : panelWidMin / 2);
-                if (ranges.yrange.min >= panY + panelHgtMax / 2) panY = ranges.yrange.max + (this._props.originTopLeft ? 0 : panelHgtMax / 2);
-                else if (ranges.yrange.max <= panY - panelHgtMin / 2) panY = ranges.yrange.min - (this._props.originTopLeft ? panelHgtMax / 2 : panelHgtMin / 2);
+                const { originTopLeft } = this._props;
+                // const xrangeMin = Math.min(...measuredDocs.map(doc => doc.x), originTopLeft ? 0 : Number.MAX_VALUE);
+                // const yrangeMin = Math.min(...measuredDocs.map(doc => doc.y), originTopLeft ? 0 : Number.MAX_VALUE);
+                // const xrangeMax = Math.max(...measuredDocs.map(doc => doc.x + doc.width));
+                // const yrangeMax = Math.max(...measuredDocs.map(doc => doc.y + doc.height));
+                const { bounds: { x: xrangeMin, y: yrangeMin, r: xrangeMax, b: yrangeMax } } = this.contentBounds(); // prettier-ignore
+                const nativeScaling = this._props.NativeDimScaling?.() || 1;
+                const scaling = this.zoomScaling() * nativeScaling;
+                const [widScaling, hgtScaling] = [this._props.PanelWidth() / scaling, this._props.PanelHeight() / scaling];
+                const panelWidMax = widScaling * (originTopLeft ? 2 / nativeScaling : 1);
+                const panelHgtMax = hgtScaling * (originTopLeft ? 2 / nativeScaling : 1);
+                const panelWidMin = widScaling * (originTopLeft ? 0 : 1);
+                const panelHgtMin = hgtScaling * (originTopLeft ? 0 : 1);
+                if      (xrangeMin >= panX + panelWidMax / 2) panX = xrangeMax + (originTopLeft ? 0               : panelWidMax / 2);
+                else if (xrangeMax <= panX - panelWidMin / 2) panX = xrangeMin - (originTopLeft ? panelWidMax / 2 : panelWidMin / 2); // prettier-ignore
+                if      (yrangeMin >= panY + panelHgtMax / 2) panY = yrangeMax + (originTopLeft ? 0               : panelHgtMax / 2);
+                else if (yrangeMax <= panY - panelHgtMin / 2) panY = yrangeMin - (originTopLeft ? panelHgtMax / 2 : panelHgtMin / 2); // prettier-ignore
             }
         }
         if (!this.layoutDoc._lockedTransform || LightboxView.LightboxDoc) {
diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx
index 023b72778..b684b65e5 100644
--- a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx
+++ b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx
@@ -1014,9 +1014,10 @@ export class CollectionSchemaView extends CollectionSubView() {
     };
 
     @computed get sortedDocs() {
+        const draggedDocs = this.isContentActive() ? DragManager.docsBeingDragged : [];
         const field = StrCast(this.layoutDoc.sortField);
         const desc = BoolCast(this.layoutDoc.sortDesc); // is this an ascending or descending sort
-        const staticDocs = this.childDocs.filter(d => !DragManager.docsBeingDragged.includes(d));
+        const staticDocs = this.childDocs.filter(d => !draggedDocs.includes(d));
         const docs = !field
             ? staticDocs
             : [...staticDocs].sort((docA, docB) => {
@@ -1030,7 +1031,7 @@ export class CollectionSchemaView extends CollectionSubView() {
                   return out;
               });
 
-        docs.splice(this.rowDropIndex, 0, ...DragManager.docsBeingDragged);
+        docs.splice(this.rowDropIndex, 0, ...draggedDocs);
         return { docs };
     }
 
-- 
cgit v1.2.3-70-g09d2


From b858bd3cad81da41e63b9f8e807e41421ca4aa34 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Wed, 8 May 2024 21:03:08 -0400
Subject: lots of api cleanup and cycle removal

---
 .../apis/google_docs/GooglePhotosClientUtils.ts    |   2 +-
 src/client/documents/DocFromField.ts               |  59 +++------
 src/client/util/CaptureManager.tsx                 |   5 +-
 src/client/util/DocumentManager.ts                 |  15 ++-
 src/client/util/ReplayMovements.ts                 |  51 +++++---
 src/client/util/SnappingManager.ts                 |   3 +
 src/client/util/UndoManager.ts                     |  10 +-
 src/client/views/DashboardView.tsx                 |   5 -
 src/client/views/DocComponent.tsx                  |  62 +---------
 src/client/views/DocumentDecorations.tsx           |   5 +-
 src/client/views/GestureOverlay.tsx                |  17 +--
 src/client/views/GlobalKeyHandler.ts               |   7 +-
 src/client/views/InkingStroke.tsx                  |   3 +-
 src/client/views/LightboxView.tsx                  |  33 +++--
 src/client/views/Main.tsx                          |  99 +++++++++++++--
 src/client/views/MainView.tsx                      |  29 +++--
 src/client/views/ObservableReactComponent.tsx      |  11 ++
 src/client/views/PropertiesDocContextSelector.tsx  |   4 +-
 src/client/views/StyleProvider.tsx                 |  12 +-
 src/client/views/TemplateMenu.tsx                  |   4 +-
 src/client/views/ViewBoxInterface.ts               |  60 +++++++++
 .../views/collections/CollectionDockingView.tsx    |  13 +-
 src/client/views/collections/CollectionMenu.tsx    |   4 +-
 .../views/collections/CollectionNoteTakingView.tsx |   3 +-
 .../collections/CollectionNoteTakingViewColumn.tsx |  54 +--------
 .../collections/CollectionStackedTimeline.tsx      |   5 +-
 .../views/collections/CollectionStackingView.tsx   |   3 +-
 .../views/collections/CollectionTreeView.tsx       |   2 +-
 src/client/views/collections/CollectionView.tsx    |   4 +-
 src/client/views/collections/TabDocView.tsx        |  16 ++-
 .../CollectionFreeFormPannableContents.tsx         |   5 +
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  25 ++--
 .../collectionSchema/SchemaTableCell.tsx           |  70 +++++------
 src/client/views/linking/LinkPopup.tsx             |   3 +-
 .../views/newlightbox/ButtonMenu/ButtonMenu.tsx    |  22 ++--
 .../views/newlightbox/ExploreView/ExploreView.tsx  |  30 ++---
 .../views/newlightbox/Header/LightboxHeader.tsx    |   8 +-
 src/client/views/newlightbox/NewLightboxView.tsx   |  62 ++--------
 .../RecommendationList/RecommendationList.tsx      | 134 ++++++++++-----------
 .../views/nodes/CollectionFreeFormDocumentView.tsx |   2 +-
 src/client/views/nodes/ComparisonBox.tsx           |   2 +-
 src/client/views/nodes/DataVizBox/DataVizBox.tsx   |   4 +-
 src/client/views/nodes/DocumentContentsView.tsx    |  96 +++------------
 src/client/views/nodes/DocumentView.tsx            | 120 ++++++++++--------
 src/client/views/nodes/EquationBox.tsx             |   4 +-
 src/client/views/nodes/FieldView.tsx               |   2 +-
 src/client/views/nodes/KeyValuePair.tsx            |   3 +-
 src/client/views/nodes/LinkBox.tsx                 |   5 +-
 src/client/views/nodes/MapBox/MapBox.tsx           |   4 +-
 .../views/nodes/RecordingBox/RecordingBox.tsx      |  13 +-
 src/client/views/nodes/VideoBox.tsx                |  31 ++---
 src/client/views/nodes/WebBox.tsx                  |   6 +-
 .../views/nodes/formattedText/FormattedTextBox.tsx |  35 ++----
 src/client/views/nodes/trails/PresBox.tsx          |  21 ++--
 src/client/views/search/SearchBox.tsx              |  54 ++++-----
 src/client/views/topbar/TopBar.tsx                 |   4 +-
 src/fields/Doc.ts                                  |   2 +-
 src/fields/InkField.ts                             |  11 ++
 src/fields/Proxy.ts                                |   2 +-
 src/fields/RichTextUtils.ts                        |   2 +-
 src/fields/util.ts                                 |   2 +-
 61 files changed, 664 insertions(+), 720 deletions(-)
 create mode 100644 src/client/views/ViewBoxInterface.ts

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

diff --git a/src/client/apis/google_docs/GooglePhotosClientUtils.ts b/src/client/apis/google_docs/GooglePhotosClientUtils.ts
index fdc185a8e..b238f07e9 100644
--- a/src/client/apis/google_docs/GooglePhotosClientUtils.ts
+++ b/src/client/apis/google_docs/GooglePhotosClientUtils.ts
@@ -331,7 +331,7 @@ export namespace GooglePhotos {
             if (typeof target === 'string') {
                 description = target;
             } else if (target instanceof RichTextField) {
-                description = RichTextUtils.ToPlainText(EditorState.fromJSON(FormattedTextBox.Instance.config, JSON.parse(target.Data)));
+                description = RichTextUtils.ToPlainText(EditorState.fromJSON(new FormattedTextBox({} as any).config, JSON.parse(target.Data)));
             }
             return description;
         };
diff --git a/src/client/documents/DocFromField.ts b/src/client/documents/DocFromField.ts
index 1c0a9755b..b65bbbdf5 100644
--- a/src/client/documents/DocFromField.ts
+++ b/src/client/documents/DocFromField.ts
@@ -1,50 +1,31 @@
-/* eslint-disable prefer-destructuring */
-/* eslint-disable default-param-last */
-/* eslint-disable no-use-before-define */
 import { Doc, DocListCast } from '../../fields/Doc';
 import { InkField } from '../../fields/InkField';
 import { List } from '../../fields/List';
+import { StrCast } from '../../fields/Types';
 import { AudioField, ImageField, PdfField, VideoField } from '../../fields/URLField';
-import { InkingStroke } from '../views/InkingStroke';
-import { CollectionView } from '../views/collections/CollectionView';
-import { AudioBox } from '../views/nodes/AudioBox';
-import { ImageBox } from '../views/nodes/ImageBox';
-import { PDFBox } from '../views/nodes/PDFBox';
-import { VideoBox } from '../views/nodes/VideoBox';
-import { FormattedTextBox } from '../views/nodes/formattedText/FormattedTextBox';
 import { Docs, DocumentOptions } from './Documents';
 
+/**
+ * Changes the field key in the doc's layout string to be the specified field
+ */
+export function ResetLayoutFieldKey(doc: Doc, fieldKey: string) {
+    doc.layout = StrCast(doc.layout).replace(/={'.*'}/, `={'${fieldKey}'}`);
+    return doc;
+}
 export function DocumentFromField(target: Doc, fieldKey: string, proto?: Doc, options?: DocumentOptions): Doc | undefined {
-    let created: Doc | undefined;
     const field = target[fieldKey];
     const resolved = options ?? {};
-    if (field instanceof ImageField) {
-        created = Docs.Create.ImageDocument(field.url.href, resolved);
-        created.layout = ImageBox.LayoutString(fieldKey);
-    } else if (field instanceof Doc) {
-        created = field;
-    } else if (field instanceof VideoField) {
-        created = Docs.Create.VideoDocument(field.url.href, resolved);
-        created.layout = VideoBox.LayoutString(fieldKey);
-    } else if (field instanceof PdfField) {
-        created = Docs.Create.PdfDocument(field.url.href, resolved);
-        created.layout = PDFBox.LayoutString(fieldKey);
-    } else if (field instanceof AudioField) {
-        created = Docs.Create.AudioDocument(field.url.href, resolved);
-        created.layout = AudioBox.LayoutString(fieldKey);
-    } else if (field instanceof InkField) {
-        created = Docs.Create.InkDocument(field.inkData, resolved);
-        created.layout = InkingStroke.LayoutString(fieldKey);
-    } else if (field instanceof List && field[0] instanceof Doc) {
-        created = Docs.Create.StackingDocument(DocListCast(field), resolved);
-        created.layout = CollectionView.LayoutString(fieldKey);
-    } else {
-        created = Docs.Create.TextDocument('', { ...{ _width: 200, _height: 25, _layout_autoHeight: true }, ...resolved });
-        created.layout = FormattedTextBox.LayoutString(fieldKey);
-    }
-    if (created) {
-        created.title = fieldKey;
-        proto && created.proto && (created.proto = Doc.GetProto(proto));
-    }
+    const nonDocFieldToDoc = () => {
+        if (field instanceof ImageField) return Docs.Create.ImageDocument(field.url.href, resolved);
+        if (field instanceof VideoField) return Docs.Create.VideoDocument(field.url.href, resolved);
+        if (field instanceof PdfField) return Docs.Create.PdfDocument(field.url.href, resolved);
+        if (field instanceof AudioField) return Docs.Create.AudioDocument(field.url.href, resolved);
+        if (field instanceof InkField) return Docs.Create.InkDocument(field.inkData, resolved);
+        if (field instanceof List && field[0] instanceof Doc) return Docs.Create.StackingDocument(DocListCast(field), resolved);
+        return Docs.Create.TextDocument('', { ...{ _width: 200, _height: 25, _layout_autoHeight: true }, ...resolved });
+    };
+    const created = field instanceof Doc ? field : ResetLayoutFieldKey(nonDocFieldToDoc(), fieldKey);
+    created.title = fieldKey;
+    proto && created.proto && (created.proto = Doc.GetProto(proto));
     return created;
 }
diff --git a/src/client/util/CaptureManager.tsx b/src/client/util/CaptureManager.tsx
index 4fd934774..253cdd8b5 100644
--- a/src/client/util/CaptureManager.tsx
+++ b/src/client/util/CaptureManager.tsx
@@ -7,10 +7,9 @@ import * as React from 'react';
 import { addStyleSheet } from '../../ClientUtils';
 import { Doc } from '../../fields/Doc';
 import { DocCast, StrCast } from '../../fields/Types';
-import { LightboxView } from '../views/LightboxView';
 import { MainViewModal } from '../views/MainViewModal';
-import './CaptureManager.scss';
 import { DocumentView } from '../views/nodes/DocumentView';
+import './CaptureManager.scss';
 
 @observer
 export class CaptureManager extends React.Component<{}> {
@@ -79,7 +78,7 @@ export class CaptureManager extends React.Component<{}> {
                     <div
                         className="save"
                         onClick={() => {
-                            LightboxView.Instance.SetLightboxDoc(this._document);
+                            DocumentView.SetLightboxDoc(this._document);
                             this.close();
                         }}>
                         Save
diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts
index 5bcac7330..97051207b 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -7,7 +7,6 @@ import { listSpec } from '../../fields/Schema';
 import { Cast, DocCast, NumCast, StrCast } from '../../fields/Types';
 import { AudioField } from '../../fields/URLField';
 import { CollectionViewType } from '../documents/DocumentTypes';
-import { LightboxView } from '../views/LightboxView';
 import { DocumentView, DocumentViewInternal } from '../views/nodes/DocumentView';
 import { FocusViewOptions } from '../views/nodes/FocusViewOptions';
 import { OpenWhere } from '../views/nodes/OpenWhere';
@@ -25,7 +24,7 @@ export class DocumentManager {
     // global holds all of the nodes (regardless of which collection they're in)
     @observable private _documentViews = new Set<DocumentView>();
     @computed public get DocumentViews() {
-        return Array.from(this._documentViews).filter(view => (!view.ComponentView?.dontRegisterView?.() && !LightboxView.LightboxDoc) || LightboxView.Contains(view));
+        return Array.from(this._documentViews).filter(view => (!view.ComponentView?.dontRegisterView?.() && !DocumentView.LightboxDoc()) || DocumentView.LightboxContains(view));
     }
     public AddDocumentView(dv: DocumentView) {
         this._documentViews.add(dv);
@@ -68,7 +67,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 = LightboxView.LightboxDoc ? this.getLightboxDocumentView(doc) : this.getDocumentView(doc);
+            const dv = DocumentView.LightboxDoc() ? this.getLightboxDocumentView(doc) : this.getDocumentView(doc);
             this._viewRenderedCbs.push({ doc, func });
             if (dv) {
                 this.callAddViewFuncs(dv);
@@ -141,18 +140,18 @@ export class DocumentManager {
 
     public getLightboxDocumentView = (toFind: Doc): DocumentView | undefined => {
         const views: DocumentView[] = [];
-        DocumentManager.Instance.DocumentViews.forEach(view => LightboxView.Contains(view) && Doc.AreProtosEqual(view.Document, toFind) && views.push(view));
+        DocumentManager.Instance.DocumentViews.forEach(view => DocumentView.LightboxContains(view) && Doc.AreProtosEqual(view.Document, toFind) && views.push(view));
         return views?.find(view => view.ContentDiv?.getBoundingClientRect().width /* && view._props.focus !== returnFalse) || views?.find(view => view._props.focus !== returnFalse */) || (views.length ? views[0] : undefined);
     };
     public getFirstDocumentView = (toFind: Doc): DocumentView | undefined => {
-        if (LightboxView.LightboxDoc) return DocumentManager.Instance.getLightboxDocumentView(toFind);
+        if (DocumentView.LightboxDoc()) return DocumentManager.Instance.getLightboxDocumentView(toFind);
         const views = this.getDocumentViews(toFind); // .filter(view => view.Document !== originatingDoc);
         return views?.find(view => view.ContentDiv?.getBoundingClientRect().width /* && view._props.focus !== returnFalse) || views?.find(view => view._props.focus !== returnFalse */) || (views.length ? views[0] : undefined);
     };
     public getDocumentViews(toFind: Doc): DocumentView[] {
         const toReturn: DocumentView[] = [];
-        const docViews = DocumentManager.Instance.DocumentViews.filter(view => !LightboxView.Contains(view));
-        const lightViews = DocumentManager.Instance.DocumentViews.filter(view => LightboxView.Contains(view));
+        const docViews = DocumentManager.Instance.DocumentViews.filter(view => !DocumentView.LightboxContains(view));
+        const lightViews = DocumentManager.Instance.DocumentViews.filter(view => DocumentView.LightboxContains(view));
 
         // heuristic to return the "best" documents first:
         //   choose a document in the lightbox first
@@ -262,7 +261,7 @@ export class DocumentManager {
                     return;
                 }
                 options.didMove = true;
-                (!LightboxView.LightboxDoc && docContextPath.some(doc => DocumentView.activateTabView(doc))) || DocumentViewInternal.addDocTabFunc(docContextPath[0], options.openLocation ?? OpenWhere.addRight);
+                (!DocumentView.LightboxDoc() && docContextPath.some(doc => DocumentView.activateTabView(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/util/ReplayMovements.ts b/src/client/util/ReplayMovements.ts
index 530fcf211..c5afe549c 100644
--- a/src/client/util/ReplayMovements.ts
+++ b/src/client/util/ReplayMovements.ts
@@ -1,16 +1,17 @@
 import { IReactionDisposer, makeObservable, observable, reaction } from 'mobx';
 import { Doc, IdToDoc } from '../../fields/Doc';
-import { CollectionDockingView } from '../views/collections/CollectionDockingView';
 import { CollectionFreeFormView } from '../views/collections/collectionFreeForm';
+import { DocumentView } from '../views/nodes/DocumentView';
 import { OpenWhereMod } from '../views/nodes/OpenWhere';
-import { VideoBox } from '../views/nodes/VideoBox';
+import { SnappingManager } from './SnappingManager';
 import { Movement, Presentation } from './TrackMovements';
-import { DocumentView } from '../views/nodes/DocumentView';
+import { ViewBoxInterface } from '../views/ViewBoxInterface';
+import { StrCast } from '../../fields/Types';
 
 export class ReplayMovements {
     private timers: NodeJS.Timeout[] | null;
     private videoBoxDisposeFunc: IReactionDisposer | null;
-    private videoBox: VideoBox | null;
+    private videoBox: ViewBoxInterface<any> | null;
     private isPlaying: boolean;
 
     // create static instance and getter for global use
@@ -29,6 +30,22 @@ export class ReplayMovements {
         this.videoBoxDisposeFunc = null;
         this.videoBox = null;
         this.isPlaying = false;
+
+        reaction(
+            () => SnappingManager.UserPanned,
+            () => {
+                if (Doc.UserDoc()?.presentationMode === 'watching') this.pauseFromInteraction();
+            }
+        );
+        reaction(
+            () => DocumentView.Selected().slice(),
+            selviews => {
+                const selVideo = selviews.find(dv => dv.ComponentView?.playFrom);
+                if (selVideo?.ComponentView?.Play) {
+                    this.setVideoBox(selVideo.ComponentView);
+                } else this.removeVideoBox();
+            }
+        );
     }
 
     // pausing movements will dispose all timers that are planned to replay the movements
@@ -45,18 +62,16 @@ export class ReplayMovements {
         this.timers?.map(timer => clearTimeout(timer));
     };
 
-    setVideoBox = async (videoBox: VideoBox) => {
-        // console.info('setVideoBox', videoBox);
+    setVideoBox = async (videoBox: ViewBoxInterface<any>) => {
         if (this.videoBox !== null) {
             console.warn('setVideoBox on already videoBox');
         }
-        if (this.videoBoxDisposeFunc !== null) {
-            console.warn('setVideoBox on already videoBox dispose func');
-            this.videoBoxDisposeFunc();
-        }
+        this.videoBoxDisposeFunc?.();
+
+        const data = StrCast(videoBox.dataDoc?.[videoBox.fieldKey + '_presentation']);
+        const presentation = data ? JSON.parse(data) : null;
 
-        const { presentation } = videoBox;
-        if (presentation == null) {
+        if (presentation === null) {
             console.warn('setVideoBox on null videoBox presentation');
             return;
         }
@@ -64,18 +79,14 @@ export class ReplayMovements {
         this.loadPresentation(presentation);
 
         this.videoBoxDisposeFunc = reaction(
-            () => ({ playing: videoBox._playing, timeViewed: videoBox.player?.currentTime || 0 }),
+            () => ({ playing: videoBox.IsPlaying?.(), timeViewed: videoBox.PlayerTime?.() || 0 }),
             ({ playing, timeViewed }) => (playing ? this.playMovements(presentation, timeViewed) : this.pauseMovements())
         );
         this.videoBox = videoBox;
     };
 
     removeVideoBox = () => {
-        if (this.videoBoxDisposeFunc == null) {
-            console.warn('removeVideoBox on null videoBox');
-            return;
-        }
-        this.videoBoxDisposeFunc();
+        this.videoBoxDisposeFunc?.();
 
         this.videoBox = null;
         this.videoBoxDisposeFunc = null;
@@ -83,7 +94,7 @@ export class ReplayMovements {
 
     // should be called from interacting with the screen
     pauseFromInteraction = () => {
-        this.videoBox?.Pause();
+        this.videoBox?.Pause?.();
 
         this.pauseMovements();
     };
@@ -117,7 +128,7 @@ export class ReplayMovements {
             return undefined;
         }
         // console.log('openTab', docId, doc);
-        CollectionDockingView.AddSplit(doc, OpenWhereMod.right);
+        DocumentView.addSplit(doc, OpenWhereMod.right);
         const docView = DocumentView.getDocumentView(doc);
         // BUG - this returns undefined if the doc is already open
         return docView?.ComponentView as CollectionFreeFormView;
diff --git a/src/client/util/SnappingManager.ts b/src/client/util/SnappingManager.ts
index 6789c2ab8..1337d271f 100644
--- a/src/client/util/SnappingManager.ts
+++ b/src/client/util/SnappingManager.ts
@@ -26,6 +26,7 @@ export class SnappingManager {
     @observable _serverVersion: string = '';
     @observable _lastBtnId: string = '';
     @observable _propertyWid: number = 0;
+    @observable _printToConsole: boolean = false;
 
     private constructor() {
         SnappingManager._manager = this;
@@ -55,6 +56,7 @@ export class SnappingManager {
     public static get ServerVersion()  { return this.Instance._serverVersion; } // prettier-ignore
     public static get LastPressedBtn() { return this.Instance._lastBtnId; } // prettier-ignore
     public static get PropertiesWidth(){ return this.Instance._propertyWid; } // prettier-ignore
+    public static get PrintToConsole() { return this.Instance._printToConsole; } // prettier-ignore
 
     public static SetShiftKey       = (down: boolean) => runInAction(() => {this.Instance._shiftKey = down}); // prettier-ignore
     public static SetCtrlKey        = (down: boolean) => runInAction(() => {this.Instance._ctrlKey = down}); // prettier-ignore
@@ -69,6 +71,7 @@ export class SnappingManager {
     public static SetServerVersion  = (version:string) =>runInAction(() => {this.Instance._serverVersion = version}); // prettier-ignore
     public static SetLastPressedBtn = (id:string)      =>runInAction(() => {this.Instance._lastBtnId = id}); // prettier-ignore
     public static SetPropertiesWidth= (wid:number)     =>runInAction(() => {this.Instance._propertyWid = wid}); // prettier-ignore
+    public static SetPrintToConsole = (state:boolean)  =>runInAction(() => {this.Instance._printToConsole = state}); // prettier-ignore
 
     public static userColor: string | undefined;
     public static userVariantColor: string | undefined;
diff --git a/src/client/util/UndoManager.ts b/src/client/util/UndoManager.ts
index 956c0e674..534ffd2c8 100644
--- a/src/client/util/UndoManager.ts
+++ b/src/client/util/UndoManager.ts
@@ -3,9 +3,7 @@
 import { action, observable, runInAction } from 'mobx';
 import { Without } from '../../Utils';
 import { RichTextField } from '../../fields/RichTextField';
-
-// eslint-disable-next-line prefer-const
-let printToConsole = false; // Doc.MyDockedBtns.linearView_IsOpen
+import { SnappingManager } from './SnappingManager';
 
 function getBatchName(target: any, key: string | symbol): string {
     const keyName = key.toString();
@@ -108,7 +106,7 @@ export namespace UndoManager {
 
     export function AddEvent(event: UndoEvent, value?: any): void {
         if (currentBatch && batchCounter.get() && !undoing) {
-            printToConsole &&
+            SnappingManager.PrintToConsole &&
                 console.log(
                     '                         '.slice(0, batchCounter.get()) +
                         'UndoEvent : ' +
@@ -183,7 +181,7 @@ export namespace UndoManager {
     }
 
     export function StartBatch(batchName: string): Batch {
-        printToConsole && console.log('                          '.slice(0, batchCounter.get()) + 'Start ' + batchCounter + ' ' + batchName);
+        SnappingManager.PrintToConsole && console.log('                          '.slice(0, batchCounter.get()) + 'Start ' + batchCounter + ' ' + batchName);
         runInAction(() => batchCounter.set(batchCounter.get() + 1));
         if (currentBatch === undefined) {
             currentBatch = [];
@@ -193,7 +191,7 @@ export namespace UndoManager {
 
     const EndBatch = action((batchName: string, cancel: boolean = false) => {
         runInAction(() => batchCounter.set(batchCounter.get() - 1));
-        printToConsole && console.log('                          '.slice(0, batchCounter.get()) + 'End ' + batchName + ' (' + (currentBatch?.length ?? 0) + ')');
+        SnappingManager.PrintToConsole && console.log('                          '.slice(0, batchCounter.get()) + 'End ' + batchName + ' (' + (currentBatch?.length ?? 0) + ')');
         if (batchCounter.get() === 0 && currentBatch?.length) {
             if (!cancel) {
                 undoStack.push(currentBatch);
diff --git a/src/client/views/DashboardView.tsx b/src/client/views/DashboardView.tsx
index fc6a330f7..b7383a37e 100644
--- a/src/client/views/DashboardView.tsx
+++ b/src/client/views/DashboardView.tsx
@@ -305,11 +305,6 @@ export class DashboardView extends ObservableReactComponent<{}> {
                 });
             if (state.readonly === true || state.readonly === null) {
                 DocServer.Control.makeReadOnly();
-                // } else if (state.safe) {
-                //     if (!state.nro) {
-                //         DocServer.Control.makeReadOnly();
-                //     }
-                //     CollectionView.SetSafeMode(true);
             } else if (state.nro || state.nro === null || state.readonly === false) {
                 /* empty */
             } else if (doc.readOnly) {
diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx
index f01666d62..94e84e647 100644
--- a/src/client/views/DocComponent.tsx
+++ b/src/client/views/DocComponent.tsx
@@ -2,66 +2,16 @@ import { action, computed, makeObservable, observable } from 'mobx';
 import * as React from 'react';
 import { returnFalse } from '../../ClientUtils';
 import { DateField } from '../../fields/DateField';
-import { Doc, DocListCast, FieldType, Opt } from '../../fields/Doc';
-import { AclAdmin, AclAugment, AclEdit, AclPrivate, AclReadonly, DocData, DocViews } from '../../fields/DocSymbols';
+import { Doc, DocListCast, Opt } from '../../fields/Doc';
+import { AclAdmin, AclAugment, AclEdit, AclPrivate, AclReadonly, DocData } from '../../fields/DocSymbols';
 import { List } from '../../fields/List';
-import { RefField } from '../../fields/RefField';
 import { toList } from '../../fields/Types';
 import { GetEffectiveAcl, inheritParentAcls } from '../../fields/util';
 import { DocumentType } from '../documents/DocumentTypes';
-import { DragManager } from '../util/DragManager';
 import { ObservableReactComponent } from './ObservableReactComponent';
-import { PinProps } from './PinFuncs';
-import { DocumentView } from './nodes/DocumentView';
-import { FocusViewOptions } from './nodes/FocusViewOptions';
+import { ViewBoxInterface } from './ViewBoxInterface';
 import { FieldViewProps } from './nodes/FieldView';
-import { OpenWhere } from './nodes/OpenWhere';
-// import { DocUtils } from '../documents/Documents';
 
-/**
- * Shared interface among all viewBox'es (ie, react classes that render the contents of a Doc)
- * Many of these methods only make sense for specific viewBox'es, but they should be written to
- * be as general as possible
- */
-export class ViewBoxInterface<P> extends ObservableReactComponent<React.PropsWithChildren<P>> {
-    promoteCollection?: () => void; // moves contents of collection to parent
-    updateIcon?: () => void; // updates the icon representation of the document
-    getAnchor?: (addAsAnnotation: boolean, pinData?: PinProps) => Doc; // returns an Anchor Doc that represents the current state of the doc's componentview (e.g., the current playhead location of a an audio/video box)
-    restoreView?: (viewSpec: Doc) => boolean;
-    scrollPreview?: (docView: DocumentView, doc: Doc, focusSpeed: number, options: FocusViewOptions) => Opt<number>; // returns the duration of the focus
-    brushView?: (view: { width: number; height: number; panX: number; panY: number }, transTime: number, holdTime: number) => void; // highlight a region of a view (used by freeforms)
-    getView?: (doc: Doc, options: FocusViewOptions) => Promise<Opt<DocumentView>>; // returns a nested DocumentView for the specified doc or undefined
-    addDocTab?: (doc: Doc, where: OpenWhere) => boolean; // determines how to add a document - used in following links to open the target ina local lightbox
-    addDocument?: (doc: Doc | Doc[], annotationKey?: string) => boolean; // add a document (used only by collections)
-    removeDocument?: (doc: Doc | Doc[], annotationKey?: string, leavePushpin?: boolean, dontAddToRemoved?: boolean) => boolean; // add a document (used only by collections)
-    select?: (ctrlKey: boolean, shiftKey: boolean) => void;
-    focus?: (textAnchor: Doc, options: FocusViewOptions) => Opt<number>;
-    viewTransition?: () => Opt<string>; // duration of a view transition animation
-    isAnyChildContentActive?: () => boolean; // is any child content of the document active
-    onClickScriptDisable?: () => 'never' | 'always'; // disable click scripts : never, always, or undefined = only when selected
-    getKeyFrameEditing?: () => boolean; // whether the document is in keyframe editing mode (if it is, then all hidden documents that are not active at the keyframe time will still be shown)
-    setKeyFrameEditing?: (set: boolean) => void; // whether the document is in keyframe editing mode (if it is, then all hidden documents that are not active at the keyframe time will still be shown)
-    playTrail?: (docs: Doc[]) => void;
-    playFrom?: (time: number, endTime?: number) => void;
-    Pause?: () => void; // pause a media document (eg, audio/video)
-    IsPlaying?: () => boolean; // is a media document playing
-    TogglePause?: (keep?: boolean) => void; // toggle media document playing state
-    setFocus?: () => void; // sets input focus to the componentView
-    setData?: (data: FieldType | Promise<RefField | undefined>) => boolean;
-    componentUI?: (boundsLeft: number, boundsTop: number) => JSX.Element | null;
-    dragStarting?: (snapToDraggedDoc: boolean, showGroupDragTarget: boolean, visited: Set<Doc>) => void;
-    dragConfig?: (dragData: DragManager.DocumentDragData) => void; // function to setup dragData in custom way (see TreeViews which add a tree view flag)
-    incrementalRendering?: () => void;
-    infoUI?: () => JSX.Element | null;
-    contentBounds?: () => undefined | { bounds: { x: number; y: number; r: number; b: number }; cx: number; cy: number; width: number; height: number }; // bounds of contents in collection coordinate space (used by TabDocViewThumb)
-    screenBounds?: () => Opt<{ left: number; top: number; right: number; bottom: number; transition?: string }>;
-    ptToScreen?: (pt: { X: number; Y: number }) => { X: number; Y: number };
-    ptFromScreen?: (pt: { X: number; Y: number }) => { X: number; Y: number };
-    snapPt?: (pt: { X: number; Y: number }, excludeSegs?: number[]) => { nearestPt: { X: number; Y: number }; distance: number };
-    search?: (str: string, bwd?: boolean, clear?: boolean) => boolean;
-    dontRegisterView?: () => boolean; // KeyValueBox's don't want to register their views
-    isUnstyledView?: () => boolean; // SchemaView and KeyValue are unstyled -- not titles, no opacity, no animations
-}
 /**
  * DocComponent returns a React base class used by Doc views with accessors for unpacking the Document,layoutDoc, and dataDoc's
  *  (note: this should not be used for the 'Box' views that render the contents of Doc views)
@@ -238,11 +188,7 @@ export function ViewBoxAnnotatableComponent<P extends FieldViewProps>() {
                         Doc.RemoveEmbedding(rdoc, rdoc);
                     }
                 });
-                if (targetDataDoc.isGroup && DocListCast(targetDataDoc[annotationKey ?? this.annotationKey]).length < 2) {
-                    Array.from(targetDataDoc[DocViews])[0]?.ComponentView?.promoteCollection?.();
-                } else {
-                    this.isAnyChildContentActive() && this._props.select(false);
-                }
+                this.isAnyChildContentActive() && this._props.select(false);
                 return true;
             }
 
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 4262c2d57..fd44909c0 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -25,7 +25,6 @@ import { DocumentButtonBar } from './DocumentButtonBar';
 import './DocumentDecorations.scss';
 import { InkStrokeProperties } from './InkStrokeProperties';
 import { InkingStroke } from './InkingStroke';
-import { LightboxView } from './LightboxView';
 import { ObservableReactComponent } from './ObservableReactComponent';
 import { CollectionDockingView } from './collections/CollectionDockingView';
 import { CollectionFreeFormView } from './collections/collectionFreeForm';
@@ -95,7 +94,7 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
     @computed get ClippedBounds() {
         const bounds = { ...this.Bounds };
         const leftBounds = this._props.boundsLeft;
-        const topBounds = LightboxView.LightboxDoc ? 0 : this._props.boundsTop;
+        const topBounds = DocumentView.LightboxDoc() ? 0 : this._props.boundsTop;
         bounds.x = Math.max(leftBounds, bounds.x - this._resizeBorderWidth / 2) + this._resizeBorderWidth / 2;
         bounds.y = Math.max(topBounds, bounds.y - this._resizeBorderWidth / 2 - this._titleHeight) + this._resizeBorderWidth / 2 + this._titleHeight;
         const borderRadiusDraggerWidth = 15;
@@ -277,7 +276,7 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
                     openDoc = Doc.GetEmbeddings(openDoc).find(embedding => !embedding.embedContainer) ?? Doc.MakeEmbedding(openDoc);
                     Doc.deiconifyView(openDoc);
                 }
-                LightboxView.Instance.SetLightboxDoc(openDoc, undefined, selectedDocs.slice(1));
+                DocumentView.SetLightboxDoc(openDoc, undefined, selectedDocs.slice(1));
             }
         }
         DocumentView.DeselectAll();
diff --git a/src/client/views/GestureOverlay.tsx b/src/client/views/GestureOverlay.tsx
index 7246f0ba0..2f26bdaef 100644
--- a/src/client/views/GestureOverlay.tsx
+++ b/src/client/views/GestureOverlay.tsx
@@ -21,7 +21,7 @@ import {
     SetActiveInkColor,
     SetActiveInkWidth,
 } from '../../fields/Doc';
-import { InkData, InkTool } from '../../fields/InkField';
+import { InkData, InkField, InkTool } from '../../fields/InkField';
 import { NumCast } from '../../fields/Types';
 // import MobileInkOverlay from '../../mobile/MobileInkOverlay';
 import { Gestures } from '../../pen-gestures/GestureTypes';
@@ -362,7 +362,7 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
                     detail: {
                         points,
                         gesture,
-                        bounds: GestureOverlay.getBounds(points),
+                        bounds: InkField.getBounds(points),
                         text,
                     },
                 })
@@ -370,19 +370,8 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
         );
     };
 
-    public static getBounds = (stroke: InkData, pad?: boolean) => {
-        const padding = pad ? [-20000, 20000] : [];
-        const xs = [...padding, ...stroke.map(p => p.X)];
-        const ys = [...padding, ...stroke.map(p => p.Y)];
-        const right = Math.max(...xs);
-        const left = Math.min(...xs);
-        const bottom = Math.max(...ys);
-        const top = Math.min(...ys);
-        return { right, left, bottom, top, width: right - left, height: bottom - top };
-    };
-
     @computed get svgBounds() {
-        return GestureOverlay.getBounds(this._points);
+        return InkField.getBounds(this._points);
     }
 
     get elements() {
diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts
index c73da35bc..18ec0b6a9 100644
--- a/src/client/views/GlobalKeyHandler.ts
+++ b/src/client/views/GlobalKeyHandler.ts
@@ -15,7 +15,6 @@ import { UndoManager, undoable } from '../util/UndoManager';
 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';
@@ -146,7 +145,7 @@ export class KeyManager {
                     }
                     if (doDeselect) {
                         DocumentView.DeselectAll();
-                        LightboxView.Instance.SetLightboxDoc(undefined);
+                        DocumentView.SetLightboxDoc(undefined);
                     }
                     // DictationManager.Controls.stop();
                     GoogleAuthenticationManager.Instance.cancel();
@@ -163,8 +162,8 @@ export class KeyManager {
             case 'delete':
             case 'backspace':
                 if (document.activeElement?.tagName !== 'INPUT' && document.activeElement?.tagName !== 'TEXTAREA') {
-                    if (LightboxView.LightboxDoc) {
-                        LightboxView.Instance.SetLightboxDoc(undefined);
+                    if (DocumentView.LightboxDoc()) {
+                        DocumentView.SetLightboxDoc(undefined);
                         DocumentView.DeselectAll();
                     } else if (!window.getSelection()?.toString()) DocumentDecorations.Instance.onCloseClick(true);
                     return { stopPropagation: true, preventDefault: true };
diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx
index f497ca447..55f28f415 100644
--- a/src/client/views/InkingStroke.tsx
+++ b/src/client/views/InkingStroke.tsx
@@ -35,7 +35,8 @@ import { InteractionUtils } from '../util/InteractionUtils';
 import { SnappingManager } from '../util/SnappingManager';
 import { UndoManager } from '../util/UndoManager';
 import { ContextMenu } from './ContextMenu';
-import { ViewBoxAnnotatableComponent, ViewBoxInterface } from './DocComponent';
+import { ViewBoxInterface } from './ViewBoxInterface';
+import { ViewBoxAnnotatableComponent } from './DocComponent';
 import { Colors } from './global/globalEnums';
 import { InkControlPtHandles, InkEndPtHandles } from './InkControlPtHandles';
 import './InkStroke.scss';
diff --git a/src/client/views/LightboxView.tsx b/src/client/views/LightboxView.tsx
index 12d899388..269f4fa83 100644
--- a/src/client/views/LightboxView.tsx
+++ b/src/client/views/LightboxView.tsx
@@ -19,14 +19,16 @@ import { GestureOverlay } from './GestureOverlay';
 import './LightboxView.scss';
 import { ObservableReactComponent } from './ObservableReactComponent';
 import { DefaultStyleProvider, wavyBorderPath } from './StyleProvider';
-import { CollectionDockingView } from './collections/CollectionDockingView';
 import { DocumentView } from './nodes/DocumentView';
 import { OpenWhere, OpenWhereMod } from './nodes/OpenWhere';
+import { ScriptingGlobals } from '../util/ScriptingGlobals';
+import { OverlayView } from './OverlayView';
 
 interface LightboxViewProps {
     PanelWidth: number;
     PanelHeight: number;
     maxBorder: number[];
+    addSplit: (document: Doc, pullSide: OpenWhereMod, stack?: any, panelName?: string | undefined, keyValue?: boolean | undefined) => boolean;
 }
 
 const savedKeys = ['freeform_panX', 'freeform_panY', 'freeform_scale', 'layout_scrollTop', 'layout_fieldKey'];
@@ -39,7 +41,7 @@ export class LightboxView extends ObservableReactComponent<LightboxViewProps> {
      * @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
+    public static LightboxDoc = () => LightboxView.Instance?._doc;
     // eslint-disable-next-line no-use-before-define
     static Instance: LightboxView;
     private _path: {
@@ -65,10 +67,20 @@ export class LightboxView extends ObservableReactComponent<LightboxViewProps> {
         super(props);
         makeObservable(this);
         LightboxView.Instance = this;
+        DocumentView._setLightboxDoc = this.SetLightboxDoc;
+        DocumentView._lightboxContains = LightboxView.Contains;
+        DocumentView._lightboxDoc = LightboxView.LightboxDoc;
     }
-
+    /**
+     * Sets the root Doc to render in the lightbox view.
+     * @param doc
+     * @param target a Doc within 'doc' to focus on (useful for freeform collections)
+     * @param future a list of Docs to step through with the arrow buttons of the lightbox
+     * @param layoutTemplate a template to apply to 'doc' to render it.
+     * @returns success flag which is currently always true
+     */
     @action
-    public SetLightboxDoc(doc: Opt<Doc>, target?: Doc, future?: Doc[], layoutTemplate?: Doc | string) {
+    public SetLightboxDoc = (doc: Opt<Doc>, target?: Doc, future?: Doc[], layoutTemplate?: Doc | string) => {
         const lightDoc = this._doc;
         lightDoc &&
             lightDoc !== doc &&
@@ -110,7 +122,7 @@ export class LightboxView extends ObservableReactComponent<LightboxViewProps> {
         this._docTarget = target ?? doc;
 
         return true;
-    }
+    };
 
     public AddDocTab = (docs: Doc | Doc[], location: OpenWhere, layoutTemplate?: Doc | string) => {
         const doc = toList(docs).lastElement();
@@ -183,7 +195,7 @@ export class LightboxView extends ObservableReactComponent<LightboxViewProps> {
         const lightDoc = this._docTarget ?? this._doc;
         if (lightDoc) {
             Doc.RemoveDocFromList(Doc.MyRecentlyClosed, 'data', lightDoc);
-            CollectionDockingView.AddSplit(lightDoc, OpenWhereMod.none);
+            this._props.addSplit(lightDoc, OpenWhereMod.none);
             this.SetLightboxDoc(undefined);
         }
     };
@@ -243,7 +255,9 @@ export class LightboxView extends ObservableReactComponent<LightboxViewProps> {
                 />
             </div>
         );
-        return !this._doc ? null : (
+        return !this._doc ? (
+            <OverlayView />
+        ) : (
             <div
                 className="lightboxView-frame"
                 style={{ background: SnappingManager.userBackgroundColor }}
@@ -324,3 +338,8 @@ export class LightboxTourBtn extends React.Component<LightboxTourBtnProps> {
         return this.props.navBtn('50%', 0, 0, 'chevron-down', this.props.lightboxDoc(), this.props.stepInto, '');
     }
 }
+
+// eslint-disable-next-line prefer-arrow-callback
+ScriptingGlobals.add(function deiconifyViewToLightbox(documentView: DocumentView) {
+    LightboxView.Instance.AddDocTab(documentView.Document, OpenWhere.lightbox, 'layout'); // , 0);
+});
diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx
index 259ffbbc5..8968acbbb 100644
--- a/src/client/views/Main.tsx
+++ b/src/client/views/Main.tsx
@@ -7,7 +7,6 @@ import * as dotenv from 'dotenv'; // see https://github.com/motdotla/dotenv#how-
 import * as React from 'react';
 import * as ReactDOM from 'react-dom/client';
 import { AssignAllExtensions } from '../../extensions/Extensions';
-import { Doc } from '../../fields/Doc';
 import { FieldLoader } from '../../fields/FieldLoader';
 import { BranchingTrailManager } from '../util/BranchingTrailManager';
 import { CurrentUserUtils } from '../util/CurrentUserUtils';
@@ -16,11 +15,49 @@ import { PingManager } from '../util/PingManager';
 import { ReplayMovements } from '../util/ReplayMovements';
 import { TrackMovements } from '../util/TrackMovements';
 import { KeyManager } from './GlobalKeyHandler';
+import { InkingStroke } from './InkingStroke';
 import { MainView } from './MainView';
+import { CollectionCalendarView } from './collections/CollectionCalendarView';
+import { CollectionDockingView } from './collections/CollectionDockingView';
 import { CollectionView } from './collections/CollectionView';
+import { TabDocView } from './collections/TabDocView';
+import { CollectionFreeFormView } from './collections/collectionFreeForm';
 import { CollectionFreeFormInfoUI } from './collections/collectionFreeForm/CollectionFreeFormInfoUI';
+import { CollectionSchemaView } from './collections/collectionSchema/CollectionSchemaView';
+import { SchemaRowBox } from './collections/collectionSchema/SchemaRowBox';
 import './global/globalScripts';
+import { AudioBox } from './nodes/AudioBox';
+import { ComparisonBox } from './nodes/ComparisonBox';
+import { DataVizBox } from './nodes/DataVizBox/DataVizBox';
+import { DocumentContentsView, HTMLtag } from './nodes/DocumentContentsView';
+import { EquationBox } from './nodes/EquationBox';
+import { FieldView } from './nodes/FieldView';
+import { FontIconBox } from './nodes/FontIconBox/FontIconBox';
+import { FunctionPlotBox } from './nodes/FunctionPlotBox';
+import { ImageBox } from './nodes/ImageBox';
 import { KeyValueBox } from './nodes/KeyValueBox';
+import { LabelBox } from './nodes/LabelBox';
+import { LinkBox } from './nodes/LinkBox';
+import { LoadingBox } from './nodes/LoadingBox';
+import { MapBox } from './nodes/MapBox/MapBox';
+import { MapPushpinBox } from './nodes/MapBox/MapPushpinBox';
+import { PDFBox } from './nodes/PDFBox';
+import { PhysicsSimulationBox } from './nodes/PhysicsBox/PhysicsSimulationBox';
+import { RecordingBox } from './nodes/RecordingBox';
+import { ScreenshotBox } from './nodes/ScreenshotBox';
+import { ScriptingBox } from './nodes/ScriptingBox';
+import { VideoBox } from './nodes/VideoBox';
+import { WebBox } from './nodes/WebBox';
+import { DashDocCommentView } from './nodes/formattedText/DashDocCommentView';
+import { DashDocView } from './nodes/formattedText/DashDocView';
+import { DashFieldView } from './nodes/formattedText/DashFieldView';
+import { EquationView } from './nodes/formattedText/EquationView';
+import { FootnoteView } from './nodes/formattedText/FootnoteView';
+import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox';
+import { SummaryView } from './nodes/formattedText/SummaryView';
+import { ImportElementBox } from './nodes/importBox/ImportElementBox';
+import { PresBox, PresElementBox } from './nodes/trails';
+import { SearchBox } from './search/SearchBox';
 
 dotenv.config();
 
@@ -36,22 +73,14 @@ FieldLoader.ServerLoadStatus = { requested: 0, retrieved: 0, message: 'cache' };
     root.render(<FieldLoader />);
     window.location.search.includes('safe') && CollectionView.SetSafeMode(true);
     const info = await CurrentUserUtils.loadCurrentUser();
-    // if (info.email === 'guest') DocServer.Control.makeReadOnly();
     if (!info.userDocumentId) {
         alert('Fatal Error: user not found in database');
         return;
     }
     await CurrentUserUtils.loadUserDocument(info);
     setTimeout(() => {
-        document.getElementById('root')!.addEventListener(
-            'wheel',
-            event => {
-                if (event.ctrlKey) {
-                    event.preventDefault();
-                }
-            },
-            true
-        );
+        // prevent zooming browser
+        document.getElementById('root')!.addEventListener('wheel', event => event.ctrlKey && event.preventDefault(), true);
         const startload = (document as any).startLoad;
         const loading = Date.now() - (startload ? Number(startload) : Date.now() - 3000);
         console.log('Loading Time = ' + loading);
@@ -65,10 +94,56 @@ FieldLoader.ServerLoadStatus = { requested: 0, retrieved: 0, message: 'cache' };
         new PingManager();
         new KeyManager();
 
-        // iniitialize plugin apis
+        // initialize plugins and classes that require plugins
+        CollectionDockingView.Init(TabDocView);
+        FormattedTextBox.Init((tbox: FormattedTextBox) => ({
+            dashComment(node: any, view: any, getPos: any) { return new DashDocCommentView(node, view, getPos); }, // prettier-ignore
+            dashDoc(node: any, view: any, getPos: any)     { return new DashDocView(node, view, getPos, tbox); }, // prettier-ignore
+            dashField(node: any, view: any, getPos: any)   { return new DashFieldView(node, view, getPos, tbox); }, // prettier-ignore
+            equation(node: any, view: any, getPos: any)    { return new EquationView(node, view, getPos, tbox); }, // prettier-ignore
+            summary(node: any, view: any, getPos: any)     { return new SummaryView(node, view, getPos); }, // prettier-ignore
+            footnote(node: any, view: any, getPos: any)    { return new FootnoteView(node, view, getPos); }, // prettier-ignore
+        }));
         CollectionFreeFormInfoUI.Init();
         LinkFollower.Init();
         KeyValueBox.Init();
+        PresBox.Init(TabDocView.AllTabDocs);
+        DocumentContentsView.Init(KeyValueBox.LayoutString(), {
+            FormattedTextBox,
+            ImageBox,
+            FontIconBox,
+            LabelBox,
+            EquationBox,
+            FieldView,
+            CollectionFreeFormView,
+            CollectionDockingView,
+            CollectionSchemaView,
+            CollectionCalendarView,
+            CollectionView,
+            WebBox,
+            KeyValueBox,
+            PDFBox,
+            VideoBox,
+            AudioBox,
+            RecordingBox,
+            PresBox,
+            PresElementBox,
+            SearchBox,
+            FunctionPlotBox,
+            InkingStroke,
+            LinkBox,
+            ScriptingBox,
+            MapBox,
+            ScreenshotBox,
+            DataVizBox,
+            HTMLtag,
+            ComparisonBox,
+            LoadingBox,
+            PhysicsSimulationBox,
+            SchemaRowBox,
+            ImportElementBox,
+            MapPushpinBox,
+        });
         root.render(<MainView />);
     }, 0);
 })();
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 637824c31..e4a18dcea 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -12,7 +12,7 @@ import '../../../node_modules/browndash-components/dist/styles/global.min.css';
 import { ClientUtils, lightOrDark, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue, returnZero, setupMoveUpEvents } from '../../ClientUtils';
 import { emptyFunction } from '../../Utils';
 import { Doc, DocListCast, GetDocFromUrl, Opt } from '../../fields/Doc';
-import { DocData, DocViews } from '../../fields/DocSymbols';
+import { DocData } from '../../fields/DocSymbols';
 import { Id } from '../../fields/FieldSymbols';
 import { DocCast, StrCast, toList } from '../../fields/Types';
 import { DocServer } from '../DocServer';
@@ -45,10 +45,9 @@ import { GestureOverlay } from './GestureOverlay';
 import { LightboxView } from './LightboxView';
 import './MainView.scss';
 import { ObservableReactComponent } from './ObservableReactComponent';
-import { OverlayView } from './OverlayView';
 import { PreviewCursor } from './PreviewCursor';
 import { PropertiesView } from './PropertiesView';
-import { DashboardStyleProvider, DefaultStyleProvider } from './StyleProvider';
+import { DashboardStyleProvider, DefaultStyleProvider, returnEmptyDocViewList } from './StyleProvider';
 import { TimelineMenu } from './animationtimeline/TimelineMenu';
 import { CollectionDockingView } from './collections/CollectionDockingView';
 import { CollectionMenu } from './collections/CollectionMenu';
@@ -61,7 +60,7 @@ import { LinkMenu } from './linking/LinkMenu';
 import { AudioBox } from './nodes/AudioBox';
 import { SchemaCSVPopUp } from './nodes/DataVizBox/SchemaCSVPopUp';
 import { DocButtonState } from './nodes/DocumentLinksButton';
-import { DocumentView, DocumentViewInternal, returnEmptyDocViewList } from './nodes/DocumentView';
+import { DocumentView, DocumentViewInternal } from './nodes/DocumentView';
 import { ImageEditorData as ImageEditor } from './nodes/ImageBox';
 import { LinkDescriptionPopup } from './nodes/LinkDescriptionPopup';
 import { LinkDocPreview, LinkInfo } from './nodes/LinkDocPreview';
@@ -102,7 +101,7 @@ export class MainView extends ObservableReactComponent<{}> {
         return this._hideUI ? 0 : 27;
     } //  27 comes form lm.config.defaultConfig.dimensions.headerHeight in goldenlayout.js
     @computed private get topOfDashUI() {
-        return this._hideUI || LightboxView.LightboxDoc ? 0 : Number(TOPBAR_HEIGHT.replace('px', ''));
+        return this._hideUI || DocumentView.LightboxDoc() ? 0 : Number(TOPBAR_HEIGHT.replace('px', ''));
     }
     @computed private get topOfHeaderBarDoc() {
         return this.topOfDashUI;
@@ -171,6 +170,10 @@ export class MainView extends ObservableReactComponent<{}> {
             () => DocumentView.Selected().slice(),
             views => views.length > 1 && (document.activeElement as any)?.blur !== undefined && (document.activeElement as any)!.blur()
         );
+        reaction(
+            () => Doc.MyDockedBtns.linearView_IsOpen,
+            open => SnappingManager.SetPrintToConsole(!!open)
+        );
         const scriptTag = document.createElement('script');
         scriptTag.setAttribute('type', 'text/javascript');
         scriptTag.setAttribute('src', 'https://www.bing.com/api/maps/mapcontrol?callback=makeMap');
@@ -252,7 +255,6 @@ export class MainView extends ObservableReactComponent<{}> {
     constructor(props: any) {
         super(props);
         makeObservable(this);
-        CollectionDockingView.setTabJSXComponent(TabDocView);
         DocumentViewInternal.addDocTabFunc = MainView.addDocTabFunc_impl;
         MainView.Instance = this;
         DashboardView._urlState = HistoryUtil.parseUrl(window.location) || ({} as any);
@@ -686,7 +688,7 @@ export class MainView extends ObservableReactComponent<{}> {
 
     @computed get dockingContent() {
         return (
-            <GestureOverlay isActive={!LightboxView.LightboxDoc}>
+            <GestureOverlay isActive={!DocumentView.LightboxDoc()}>
                 <div
                     key="docking"
                     className={`mainView-dockingContent${this._leftMenuFlyoutWidth ? '-flyout' : ''}`}
@@ -697,7 +699,7 @@ export class MainView extends ObservableReactComponent<{}> {
                     style={{
                         width: `calc(100% - ${this._leftMenuFlyoutWidth + this.leftMenuWidth() + this.propertiesWidth()}px)`,
                         minWidth: `calc(100% - ${this._leftMenuFlyoutWidth + this.leftMenuWidth() + this.propertiesWidth()}px)`,
-                        transform: LightboxView.LightboxDoc ? 'scale(0.0001)' : undefined,
+                        transform: DocumentView.LightboxDoc() ? 'scale(0.0001)' : undefined,
                     }}>
                     {!this.mainContainer ? null : this.mainDocView}
                 </div>
@@ -791,7 +793,7 @@ export class MainView extends ObservableReactComponent<{}> {
 
     @computed get leftMenuPanel() {
         return (
-            <div key="menu" className="mainView-leftMenuPanel" style={{ background: SnappingManager.userBackgroundColor, display: LightboxView.LightboxDoc ? 'none' : undefined }}>
+            <div key="menu" className="mainView-leftMenuPanel" style={{ background: SnappingManager.userBackgroundColor, display: DocumentView.LightboxDoc() ? 'none' : undefined }}>
                 <DocumentView
                     Document={Doc.MyLeftSidebarMenu}
                     addDocument={undefined}
@@ -957,7 +959,7 @@ export class MainView extends ObservableReactComponent<{}> {
     }
     @computed get snapLines() {
         const dragged = DragManager.docsBeingDragged.lastElement() ?? DocumentView.SelectedDocs().lastElement();
-        const dragPar = dragged ? CollectionFreeFormView.from(Array.from(dragged[DocViews]).lastElement()) : undefined;
+        const dragPar = dragged ? CollectionFreeFormView.from(DocumentView.getViews(dragged).lastElement()) : undefined;
         return !dragPar?.layoutDoc.freeform_snapLines ? null : (
             <div className="mainView-snapLines">
                 <svg style={{ width: '100%', height: '100%' }}>
@@ -1066,7 +1068,7 @@ export class MainView extends ObservableReactComponent<{}> {
                         case 'home': return <DashboardView />;
                         case 'dashboard':
                         default: return (<>
-                                        <div key="dashdiv" style={{ position: 'relative', display: this._hideUI || LightboxView.LightboxDoc ? 'none' : undefined, zIndex: 2001 }}>
+                                        <div key="dashdiv" style={{ position: 'relative', display: this._hideUI || DocumentView.LightboxDoc() ? 'none' : undefined, zIndex: 2001 }}>
                                             <CollectionMenu panelWidth={this.topMenuWidth} panelHeight={this.topMenuHeight} togglePropertiesFlyout={this.togglePropertiesFlyout} toggleTopBar={this.toggleTopBar} topBarHeight={this.headerBarHeightFunc}/>
                                         </div>
                                         {this.mainDashboardArea}
@@ -1083,14 +1085,11 @@ export class MainView extends ObservableReactComponent<{}> {
                 <MarqueeOptionsMenu />
                 <TimelineMenu />
                 <RichTextMenu />
-                {/* <InkTranscription /> */}
                 {this.snapLines}
-                <LightboxView key="lightbox" PanelWidth={this._windowWidth} PanelHeight={this._windowHeight} maxBorder={this.lightboxMaxBorder} />
-                {LightboxView.LightboxDoc ? null : <OverlayView />}
+                <LightboxView key="lightbox" PanelWidth={this._windowWidth} addSplit={CollectionDockingView.AddSplit} PanelHeight={this._windowHeight} maxBorder={this.lightboxMaxBorder} />
                 <GPTPopup key="gptpopup" />
                 <SchemaCSVPopUp key="schemacsvpopup" />
                 <GenerativeFill imageEditorOpen={ImageEditor.Open} imageEditorSource={ImageEditor.Source} imageRootDoc={ImageEditor.RootDoc} addDoc={ImageEditor.AddDoc} />
-                {/* <NewLightboxView key="newLightbox" PanelWidth={this._windowWidth} PanelHeight={this._windowHeight} maxBorder={[200, 50]} /> */}
             </div>
         );
     }
diff --git a/src/client/views/ObservableReactComponent.tsx b/src/client/views/ObservableReactComponent.tsx
index 266411770..34da82b6c 100644
--- a/src/client/views/ObservableReactComponent.tsx
+++ b/src/client/views/ObservableReactComponent.tsx
@@ -1,6 +1,8 @@
 import { action, makeObservable, observable } from 'mobx';
 import * as React from 'react';
 import './AntimodeMenu.scss';
+import { observer } from 'mobx-react';
+import JsxParser from 'react-jsx-parser';
 
 /**
  * This is an abstract class that serves as the base for a PDF-style or Marquee-style
@@ -21,3 +23,12 @@ export abstract class ObservableReactComponent<T> extends React.Component<T, {}>
             })); // prettier-ignore
     }
 }
+
+class ObserverJsxParser1 extends JsxParser {
+    constructor(props: any) {
+        super(props);
+        observer(this as any);
+    }
+}
+
+export const ObserverJsxParser: typeof JsxParser = ObserverJsxParser1 as any;
diff --git a/src/client/views/PropertiesDocContextSelector.tsx b/src/client/views/PropertiesDocContextSelector.tsx
index 7465c727a..1fea36d16 100644
--- a/src/client/views/PropertiesDocContextSelector.tsx
+++ b/src/client/views/PropertiesDocContextSelector.tsx
@@ -10,7 +10,7 @@ import { Cast, StrCast } from '../../fields/Types';
 import { ObservableReactComponent } from './ObservableReactComponent';
 import './PropertiesDocContextSelector.scss';
 import { CollectionDockingView } from './collections/CollectionDockingView';
-import { DocFocusOrOpen, DocumentView } from './nodes/DocumentView';
+import { DocumentView } from './nodes/DocumentView';
 import { OpenWhere } from './nodes/OpenWhere';
 
 type PropertiesDocContextSelectorProps = {
@@ -58,7 +58,7 @@ export class PropertiesDocContextSelector extends ObservableReactComponent<Prope
     getOnClick = (clickCol: Doc) => {
         if (!this._props.DocView) return;
         const col = Doc.IsDataProto(clickCol) ? Doc.MakeDelegate(clickCol) : clickCol;
-        DocFocusOrOpen(Doc.GetProto(this._props.DocView.Document), undefined, col);
+        DocumentView.FocusOrOpen(Doc.GetProto(this._props.DocView.Document), undefined, col);
     };
 
     render() {
diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx
index e6e95e86c..0b8201903 100644
--- a/src/client/views/StyleProvider.tsx
+++ b/src/client/views/StyleProvider.tsx
@@ -12,7 +12,6 @@ import { BsArrowDown, BsArrowDownUp, BsArrowUp } from 'react-icons/bs';
 import { FaFilter } from 'react-icons/fa';
 import { ClientUtils, DashColor, lightOrDark } from '../../ClientUtils';
 import { Doc, Opt, StrListCast } from '../../fields/Doc';
-import { DocViews } from '../../fields/DocSymbols';
 import { Id } from '../../fields/FieldSymbols';
 import { ScriptField } from '../../fields/ScriptField';
 import { BoolCast, Cast, DocCast, ImageCast, NumCast, ScriptCast, StrCast } from '../../fields/Types';
@@ -23,10 +22,11 @@ import { SnappingManager } from '../util/SnappingManager';
 import { undoBatch, UndoManager } from '../util/UndoManager';
 import { TreeSort } from './collections/TreeSort';
 import { Colors } from './global/globalEnums';
-import { DocFocusOrOpen, DocumentView, DocumentViewProps } from './nodes/DocumentView';
+import { DocumentView, DocumentViewProps } from './nodes/DocumentView';
 import { FieldViewProps } from './nodes/FieldView';
 import { StyleProp } from './StyleProp';
 import './StyleProvider.scss';
+import { emptyPath } from '../../Utils';
 
 function toggleLockedPosition(doc: Doc) {
     UndoManager.RunInBatch(() => Doc.toggleLockedPosition(doc), 'toggleBackground');
@@ -134,7 +134,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps &
         case StyleProp.Highlighting:
             if (doc && (Doc.IsSystem(doc) || doc.type === DocumentType.FONTICON)) return undefined;            
             if (doc && !doc.layout_disableBrushing && !disableBrushing) {
-                const selected = Array.from(doc?.[DocViews]??[]).filter(dv => dv.IsSelected).length;
+                const selected = DocumentView.getViews(doc).filter(dv => dv.IsSelected).length;
                 const highlightIndex = Doc.GetBrushHighlightStatus(doc) || (selected ? Doc.DocBrushStatus.selfBrushed : 0);
                 const highlightColor = ['transparent', 'rgb(68, 118, 247)', selected ? "black" : 'rgb(68, 118, 247)', 'orange', 'lightBlue'][highlightIndex];
                 const highlightStyle = ['solid', 'dashed', 'solid', 'solid', 'solid'][highlightIndex];
@@ -403,7 +403,11 @@ export function DashboardStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps>
     if (doc && property.split(':')[0] === StyleProp.Decorations) {
         return doc._type_collection === CollectionViewType.Docking || Doc.IsSystem(doc)
             ? null
-            : DashboardToggleButton(doc, 'hidden', 'eye-slash', 'eye', () => DocFocusOrOpen(doc, { toggleTarget: true, willZoomCentered: true, zoomScale: 0 }, DocCast(doc?.embedContainer ?? doc?.annotationOn)));
+            : DashboardToggleButton(doc, 'hidden', 'eye-slash', 'eye', () => DocumentView.FocusOrOpen(doc, { toggleTarget: true, willZoomCentered: true, zoomScale: 0 }, DocCast(doc?.embedContainer ?? doc?.annotationOn)));
     }
     return DefaultStyleProvider(doc, props, property);
 }
+
+export function returnEmptyDocViewList() {
+    return emptyPath;
+}
diff --git a/src/client/views/TemplateMenu.tsx b/src/client/views/TemplateMenu.tsx
index 5a7c2ef5b..cff32a557 100644
--- a/src/client/views/TemplateMenu.tsx
+++ b/src/client/views/TemplateMenu.tsx
@@ -13,8 +13,8 @@ import { DocUtils } from '../documents/DocUtils';
 import { ScriptingGlobals } from '../util/ScriptingGlobals';
 import { Transform } from '../util/Transform';
 import { CollectionTreeView } from './collections/CollectionTreeView';
-import { DocumentView, returnEmptyDocViewList } from './nodes/DocumentView';
-import { DefaultStyleProvider } from './StyleProvider';
+import { DocumentView } from './nodes/DocumentView';
+import { DefaultStyleProvider, returnEmptyDocViewList } from './StyleProvider';
 import './TemplateMenu.scss';
 
 @observer
diff --git a/src/client/views/ViewBoxInterface.ts b/src/client/views/ViewBoxInterface.ts
new file mode 100644
index 000000000..c633f34fb
--- /dev/null
+++ b/src/client/views/ViewBoxInterface.ts
@@ -0,0 +1,60 @@
+import * as React from 'react';
+import { Doc, FieldType, Opt } from '../../fields/Doc';
+import { RefField } from '../../fields/RefField';
+import { DragManager } from '../util/DragManager';
+import { ObservableReactComponent } from './ObservableReactComponent';
+import { PinProps } from './PinFuncs';
+import { DocumentView } from './nodes/DocumentView';
+import { FocusViewOptions } from './nodes/FocusViewOptions';
+import { OpenWhere } from './nodes/OpenWhere';
+// import { DocUtils } from '../documents/Documents';
+
+/**
+ * Shared interface among all viewBox'es (ie, react classes that render the contents of a Doc)
+ * Many of these methods only make sense for specific viewBox'es, but they should be written to
+ * be as general as possible
+ */
+export abstract class ViewBoxInterface<P> extends ObservableReactComponent<React.PropsWithChildren<P>> {
+    abstract get Document(): Doc;
+    abstract get dataDoc(): Doc;
+    abstract get fieldKey(): string;
+    promoteCollection?: () => void; // moves contents of collection to parent
+    updateIcon?: () => void; // updates the icon representation of the document
+    getAnchor?: (addAsAnnotation: boolean, pinData?: PinProps) => Doc; // returns an Anchor Doc that represents the current state of the doc's componentview (e.g., the current playhead location of a an audio/video box)
+    restoreView?: (viewSpec: Doc) => boolean;
+    scrollPreview?: (docView: DocumentView, doc: Doc, focusSpeed: number, options: FocusViewOptions) => Opt<number>; // returns the duration of the focus
+    brushView?: (view: { width: number; height: number; panX: number; panY: number }, transTime: number, holdTime: number) => void; // highlight a region of a view (used by freeforms)
+    getView?: (doc: Doc, options: FocusViewOptions) => Promise<Opt<DocumentView>>; // returns a nested DocumentView for the specified doc or undefined
+    addDocTab?: (doc: Doc, where: OpenWhere) => boolean; // determines how to add a document - used in following links to open the target ina local lightbox
+    addDocument?: (doc: Doc | Doc[], annotationKey?: string) => boolean; // add a document (used only by collections)
+    removeDocument?: (doc: Doc | Doc[], annotationKey?: string, leavePushpin?: boolean, dontAddToRemoved?: boolean) => boolean; // add a document (used only by collections)
+    select?: (ctrlKey: boolean, shiftKey: boolean) => void;
+    focus?: (textAnchor: Doc, options: FocusViewOptions) => Opt<number>;
+    viewTransition?: () => Opt<string>; // duration of a view transition animation
+    isAnyChildContentActive?: () => boolean; // is any child content of the document active
+    onClickScriptDisable?: () => 'never' | 'always'; // disable click scripts : never, always, or undefined = only when selected
+    getKeyFrameEditing?: () => boolean; // whether the document is in keyframe editing mode (if it is, then all hidden documents that are not active at the keyframe time will still be shown)
+    setKeyFrameEditing?: (set: boolean) => void; // whether the document is in keyframe editing mode (if it is, then all hidden documents that are not active at the keyframe time will still be shown)
+    playTrail?: (docs: Doc[]) => void;
+    playFrom?: (time: number, endTime?: number, fullPlay?: boolean) => void; // play a range of a media document
+    Play?: () => void; // play a media documents
+    Pause?: () => void; // pause a media document (eg, audio/video)
+    IsPlaying?: () => boolean; // is a media document playing
+    PlayerTime?: () => number | undefined; // current timecode of player
+    TogglePause?: (keep?: boolean) => void; // toggle media document playing state
+    setFocus?: () => void; // sets input focus to the componentView
+    setData?: (data: FieldType | Promise<RefField | undefined>) => boolean;
+    componentUI?: (boundsLeft: number, boundsTop: number) => JSX.Element | null;
+    dragStarting?: (snapToDraggedDoc: boolean, showGroupDragTarget: boolean, visited: Set<Doc>) => void;
+    dragConfig?: (dragData: DragManager.DocumentDragData) => void; // function to setup dragData in custom way (see TreeViews which add a tree view flag)
+    incrementalRendering?: () => void;
+    infoUI?: () => JSX.Element | null;
+    contentBounds?: () => undefined | { bounds: { x: number; y: number; r: number; b: number }; cx: number; cy: number; width: number; height: number }; // bounds of contents in collection coordinate space (used by TabDocViewThumb)
+    screenBounds?: () => Opt<{ left: number; top: number; right: number; bottom: number; transition?: string }>;
+    ptToScreen?: (pt: { X: number; Y: number }) => { X: number; Y: number };
+    ptFromScreen?: (pt: { X: number; Y: number }) => { X: number; Y: number };
+    snapPt?: (pt: { X: number; Y: number }, excludeSegs?: number[]) => { nearestPt: { X: number; Y: number }; distance: number };
+    search?: (str: string, bwd?: boolean, clear?: boolean) => boolean;
+    dontRegisterView?: () => boolean; // KeyValueBox's don't want to register their views
+    isUnstyledView?: () => boolean; // SchemaView and KeyValue are unstyled -- not titles, no opacity, no animations
+}
diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index 0ee3575f3..8fb2b30f1 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -22,7 +22,6 @@ import { ScriptingGlobals } from '../../util/ScriptingGlobals';
 import { SnappingManager } from '../../util/SnappingManager';
 import { undoable, undoBatch, UndoManager } from '../../util/UndoManager';
 import { DashboardView } from '../DashboardView';
-import { LightboxView } from '../LightboxView';
 import { DocumentView } from '../nodes/DocumentView';
 import { OpenWhere, OpenWhereMod } from '../nodes/OpenWhere';
 import { OverlayView } from '../OverlayView';
@@ -37,11 +36,13 @@ const _global = (window /* browser */ || global) /* node */ as any;
 export class CollectionDockingView extends CollectionSubView() {
     static tabClass: JSX.Element | null = null;
     /**
-     * Configure golden layout to render its documents using the specified React component
+     * Initialize by assigning the add split method to DocumentView and by
+     * configuring golden layout to render its documents using the specified React component
      * @param ele - typically would be set to TabDocView
      */
-    static setTabJSXComponent(ele: any) {
+    public static Init(ele: any) {
         this.tabClass = ele;
+        DocumentView.addSplit = CollectionDockingView.AddSplit;
     }
     // eslint-disable-next-line no-use-before-define
     @observable public static Instance: CollectionDockingView | undefined = undefined;
@@ -336,7 +337,7 @@ export class CollectionDockingView extends CollectionSubView() {
         SetPropSetterCb('title', this.titleChanged); // this overrides any previously assigned callback for the property
         if (this._containerRef.current) {
             this._lightboxReactionDisposer = reaction(
-                () => LightboxView.LightboxDoc,
+                () => DocumentView.LightboxDoc(),
                 doc => setTimeout(() => !doc && this.onResize())
             );
             new _global.ResizeObserver(this.onResize).observe(this._containerRef.current);
@@ -391,7 +392,7 @@ export class CollectionDockingView extends CollectionSubView() {
     onResize = () => {
         const cur = this._containerRef.current;
         // bcz: since GoldenLayout isn't a React component itself, we need to notify it to resize when its document container's size has changed
-        !LightboxView.LightboxDoc && cur && this._goldenLayout?.updateSize(cur.getBoundingClientRect().width, cur.getBoundingClientRect().height);
+        !DocumentView.LightboxDoc() && cur && this._goldenLayout?.updateSize(cur.getBoundingClientRect().width, cur.getBoundingClientRect().height);
     };
 
     endUndoBatch = () => {
@@ -633,7 +634,7 @@ export class CollectionDockingView extends CollectionSubView() {
 ScriptingGlobals.add(
     // eslint-disable-next-line prefer-arrow-callback
     function openInLightbox(doc: any) {
-        LightboxView.Instance.AddDocTab(doc, OpenWhere.lightbox);
+        CollectionDockingView.Instance?._props.addDocTab(doc, OpenWhere.lightbox);
     },
     'opens up document in a lightbox',
     '(doc: any)'
diff --git a/src/client/views/collections/CollectionMenu.tsx b/src/client/views/collections/CollectionMenu.tsx
index e53071584..3eb3008c4 100644
--- a/src/client/views/collections/CollectionMenu.tsx
+++ b/src/client/views/collections/CollectionMenu.tsx
@@ -26,8 +26,8 @@ import { Transform } from '../../util/Transform';
 import { undoBatch } from '../../util/UndoManager';
 import { AntimodeMenu } from '../AntimodeMenu';
 import { EditableView } from '../EditableView';
-import { DefaultStyleProvider } from '../StyleProvider';
-import { DocumentView, DocumentViewInternal, returnEmptyDocViewList } from '../nodes/DocumentView';
+import { DefaultStyleProvider, returnEmptyDocViewList } from '../StyleProvider';
+import { DocumentView, DocumentViewInternal } from '../nodes/DocumentView';
 import './CollectionMenu.scss';
 import { CollectionLinearView } from './collectionLinear';
 
diff --git a/src/client/views/collections/CollectionNoteTakingView.tsx b/src/client/views/collections/CollectionNoteTakingView.tsx
index 53211be77..16c474996 100644
--- a/src/client/views/collections/CollectionNoteTakingView.tsx
+++ b/src/client/views/collections/CollectionNoteTakingView.tsx
@@ -22,7 +22,6 @@ import { ContextMenu } from '../ContextMenu';
 import { ContextMenuProps } from '../ContextMenuItem';
 import { FieldsDropdown } from '../FieldsDropdown';
 import { Colors } from '../global/globalEnums';
-import { LightboxView } from '../LightboxView';
 import { CollectionFreeFormDocumentView } from '../nodes/CollectionFreeFormDocumentView';
 import { DocumentView } from '../nodes/DocumentView';
 import { FieldViewProps } from '../nodes/FieldView';
@@ -177,7 +176,7 @@ export class CollectionNoteTakingView extends CollectionSubView() {
         );
 
         this._disposers.refList = reaction(
-            () => ({ refList: this._refList.slice(), autoHeight: this.layoutDoc._layout_autoHeight && !LightboxView.Contains(this.DocumentView?.()) }),
+            () => ({ refList: this._refList.slice(), autoHeight: this.layoutDoc._layout_autoHeight && !DocumentView.LightboxContains(this.DocumentView?.()) }),
             ({ refList, autoHeight }) => {
                 if (autoHeight) {
                     refList.forEach(r => this.observer.observe(r));
diff --git a/src/client/views/collections/CollectionNoteTakingViewColumn.tsx b/src/client/views/collections/CollectionNoteTakingViewColumn.tsx
index c098c033b..44ab1968d 100644
--- a/src/client/views/collections/CollectionNoteTakingViewColumn.tsx
+++ b/src/client/views/collections/CollectionNoteTakingViewColumn.tsx
@@ -4,14 +4,11 @@ import { action, computed, makeObservable, observable } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
 import { lightOrDark, returnEmptyString } from '../../../ClientUtils';
-import { Doc, DocListCast, Opt } from '../../../fields/Doc';
-import { RichTextField } from '../../../fields/RichTextField';
+import { Doc, Opt } from '../../../fields/Doc';
 import { listSpec } from '../../../fields/Schema';
 import { SchemaHeaderField } from '../../../fields/SchemaHeaderField';
 import { Cast, NumCast } from '../../../fields/Types';
-import { ImageField } from '../../../fields/URLField';
 import { TraceMobx } from '../../../fields/util';
-import { DocumentFromField } from '../../documents/DocFromField';
 import { DocUtils } from '../../documents/DocUtils';
 import { Docs } from '../../documents/Documents';
 import { DragManager } from '../../util/DragManager';
@@ -19,11 +16,10 @@ import { SnappingManager } from '../../util/SnappingManager';
 import { Transform } from '../../util/Transform';
 import { undoBatch, undoable } from '../../util/UndoManager';
 import { ContextMenu } from '../ContextMenu';
-import { ContextMenuProps } from '../ContextMenuItem';
 import { EditableView } from '../EditableView';
 import { ObservableReactComponent } from '../ObservableReactComponent';
-import './CollectionNoteTakingView.scss';
 import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox';
+import './CollectionNoteTakingView.scss';
 
 interface CSVFieldColumnProps {
     Document: Doc;
@@ -171,9 +167,7 @@ export class CollectionNoteTakingViewColumn extends ObservableReactComponent<CSV
 
     menuCallback = (x: number, y: number) => {
         ContextMenu.Instance.clearItems();
-        const layoutItems: ContextMenuProps[] = [];
-        const docItems: ContextMenuProps[] = [];
-        const dataDoc = this._props.TemplateDataDocument || this._props.Document;
+        const { pivotField } = this._props;
         const pivotValue = this.getValue(this._props.heading);
 
         DocUtils.addDocumentCreatorMenuItems(
@@ -187,50 +181,10 @@ export class CollectionNoteTakingViewColumn extends ObservableReactComponent<CSV
             x,
             y,
             true,
-            this._props.pivotField,
+            pivotField, // when created, the new doc's pivotField will be set to pivotValue
             pivotValue
         );
 
-        Array.from(Object.keys(Doc.GetProto(dataDoc)))
-            .filter(fieldKey => dataDoc[fieldKey] instanceof RichTextField || dataDoc[fieldKey] instanceof ImageField || typeof dataDoc[fieldKey] === 'string')
-            .map(fieldKey =>
-                docItems.push({
-                    description: ':' + fieldKey,
-                    event: () => {
-                        const created = DocumentFromField(dataDoc, fieldKey, Doc.GetProto(this._props.Document));
-                        if (created) {
-                            if (this._props.Document.isTemplateDoc) {
-                                Doc.MakeMetadataFieldTemplate(created, this._props.Document);
-                            }
-                            return this._props.addDocument?.(created);
-                        }
-                        return undefined;
-                    },
-                    icon: 'compress-arrows-alt',
-                })
-            );
-        Array.from(Object.keys(Doc.GetProto(dataDoc)))
-            .filter(fieldKey => DocListCast(dataDoc[fieldKey]).length)
-            .map(fieldKey =>
-                docItems.push({
-                    description: ':' + fieldKey,
-                    event: () => {
-                        const created = Docs.Create.CarouselDocument([], { _width: 400, _height: 200, title: fieldKey });
-                        if (created) {
-                            const container = this._props.Document.resolvedDataDoc ? Doc.GetProto(this._props.Document) : this._props.Document;
-                            if (container.isTemplateDoc) {
-                                Doc.MakeMetadataFieldTemplate(created, container);
-                                return Doc.AddDocToList(container, Doc.LayoutFieldKey(container), created);
-                            }
-                            return this._props.addDocument?.(created) || false;
-                        }
-                        return undefined;
-                    },
-                    icon: 'compress-arrows-alt',
-                })
-            );
-        !Doc.UserDoc().noviceMode && ContextMenu.Instance.addItem({ description: 'Doc Fields ...', subitems: docItems, icon: 'eye' });
-        !Doc.UserDoc().noviceMode && ContextMenu.Instance.addItem({ description: 'Containers ...', subitems: layoutItems, icon: 'eye' });
         ContextMenu.Instance.setDefaultItem('::', (name: string): void => {
             Doc.GetProto(this._props.Document)[name] = '';
             const created = Docs.Create.TextDocument('', { title: name, _width: 250, _layout_autoHeight: true });
diff --git a/src/client/views/collections/CollectionStackedTimeline.tsx b/src/client/views/collections/CollectionStackedTimeline.tsx
index 1604920f6..fac885300 100644
--- a/src/client/views/collections/CollectionStackedTimeline.tsx
+++ b/src/client/views/collections/CollectionStackedTimeline.tsx
@@ -26,7 +26,6 @@ import { SnappingManager } from '../../util/SnappingManager';
 import { Transform } from '../../util/Transform';
 import { undoBatch, UndoManager } from '../../util/UndoManager';
 import { VideoThumbnails } from '../global/globalEnums';
-import { LightboxView } from '../LightboxView';
 import { AudioWaveform } from '../nodes/audio/AudioWaveform';
 import { DocumentView } from '../nodes/DocumentView';
 import { FocusFuncType, StyleProviderFuncType } from '../nodes/FieldView';
@@ -752,10 +751,10 @@ class StackedTimelineAnchor extends ObservableReactComponent<StackedTimelineAnch
                 // const dictationDoc = Cast(this._props.layoutDoc.data_dictation, Doc, null);
                 // const isDictation = dictationDoc && LinkManager.Links(this._props.mark).some(link => Cast(link.link_anchor_1, Doc, null)?.annotationOn === dictationDoc);
                 if (
-                    !LightboxView.LightboxDoc &&
+                    !DocumentView.LightboxDoc() &&
                     // bcz: when should links be followed?  we don't want to move away from the video to follow a link but we can open it in a sidebar/etc.  But we don't know that upfront.
                     // for now, we won't follow any links when the lightbox is oepn to avoid "losing" the video.
-                    /* (isDictation || !Doc.AreProtosEqual(LightboxView.LightboxDoc, this._props.layoutDoc)) */
+                    /* (isDictation || !Doc.AreProtosEqual(DocumentView.LightboxDoc(), this._props.layoutDoc)) */
                     !this._props.layoutDoc.dontAutoFollowLinks &&
                     Doc.Links(this._props.mark).length &&
                     time > NumCast(this._props.mark[this._props.startTag]) &&
diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx
index 07aa0f4f0..56d2a6c9c 100644
--- a/src/client/views/collections/CollectionStackingView.tsx
+++ b/src/client/views/collections/CollectionStackingView.tsx
@@ -26,7 +26,6 @@ import { undoBatch, UndoManager } from '../../util/UndoManager';
 import { ContextMenu } from '../ContextMenu';
 import { ContextMenuProps } from '../ContextMenuItem';
 import { EditableView } from '../EditableView';
-import { LightboxView } from '../LightboxView';
 import { CollectionFreeFormDocumentView } from '../nodes/CollectionFreeFormDocumentView';
 import { DocumentView } from '../nodes/DocumentView';
 import { FieldViewProps } from '../nodes/FieldView';
@@ -229,7 +228,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
             layoutAutoHeight => layoutAutoHeight && this._props.setHeight?.(this.headerMargin + (this.isStackingView ? Math.max(...this._refList.map(DivHeight)) : this._refList.reduce((p, r) => p + DivHeight(r), 0)))
         );
         this._disposers.refList = reaction(
-            () => ({ refList: this._refList.slice(), autoHeight: this.layoutDoc._layout_autoHeight && !LightboxView.Contains(this.DocumentView?.()) }),
+            () => ({ refList: this._refList.slice(), autoHeight: this.layoutDoc._layout_autoHeight && !DocumentView.LightboxContains(this.DocumentView?.()) }),
             ({ refList, autoHeight }) => {
                 this.observer.disconnect();
                 if (autoHeight) refList.forEach(r => this.observer.observe(r));
diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx
index c1247f5b0..beb8c0666 100644
--- a/src/client/views/collections/CollectionTreeView.tsx
+++ b/src/client/views/collections/CollectionTreeView.tsx
@@ -209,7 +209,7 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree
     onContextMenu = (): void => {
         // need to test if propagation has stopped because GoldenLayout forces a parallel react hierarchy to be created for its top-level layout
         const layoutItems: ContextMenuProps[] = [];
-        const menuDoc = ScriptCast(Cast(this.layoutDoc.layout_headerButton, Doc, null)?.onClick).script.originalScript === CollectionTreeView.AddTreeFunc;
+        const menuDoc = ScriptCast(Cast(this.layoutDoc.layout_headerButton, Doc, null)?.onClick)?.script.originalScript === CollectionTreeView.AddTreeFunc;
         menuDoc && layoutItems.push({ description: 'Create new folder', event: () => CollectionTreeView.addTreeFolder(this.Document), icon: 'paint-brush' });
         if (!Doc.noviceMode) {
             layoutItems.push({
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index 7cadd072b..b52c7c54c 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -13,7 +13,7 @@ import { Docs } from '../../documents/Documents';
 import { ImageUtils } from '../../util/Import & Export/ImageUtils';
 import { ContextMenu } from '../ContextMenu';
 import { ContextMenuProps } from '../ContextMenuItem';
-import { ViewBoxAnnotatableComponent, ViewBoxInterface } from '../DocComponent';
+import { ViewBoxAnnotatableComponent } from '../DocComponent';
 import { FieldView } from '../nodes/FieldView';
 import { OpenWhere } from '../nodes/OpenWhere';
 import { CollectionCalendarView } from './CollectionCalendarView';
@@ -35,7 +35,7 @@ import { CollectionMultirowView } from './collectionMulticolumn/CollectionMultir
 import { CollectionSchemaView } from './collectionSchema/CollectionSchemaView';
 
 @observer
-export class CollectionView extends ViewBoxAnnotatableComponent<CollectionViewProps>() implements ViewBoxInterface {
+export class CollectionView extends ViewBoxAnnotatableComponent<CollectionViewProps>() {
     public static LayoutString(fieldStr: string) {
         return FieldView.LayoutString(CollectionView, fieldStr);
     }
diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx
index 008ef6ab4..afd584154 100644
--- a/src/client/views/collections/TabDocView.tsx
+++ b/src/client/views/collections/TabDocView.tsx
@@ -9,7 +9,7 @@ import * as ReactDOM from 'react-dom/client';
 import { ClientUtils, DashColor, lightOrDark, returnEmptyDoclist, returnFalse, returnTrue, setupMoveUpEvents, simulateMouseClick } from '../../../ClientUtils';
 import { emptyFunction } from '../../../Utils';
 import { Doc, Opt } from '../../../fields/Doc';
-import { DocData, DocViews } from '../../../fields/DocSymbols';
+import { DocData } from '../../../fields/DocSymbols';
 import { Id } from '../../../fields/FieldSymbols';
 import { List } from '../../../fields/List';
 import { FieldId } from '../../../fields/RefField';
@@ -28,13 +28,14 @@ import { LightboxView } from '../LightboxView';
 import { ObservableReactComponent } from '../ObservableReactComponent';
 import { PinDocView, PinProps } from '../PinFuncs';
 import { StyleProp } from '../StyleProp';
-import { DefaultStyleProvider } from '../StyleProvider';
+import { DefaultStyleProvider, returnEmptyDocViewList } from '../StyleProvider';
 import { Colors } from '../global/globalEnums';
-import { DocumentView, returnEmptyDocViewList } from '../nodes/DocumentView';
+import { DocumentView } from '../nodes/DocumentView';
 import { FieldViewProps } from '../nodes/FieldView';
 import { KeyValueBox } from '../nodes/KeyValueBox';
 import { OpenWhere, OpenWhereMod } from '../nodes/OpenWhere';
-import { PresBox, PresMovement } from '../nodes/trails';
+import { PresBox } from '../nodes/trails';
+import { PresMovement } from '../nodes/trails/PresEnums';
 import { CollectionDockingView } from './CollectionDockingView';
 import { CollectionView } from './CollectionView';
 import './TabDocView.scss';
@@ -187,6 +188,11 @@ interface TabDocViewProps {
 @observer
 export class TabDocView extends ObservableReactComponent<TabDocViewProps> {
     static _allTabs = new ObservableSet<TabDocView>();
+    public static AllTabDocs() {
+        return Array.from(TabDocView._allTabs)
+            .filter(tv => tv._document)
+            .map(tv => tv._document!);
+    }
     _mainCont: HTMLDivElement | null = null;
     _tabReaction: IReactionDisposer | undefined;
 
@@ -294,7 +300,7 @@ export class TabDocView extends ObservableReactComponent<TabDocViewProps> {
     @observable _isActive: boolean = false;
     @observable _isAnyChildContentActive = false;
     public static IsSelected = (doc?: Doc) => {
-        if (Array.from(doc?.[DocViews] ?? []).some(dv => dv?.IsSelected)) {
+        if (DocumentView.getViews(doc).some(dv => dv?.IsSelected)) {
             return true;
         }
         return false;
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormPannableContents.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormPannableContents.tsx
index 65a2fe0aa..e543b4008 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormPannableContents.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormPannableContents.tsx
@@ -20,6 +20,11 @@ export interface CollectionFreeFormPannableContentsProps {
 @observer
 export class CollectionFreeFormPannableContents extends ObservableReactComponent<CollectionFreeFormPannableContentsProps> {
     static _overlayPlugin: ((fform: Doc) => React.JSX.Element) | null = null;
+    /**
+     * Setup a plugin function that returns components to display on a layer above the collection
+     *     See PresBox which renders presenstation paths over the collection
+     * @param plugin a function that receives the collection Doc and returns JSX Elements
+     */
     public static SetOverlayPlugin(plugin: ((fform: Doc) => React.JSX.Element) | null) {
         CollectionFreeFormPannableContents._overlayPlugin = plugin;
     }
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 74de6524b..dbd9fb11f 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -28,7 +28,6 @@ import { CollectionViewType, DocumentType } from '../../../documents/DocumentTyp
 import { DocUtils } from '../../../documents/DocUtils';
 import { DragManager } from '../../../util/DragManager';
 import { dropActionType } from '../../../util/DropActionTypes';
-import { ReplayMovements } from '../../../util/ReplayMovements';
 import { CompileScript } from '../../../util/Scripting';
 import { ScriptingGlobals } from '../../../util/ScriptingGlobals';
 import { freeformScrollMode, SnappingManager } from '../../../util/SnappingManager';
@@ -36,9 +35,7 @@ import { Transform } from '../../../util/Transform';
 import { undoable, undoBatch, UndoManager } from '../../../util/UndoManager';
 import { Timeline } from '../../animationtimeline/Timeline';
 import { ContextMenu } from '../../ContextMenu';
-import { GestureOverlay } from '../../GestureOverlay';
 import { InkingStroke } from '../../InkingStroke';
-import { LightboxView } from '../../LightboxView';
 import { CollectionFreeFormDocumentView } from '../../nodes/CollectionFreeFormDocumentView';
 import { SchemaCSVPopUp } from '../../nodes/DataVizBox/SchemaCSVPopUp';
 import { DocumentView } from '../../nodes/DocumentView';
@@ -367,7 +364,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         }
         const xfToCollection = options?.docTransform ?? Transform.Identity();
         const savedState = { panX: NumCast(this.Document[this.panXFieldKey]), panY: NumCast(this.Document[this.panYFieldKey]), scale: options?.willZoomCentered ? this.Document[this.scaleFieldKey] : undefined };
-        const cantTransform = this.fitContentsToBox || ((this.Document.isGroup || this.layoutDoc._lockedTransform) && !LightboxView.LightboxDoc);
+        const cantTransform = this.fitContentsToBox || ((this.Document.isGroup || this.layoutDoc._lockedTransform) && !DocumentView.LightboxDoc());
         const { panX, panY, scale } = cantTransform || (!options.willPan && !options.willZoomCentered) ? savedState : this.calculatePanIntoView(anchor, xfToCollection, options?.willZoomCentered ? options?.zoomScale ?? 0.75 : undefined);
 
         // focus on the document in the collection
@@ -620,7 +617,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         return false;
     };
     forceStrokeGesture = (e: PointerEvent, gesture: Gestures, points: InkData, text?: any) => {
-        this.onGesture(e, new GestureUtils.GestureEvent(gesture, points, GestureOverlay.getBounds(points), text));
+        this.onGesture(e, new GestureUtils.GestureEvent(gesture, points, InkField.getBounds(points), text));
     };
 
     onPointerMove = (e: PointerEvent) => {
@@ -838,8 +835,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     @action
     setPan(panXIn: number, panYIn: number, panTime: number = 0, allowScroll = false) {
         let [panX, panY] = [panXIn, panYIn];
-        // this is the easiest way to do this -> will talk with Bob about using mobx to do this to remove this line of code.
-        if (Doc.UserDoc()?.presentationMode === 'watching') ReplayMovements.Instance.pauseFromInteraction();
 
         if (!this.isAnnotationOverlay && this.childDocs.length) {
             // this section wraps the pan position, horizontally and/or vertically whenever the content is panned out of the viewing bounds
@@ -849,7 +844,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
             panX = clamp(panX, xrangeMin - widScaling / 2, xrangeMax + widScaling / 2);
             panY = clamp(panY, yrangeMin - hgtScaling / 2, yrangeMax + hgtScaling / 2);
         }
-        if (!this.layoutDoc._lockedTransform || LightboxView.LightboxDoc) {
+        if (!this.layoutDoc._lockedTransform || DocumentView.LightboxDoc()) {
             this.setPanZoomTransition(panTime);
             const minScale = NumCast(this.dataDoc._freeform_scale_min, 1);
             const scale = 1 - minScale / this.zoomScaling();
@@ -882,6 +877,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     nudge = (x: number, y: number, nudgeTime: number = 500) => {
         const collectionDoc = this.Document;
         if (collectionDoc?._type_collection !== CollectionViewType.Freeform) {
+            SnappingManager.TriggerUserPanned();
             this.setPan(
                 NumCast(this.layoutDoc[this.panXFieldKey]) + ((this._props.PanelWidth() / 2) * x) / this.zoomScaling(), // nudge x,y as a function of panel dimension and scale
                 NumCast(this.layoutDoc[this.panYFieldKey]) + ((this._props.PanelHeight() / 2) * -y) / this.zoomScaling(),
@@ -1008,6 +1004,15 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         }
         return undefined;
     };
+
+    removeDocument = (docs: Doc | Doc[], annotationKey?: string | undefined) => {
+        const ret = !!this._props.removeDocument?.(docs, annotationKey);
+        // if this is a group and we have fewer than 2 Docs, then just promote what's left to our parent and get rid of the group.
+        if (ret && DocListCast(this.dataDoc[annotationKey ?? this.fieldKey]).length < 2 && this.Document.isGroup) {
+            this.promoteCollection();
+        }
+        return ret;
+    };
     childPointerEventsFunc = () => this._childPointerEvents;
     childContentsActive = () => (this._props.childContentsActive ?? this.isContentActive() === false ? returnFalse : emptyFunction)();
     getChildDocView(entry: PoolData) {
@@ -1050,7 +1055,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                 focus={this.Document.isGroup ? this.groupFocus : this.isAnnotationOverlay ? this._props.focus : this.focus}
                 addDocTab={this.addDocTab}
                 addDocument={this._props.addDocument}
-                removeDocument={this._props.removeDocument}
+                removeDocument={this.removeDocument}
                 moveDocument={this._props.moveDocument}
                 pinToPres={this._props.pinToPres}
                 whenChildContentsActiveChanged={this._props.whenChildContentsActiveChanged}
@@ -1525,7 +1530,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     incrementalRendering = () => this.childDocs.filter(doc => !this._renderCutoffData.get(doc[Id])).length !== 0;
 
     incrementalRender = action(() => {
-        if (!LightboxView.LightboxDoc || LightboxView.Contains(this.DocumentView?.())) {
+        if (!DocumentView.LightboxDoc() || DocumentView.LightboxContains(this.DocumentView?.())) {
             const layoutUnrendered = this.childDocs.filter(doc => !this._renderCutoffData.get(doc[Id]));
             const loadIncrement = this.Document.isTemplateDoc ? Number.MAX_VALUE : 5;
             for (let i = 0; i < Math.min(layoutUnrendered.length, loadIncrement); i++) {
diff --git a/src/client/views/collections/collectionSchema/SchemaTableCell.tsx b/src/client/views/collections/collectionSchema/SchemaTableCell.tsx
index b017eb62b..5874364e0 100644
--- a/src/client/views/collections/collectionSchema/SchemaTableCell.tsx
+++ b/src/client/views/collections/collectionSchema/SchemaTableCell.tsx
@@ -25,9 +25,9 @@ import { Transform } from '../../../util/Transform';
 import { undoBatch, undoable } from '../../../util/UndoManager';
 import { EditableView } from '../../EditableView';
 import { ObservableReactComponent } from '../../ObservableReactComponent';
-import { DefaultStyleProvider } from '../../StyleProvider';
+import { DefaultStyleProvider, returnEmptyDocViewList } from '../../StyleProvider';
 import { Colors } from '../../global/globalEnums';
-import { DocFocusOrOpen, returnEmptyDocViewList } from '../../nodes/DocumentView';
+import { DocumentView } from '../../nodes/DocumentView';
 import { FieldViewProps } from '../../nodes/FieldView';
 import { FormattedTextBox } from '../../nodes/formattedText/FormattedTextBox';
 import { FInfotoColType } from './CollectionSchemaView';
@@ -59,6 +59,14 @@ export interface SchemaTableCellProps {
     rootSelected?: () => boolean;
 }
 
+function selectedCell(props: SchemaTableCellProps) {
+    return (
+        props.isRowActive() &&
+        props.selectedCol() === props.col && //
+        props.selectedCells()?.filter(d => d === props.Document)?.length
+    );
+}
+
 @observer
 export class SchemaTableCell extends ObservableReactComponent<SchemaTableCellProps> {
     constructor(props: SchemaTableCellProps) {
@@ -67,7 +75,7 @@ export class SchemaTableCell extends ObservableReactComponent<SchemaTableCellPro
     }
 
     static addFieldDoc = (docs: Doc | Doc[] /* , where: OpenWhere */) => {
-        DocFocusOrOpen(toList(docs)[0]);
+        DocumentView.FocusOrOpen(toList(docs)[0]);
         return true;
     };
     public static renderProps(props: SchemaTableCellProps) {
@@ -114,11 +122,6 @@ export class SchemaTableCell extends ObservableReactComponent<SchemaTableCellPro
         return { color, textDecoration, fieldProps, cursor, pointerEvents };
     }
 
-    @computed get selected() {
-        const selectedDocs: Doc[] | undefined = this._props.selectedCells();
-        return this._props.isRowActive() && selectedDocs?.filter(doc => doc === this._props.Document).length !== 0 && this._props.selectedCol() === this._props.col;
-    }
-
     @computed get defaultCellContent() {
         const { color, textDecoration, fieldProps, pointerEvents } = SchemaTableCell.renderProps(this._props);
 
@@ -132,12 +135,12 @@ export class SchemaTableCell extends ObservableReactComponent<SchemaTableCellPro
                     pointerEvents,
                 }}>
                 <EditableView
-                    ref={r => this.selected && this._props.autoFocus && r?.setIsFocused(true)}
+                    ref={r => selectedCell(this._props) && this._props.autoFocus && r?.setIsFocused(true)}
                     oneLine={this._props.oneLine}
                     allowCRs={this._props.allowCRs}
                     contents={undefined}
                     fieldContents={fieldProps}
-                    editing={this.selected ? undefined : false}
+                    editing={selectedCell(this._props) ? undefined : false}
                     GetValue={() => Field.toKeyValueString(fieldProps.Document, this._props.fieldKey, SnappingManager.MetaKey)}
                     SetValue={undoable((value: string, shiftDown?: boolean, enterKey?: boolean) => {
                         if (shiftDown && enterKey) {
@@ -157,30 +160,27 @@ export class SchemaTableCell extends ObservableReactComponent<SchemaTableCellPro
     get getCellType() {
         const columnTypeStr = this._props.getFinfo(this._props.fieldKey)?.fieldType;
         const cellValue = this._props.Document[this._props.fieldKey];
+
         if (cellValue instanceof ImageField) return ColumnType.Image;
         if (cellValue instanceof DateField) return ColumnType.Date;
         if (cellValue instanceof RichTextField) return ColumnType.RTF;
         if (typeof cellValue === 'number') return ColumnType.Any;
         if (typeof cellValue === 'string' && columnTypeStr !== FInfoFieldType.enumeration) return ColumnType.Any;
         if (typeof cellValue === 'boolean') return ColumnType.Boolean;
-
-        if (columnTypeStr && columnTypeStr in FInfotoColType) {
-            return FInfotoColType[columnTypeStr];
-        }
+        if (columnTypeStr && columnTypeStr in FInfotoColType) return FInfotoColType[columnTypeStr];
 
         return ColumnType.Any;
     }
 
     get content() {
-        const cellType: ColumnType = this.getCellType;
         // prettier-ignore
-        switch (cellType) {
-            case ColumnType.Image: return <SchemaImageCell {...this._props} />;
-            case ColumnType.Boolean: return <SchemaBoolCell {...this._props} />;
-            case ColumnType.RTF: return <SchemaRTFCell {...this._props} />;
+        switch (this.getCellType) {
+            case ColumnType.Image:       return <SchemaImageCell {...this._props} />;
+            case ColumnType.Boolean:     return <SchemaBoolCell {...this._props} />;
+            case ColumnType.RTF:         return <SchemaRTFCell {...this._props} />;
             case ColumnType.Enumeration: return <SchemaEnumerationCell {...this._props} options={this._props.getFinfo(this._props.fieldKey)?.values?.map(val => Field.toString(val))} />;
-            case ColumnType.Date: return <SchemaDateCell {...this._props} />;
-            default:  return this.defaultCellContent;
+            case ColumnType.Date:        return <SchemaDateCell {...this._props} />;
+            default:                     return this.defaultCellContent;
         }
     }
 
@@ -193,13 +193,13 @@ export class SchemaTableCell extends ObservableReactComponent<SchemaTableCellPro
                     const shift: boolean = e.shiftKey;
                     const ctrl: boolean = e.ctrlKey;
                     if (this._props.isRowActive?.() !== false) {
-                        if (this.selected && ctrl) {
+                        if (selectedCell(this._props) && ctrl) {
                             this._props.selectCell(this._props.Document, this._props.col, shift, ctrl);
                             e.stopPropagation();
-                        } else !this.selected && this._props.selectCell(this._props.Document, this._props.col, shift, ctrl);
+                        } else !selectedCell(this._props) && this._props.selectCell(this._props.Document, this._props.col, shift, ctrl);
                     }
                 })}
-                style={{ padding: this._props.padding, maxWidth: this._props.maxWidth?.(), width: this._props.columnWidth() || undefined, border: this.selected ? `solid 2px ${Colors.MEDIUM_BLUE}` : undefined }}>
+                style={{ padding: this._props.padding, maxWidth: this._props.maxWidth?.(), width: this._props.columnWidth() || undefined, border: selectedCell(this._props) ? `solid 2px ${Colors.MEDIUM_BLUE}` : undefined }}>
                 {this.content}
             </div>
         );
@@ -329,20 +329,14 @@ export class SchemaRTFCell extends ObservableReactComponent<SchemaTableCellProps
         makeObservable(this);
     }
 
-    @computed get selected() {
-        const selected = this._props.selectedCells();
-        return this._props.isRowActive() && selected && selected?.filter(doc => doc === this._props.Document).length !== 0 && this._props.selectedCol() === this._props.col;
-        // return this._props.isRowActive() && selected?.[0] === this._props.Document && selected[1] === this._props.col;
-    }
-
     // if the text box blurs and none of its contents are focused(), then the edit finishes
-    selectedFunc = () => this.selected;
+    selectedFunc = () => !!selectedCell(this._props);
     render() {
         const { color, textDecoration, fieldProps, cursor, pointerEvents } = SchemaTableCell.renderProps(this._props);
         fieldProps.isContentActive = this.selectedFunc;
         return (
-            <div className="schemaRTFCell" style={{ fontStyle: this.selected ? undefined : 'italic', color, textDecoration, cursor, pointerEvents }}>
-                {this.selected ? <FormattedTextBox {...fieldProps} autoFocus onBlur={() => this._props.finishEdit?.()} /> : (field => (field ? Field.toString(field) : ''))(FieldValue(fieldProps.Document[fieldProps.fieldKey]))}
+            <div className="schemaRTFCell" style={{ fontStyle: selectedCell(this._props) ? undefined : 'italic', color, textDecoration, cursor, pointerEvents }}>
+                {selectedCell(this._props) ? <FormattedTextBox {...fieldProps} autoFocus onBlur={() => this._props.finishEdit?.()} /> : (field => (field ? Field.toString(field) : ''))(FieldValue(fieldProps.Document[fieldProps.fieldKey]))}
             </div>
         );
     }
@@ -354,10 +348,6 @@ export class SchemaBoolCell extends ObservableReactComponent<SchemaTableCellProp
         makeObservable(this);
     }
 
-    @computed get selected() {
-        const selected = this._props.selectedCells();
-        return this._props.isRowActive() && selected && selected?.filter(doc => doc === this._props.Document).length !== 0 && this._props.selectedCol() === this._props.col;
-    }
     render() {
         const { color, textDecoration, fieldProps, cursor, pointerEvents } = SchemaTableCell.renderProps(this._props);
         return (
@@ -375,7 +365,7 @@ export class SchemaBoolCell extends ObservableReactComponent<SchemaTableCellProp
                 <EditableView
                     contents={undefined}
                     fieldContents={fieldProps}
-                    editing={this.selected ? undefined : false}
+                    editing={selectedCell(this._props) ? undefined : false}
                     GetValue={() => Field.toKeyValueString(this._props.Document, this._props.fieldKey)}
                     SetValue={undoBatch((value: string, shiftDown?: boolean, enterKey?: boolean) => {
                         if (shiftDown && enterKey) {
@@ -399,10 +389,6 @@ export class SchemaEnumerationCell extends ObservableReactComponent<SchemaTableC
         makeObservable(this);
     }
 
-    @computed get selected() {
-        const selected = this._props.selectedCells();
-        return this._props.isRowActive() && selected && selected?.filter(doc => doc === this._props.Document).length !== 0 && this._props.selectedCol() === this._props.col;
-    }
     render() {
         const { color, textDecoration, cursor, pointerEvents } = SchemaTableCell.renderProps(this._props);
         const options = this._props.options?.map(facet => ({ value: facet, label: facet }));
diff --git a/src/client/views/linking/LinkPopup.tsx b/src/client/views/linking/LinkPopup.tsx
index 9fb1c0fdc..76a8396ff 100644
--- a/src/client/views/linking/LinkPopup.tsx
+++ b/src/client/views/linking/LinkPopup.tsx
@@ -5,8 +5,7 @@ import { returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue } from '
 import { emptyFunction } from '../../../Utils';
 import { Doc } from '../../../fields/Doc';
 import { Transform } from '../../util/Transform';
-import { DefaultStyleProvider } from '../StyleProvider';
-import { returnEmptyDocViewList } from '../nodes/DocumentView';
+import { DefaultStyleProvider, returnEmptyDocViewList } from '../StyleProvider';
 import { SearchBox } from '../search/SearchBox';
 import './LinkPopup.scss';
 
diff --git a/src/client/views/newlightbox/ButtonMenu/ButtonMenu.tsx b/src/client/views/newlightbox/ButtonMenu/ButtonMenu.tsx
index e48e993cf..3eb99f47a 100644
--- a/src/client/views/newlightbox/ButtonMenu/ButtonMenu.tsx
+++ b/src/client/views/newlightbox/ButtonMenu/ButtonMenu.tsx
@@ -1,24 +1,27 @@
+/* eslint-disable jsx-a11y/no-static-element-interactions */
+/* eslint-disable jsx-a11y/click-events-have-key-events */
 import { action } from 'mobx';
 import * as React from 'react';
 import { Doc } from '../../../../fields/Doc';
 import { InkTool } from '../../../../fields/InkField';
 import { SnappingManager } from '../../../util/SnappingManager';
 import { CollectionDockingView } from '../../collections/CollectionDockingView';
+import { DocumentView } from '../../nodes/DocumentView';
 import { OpenWhereMod } from '../../nodes/OpenWhere';
 import { NewLightboxView } from '../NewLightboxView';
 import './ButtonMenu.scss';
-import { IButtonMenu } from './utils';
 
-export const ButtonMenu = (props: IButtonMenu) => {
+export function ButtonMenu() {
     return (
-        <div className={`newLightboxButtonMenu-container`}>
+        <div className="newLightboxButtonMenu-container">
             <div
                 className="newLightboxView-navBtn"
                 title="toggle fit width"
                 onClick={e => {
                     e.stopPropagation();
                     NewLightboxView.LightboxDoc!._fitWidth = !NewLightboxView.LightboxDoc!._fitWidth;
-                }}></div>
+                }}
+            />
             <div
                 className="newLightboxView-tabBtn"
                 title="open in tab"
@@ -27,7 +30,8 @@ export const ButtonMenu = (props: IButtonMenu) => {
                     CollectionDockingView.AddSplit(NewLightboxView.LightboxDoc || NewLightboxView.LightboxDoc!, OpenWhereMod.none);
                     DocumentView.DeselectAll();
                     NewLightboxView.SetNewLightboxDoc(undefined);
-                }}></div>
+                }}
+            />
             <div
                 className="newLightboxView-penBtn"
                 title="toggle pen annotation"
@@ -35,7 +39,8 @@ export const ButtonMenu = (props: IButtonMenu) => {
                 onClick={e => {
                     e.stopPropagation();
                     Doc.ActiveTool = Doc.ActiveTool === InkTool.Pen ? InkTool.None : InkTool.Pen;
-                }}></div>
+                }}
+            />
             <div
                 className="newLightboxView-exploreBtn"
                 title="toggle explore mode to navigate among documents only"
@@ -43,7 +48,8 @@ export const ButtonMenu = (props: IButtonMenu) => {
                 onClick={action(e => {
                     e.stopPropagation();
                     SnappingManager.SetExploreMode(!SnappingManager.ExploreMode);
-                })}></div>
+                })}
+            />
         </div>
     );
-};
+}
diff --git a/src/client/views/newlightbox/ExploreView/ExploreView.tsx b/src/client/views/newlightbox/ExploreView/ExploreView.tsx
index a1d6375c4..f8c07cc43 100644
--- a/src/client/views/newlightbox/ExploreView/ExploreView.tsx
+++ b/src/client/views/newlightbox/ExploreView/ExploreView.tsx
@@ -1,32 +1,34 @@
-import './ExploreView.scss';
-import { IBounds, IExploreView, emptyBounds } from './utils';
-import { IRecommendation } from '../components';
+/* eslint-disable jsx-a11y/no-static-element-interactions */
+/* eslint-disable jsx-a11y/click-events-have-key-events */
 import * as React from 'react';
-import { NewLightboxView } from '../NewLightboxView';
 import { StrCast } from '../../../../fields/Types';
+import { NewLightboxView } from '../NewLightboxView';
+import './ExploreView.scss';
+import { IExploreView, emptyBounds } from './utils';
 
-export const ExploreView = (props: IExploreView) => {
+export function ExploreView(props: IExploreView) {
     const { recs, bounds = emptyBounds } = props;
 
     return (
-        <div className={`exploreView-container`}>
+        <div className="exploreView-container">
             {recs &&
                 recs.map(rec => {
-                    const x_bound: number = Math.max(Math.abs(bounds.max_x), Math.abs(bounds.min_x));
-                    const y_bound: number = Math.max(Math.abs(bounds.max_y), Math.abs(bounds.min_y));
+                    const xBound: number = Math.max(Math.abs(bounds.max_x), Math.abs(bounds.min_x));
+                    const yBound: number = Math.max(Math.abs(bounds.max_y), Math.abs(bounds.min_y));
                     if (rec.embedding) {
-                        const x = (rec.embedding.x / x_bound) * 50;
-                        const y = (rec.embedding.y / y_bound) * 50;
+                        const x = (rec.embedding.x / xBound) * 50;
+                        const y = (rec.embedding.y / yBound) * 50;
                         return (
-                            <div className={`exploreView-doc`} onClick={() => {}} style={{ top: `calc(50% + ${y}%)`, left: `calc(50% + ${x}%)` }}>
+                            <div className="exploreView-doc" onClick={() => {}} style={{ top: `calc(50% + ${y}%)`, left: `calc(50% + ${x}%)` }}>
                                 {rec.title}
                             </div>
                         );
-                    } else return null;
+                    }
+                    return null;
                 })}
-            <div className={`exploreView-doc`} style={{ top: `calc(50% + ${0}%)`, left: `calc(50% + ${0}%)`, background: '#073763', color: 'white' }}>
+            <div className="exploreView-doc" style={{ top: `calc(50% + ${0}%)`, left: `calc(50% + ${0}%)`, background: '#073763', color: 'white' }}>
                 {StrCast(NewLightboxView.LightboxDoc?.title)}
             </div>
         </div>
     );
-};
+}
diff --git a/src/client/views/newlightbox/Header/LightboxHeader.tsx b/src/client/views/newlightbox/Header/LightboxHeader.tsx
index 51bfaa4e5..882d28fba 100644
--- a/src/client/views/newlightbox/Header/LightboxHeader.tsx
+++ b/src/client/views/newlightbox/Header/LightboxHeader.tsx
@@ -4,8 +4,8 @@ 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 { DocumentView } from '../../nodes/DocumentView';
 import { NewLightboxView } from '../NewLightboxView';
 import { EditableText } from '../components/EditableText';
 import { getType } from '../utils';
@@ -14,11 +14,11 @@ import { INewLightboxHeader } from './utils';
 
 export function NewLightboxHeader(props: INewLightboxHeader) {
     const { height = 100, width } = props;
-    const [doc, setDoc] = React.useState<Doc | undefined>(LightboxView.LightboxDoc);
+    const [doc, setDoc] = React.useState<Doc | undefined>(DocumentView.LightboxDoc());
     const [editing, setEditing] = React.useState<boolean>(false);
     const [title, setTitle] = React.useState<JSX.Element | null>(null);
     React.useEffect(() => {
-        const lbDoc = LightboxView.LightboxDoc;
+        const lbDoc = DocumentView.LightboxDoc();
         setDoc(lbDoc);
         if (lbDoc) {
             setTitle(
@@ -32,7 +32,7 @@ export function NewLightboxHeader(props: INewLightboxHeader) {
                 />
             );
         }
-    }, [LightboxView.LightboxDoc]);
+    }, [DocumentView.LightboxDoc()]);
 
     const [saved, setSaved] = React.useState<boolean>(false);
 
diff --git a/src/client/views/newlightbox/NewLightboxView.tsx b/src/client/views/newlightbox/NewLightboxView.tsx
index 558ce7e38..c86ddb745 100644
--- a/src/client/views/newlightbox/NewLightboxView.tsx
+++ b/src/client/views/newlightbox/NewLightboxView.tsx
@@ -1,6 +1,5 @@
 /* eslint-disable jsx-a11y/no-static-element-interactions */
 /* eslint-disable jsx-a11y/click-events-have-key-events */
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { action, computed, observable } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
@@ -12,7 +11,6 @@ import { Cast, NumCast, StrCast, toList } from '../../../fields/Types';
 import { SnappingManager } from '../../util/SnappingManager';
 import { Transform } from '../../util/Transform';
 import { GestureOverlay } from '../GestureOverlay';
-import { LightboxView } from '../LightboxView';
 import { DefaultStyleProvider } from '../StyleProvider';
 import { DocumentView } from '../nodes/DocumentView';
 import { OpenWhere } from '../nodes/OpenWhere';
@@ -108,7 +106,7 @@ export class NewLightboxView extends React.Component<LightboxViewProps> {
             DocumentView.CurrentlyPlaying?.forEach(dv => dv.ComponentView?.Pause?.());
             // DocumentView.PinDoc(doc, { hidePresBox: true });
             this._history ? this._history.push({ doc, target }) : (this._history = [{ doc, target }]);
-            if (doc !== LightboxView.LightboxDoc) {
+            if (doc !== DocumentView.LightboxDoc()) {
                 this._savedState = {
                     layout_fieldKey: StrCast(doc.layout_fieldKey),
                     panX: Cast(doc.freeform_panX, 'number', null),
@@ -151,11 +149,12 @@ export class NewLightboxView extends React.Component<LightboxViewProps> {
             if (NewLightboxView._history?.lastElement().target !== target) NewLightboxView._history?.push({ doc, target });
         } else if (!target && NewLightboxView.path.length) {
             const saved = NewLightboxView._savedState;
-            if (LightboxView.LightboxDoc && saved) {
-                LightboxView.LightboxDoc._freeform_panX = saved.panX;
-                LightboxView.LightboxDoc._freeform_panY = saved.panY;
-                LightboxView.LightboxDoc._freeform_scale = saved.scale;
-                LightboxView.LightboxDoc._layout_scrollTop = saved.scrollTop;
+            const lightboxDoc = DocumentView.LightboxDoc();
+            if (lightboxDoc && saved) {
+                lightboxDoc._freeform_panX = saved.panX;
+                lightboxDoc._freeform_panY = saved.panY;
+                lightboxDoc._freeform_scale = saved.scale;
+                lightboxDoc._layout_scrollTop = saved.scrollTop;
             }
             const pop = NewLightboxView.path.pop();
             if (pop) {
@@ -176,7 +175,7 @@ export class NewLightboxView extends React.Component<LightboxViewProps> {
             NewLightboxView.SetNewLightboxDoc(undefined);
             return;
         }
-        const { doc, target } = NewLightboxView._history?.lastElement();
+        const { doc, target } = NewLightboxView._history?.lastElement() ?? { doc: undefined, target: undefined };
         const docView = DocumentView.getLightboxDocumentView(target || doc);
         if (docView) {
             NewLightboxView._docTarget = target;
@@ -248,14 +247,15 @@ export class NewLightboxView extends React.Component<LightboxViewProps> {
 
     @computed
     get documentView() {
-        if (!LightboxView.LightboxDoc) return null;
+        const lightboxDoc = DocumentView.LightboxDoc();
+        if (!lightboxDoc) return null;
         return (
             <GestureOverlay isActive>
                 <DocumentView
                     ref={action((r: DocumentView | null) => {
                         NewLightboxView._docView = r !== null ? r : undefined;
                     })}
-                    Document={LightboxView.LightboxDoc}
+                    Document={lightboxDoc}
                     PanelWidth={this.newLightboxWidth}
                     PanelHeight={this.newLightboxHeight}
                     LayoutTemplate={NewLightboxView.LightboxDocTemplate}
@@ -281,50 +281,14 @@ export class NewLightboxView extends React.Component<LightboxViewProps> {
     newLightboxWidth = () => this.props.PanelWidth - 420;
     newLightboxHeight = () => this.props.PanelHeight - 140;
     newLightboxScreenToLocal = () => new Transform(-this.leftBorder, -this.topBorder, 1);
-    navBtn = (left: Opt<string | number>, bottom: Opt<number>, top: number, icon: string, display: () => string, click: (e: React.MouseEvent) => void, color?: string) => (
-        <div
-            className="newLightboxView-navBtn-frame"
-            style={{
-                display: display(),
-                left,
-                width: bottom !== undefined ? undefined : Math.min(this.props.PanelWidth / 4, this.props.maxBorder[0]),
-                bottom,
-            }}>
-            <div className="newLightboxView-navBtn" title={color} style={{ top, color: color ? 'red' : 'white', background: color ? 'white' : undefined }} onClick={click}>
-                <div style={{ height: 10 }}>{color}</div>
-                <FontAwesomeIcon icon={icon as any} size="3x" />
-            </div>
-        </div>
-    );
 
     docFilters = () => NewLightboxView._docFilters || [];
-    @action
-    stepInto = () => {
-        NewLightboxView.path.push({
-            doc: LightboxView.LightboxDoc,
-            target: NewLightboxView._docTarget,
-            future: NewLightboxView._future,
-            history: NewLightboxView._history,
-            saved: NewLightboxView._savedState,
-        });
-        const coll = NewLightboxView._docTarget;
-        if (coll) {
-            const fieldKey = Doc.LayoutFieldKey(coll);
-            const contents = [...DocListCast(coll[fieldKey]), ...DocListCast(coll[fieldKey + '_annotations'])];
-            const links = Doc.Links(coll)
-                .map(link => Doc.getOppositeAnchor(link, coll))
-                .filter(doc => doc)
-                .map(doc => doc!);
-            NewLightboxView.SetNewLightboxDoc(coll, undefined, contents.length ? contents : links);
-        }
-    };
 
-    future = () => NewLightboxView._future;
     render() {
         const newLightboxHeaderHeight = 100;
         let downx = 0;
         let downy = 0;
-        return !LightboxView.LightboxDoc ? null : (
+        return !DocumentView.LightboxDoc() ? null : (
             <div
                 className="newLightboxView-frame"
                 onPointerDown={e => {
@@ -376,7 +340,7 @@ export class NewLightboxTourBtn extends React.Component<NewLightboxTourBtnProps>
             0,
             0,
             'chevron-down',
-            () => (LightboxView.LightboxDoc /* && this.props.future()?.length */ ? '' : 'none'),
+            () => (DocumentView.LightboxDoc() /* && this.props.future()?.length */ ? '' : 'none'),
             e => {
                 e.stopPropagation();
                 this.props.stepInto();
diff --git a/src/client/views/newlightbox/RecommendationList/RecommendationList.tsx b/src/client/views/newlightbox/RecommendationList/RecommendationList.tsx
index 1d502b73f..dc3339cd3 100644
--- a/src/client/views/newlightbox/RecommendationList/RecommendationList.tsx
+++ b/src/client/views/newlightbox/RecommendationList/RecommendationList.tsx
@@ -1,3 +1,7 @@
+/* eslint-disable react/jsx-props-no-spreading */
+/* eslint-disable jsx-a11y/no-static-element-interactions */
+/* eslint-disable jsx-a11y/click-events-have-key-events */
+/* eslint-disable guard-for-in */
 import { IconButton, Size, Type } from 'browndash-components';
 import * as React from 'react';
 import { FaCaretDown, FaCaretUp } from 'react-icons/fa';
@@ -5,17 +9,15 @@ import { GrClose } from 'react-icons/gr';
 import { DocListCast, StrListCast } from '../../../../fields/Doc';
 import { List } from '../../../../fields/List';
 import { StrCast } from '../../../../fields/Types';
-import { LightboxView } from '../../LightboxView';
 import { Colors } from '../../global/globalEnums';
+import { DocumentView } from '../../nodes/DocumentView';
 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;
+export function RecommendationList() {
     const [loadingKeywords, setLoadingKeywords] = React.useState<boolean>(true);
     const [showMore, setShowMore] = React.useState<boolean>(false);
     const [keywordsLoc, setKeywordsLoc] = React.useState<string[]>([]);
@@ -25,21 +27,22 @@ export const RecommendationList = (props: IRecommendationList) => {
 
     React.useEffect(() => {
         const getKeywords = async () => {
-            let text = StrCast(LightboxView.LightboxDoc?.text);
+            const text = StrCast(DocumentView.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) {
+            const lightboxDoc = DocumentView.LightboxDoc();
+            if (lightboxDoc) {
                 console.log('setting keywords on doc');
-                LightboxView.LightboxDoc.keywords = new List<string>(kw);
+                lightboxDoc.keywords = new List<string>(kw);
                 setKeywordsLoc(NewLightboxView.Keywords);
             }
             setLoadingKeywords(false);
         };
-        let keywordsList = StrListCast(LightboxView.LightboxDoc!.keywords);
+        const keywordsList = StrListCast(DocumentView.LightboxDoc()!.keywords);
         if (!keywordsList || keywordsList.length < 2) {
             setLoadingKeywords(true);
             getKeywords();
@@ -57,14 +60,14 @@ export const RecommendationList = (props: IRecommendationList) => {
             console.log('fetching recommendations');
             let query = 'undefined';
             if (keywordsLoc) query = keywordsLoc.join(',');
-            let src = StrCast(NewLightboxView.LightboxDoc?.text);
-            let dashDocs: IDocRequest[] = [];
+            const src = StrCast(NewLightboxView.LightboxDoc?.text);
+            const dashDocs: IDocRequest[] = [];
             // get linked docs
-            let linkedDocs = DocListCast(NewLightboxView.LightboxDoc?.links);
+            const 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]
+            // let contextDocs: Doc[] = DocListCast(DocCast(DocumentView.LightboxDoc()?.context).data)
+            // let docId = DocumentView.LightboxDoc() && DocumentView.LightboxDoc()[Id]
             // console.log("context docs", contextDocs)
             // contextDocs.forEach((doc: Doc) => {
             //     if (docId !== doc[Id]){
@@ -79,10 +82,8 @@ export const RecommendationList = (props: IRecommendationList) => {
             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_bounds: IBounds = {
+                const theRecs = response.recommendations;
+                const responseBounds: IBounds = {
                     max_x: response.max_x,
                     max_y: response.max_y,
                     min_x: response.min_x,
@@ -93,22 +94,23 @@ export const RecommendationList = (props: IRecommendationList) => {
                 //     setKeywordsLoc(NewLightboxView.Keywords);
                 // }
                 // console.log(response_bounds)
-                NewLightboxView.SetBounds(response_bounds);
+                NewLightboxView.SetBounds(responseBounds);
                 const recommendations: IRecommendation[] = [];
-                for (const key in recs) {
+                // eslint-disable-next-line no-restricted-syntax
+                for (const key in theRecs) {
                     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 &&
+                    const { title } = theRecs[key];
+                    const { url } = theRecs[key];
+                    const { type } = theRecs[key];
+                    const { text } = theRecs[key];
+                    const { transcript } = theRecs[key];
+                    const { previewUrl } = theRecs[key];
+                    const { embedding } = theRecs[key];
+                    const { distance } = theRecs[key];
+                    const { source } = theRecs[key];
+                    const { related_concepts: relatedConcepts } = theRecs[key];
+                    const docId = theRecs[key].doc_id;
+                    relatedConcepts.length >= 1 &&
                         recommendations.push({
                             title: title,
                             data: url,
@@ -119,14 +121,15 @@ export const RecommendationList = (props: IRecommendationList) => {
                             embedding: embedding,
                             distance: Math.round(distance * 100) / 100,
                             source: source,
-                            related_concepts: related_concepts,
+                            related_concepts: relatedConcepts,
                             docId: docId,
                         });
                 }
                 recommendations.sort((a, b) => {
                     if (a.distance && b.distance) {
                         return a.distance - b.distance;
-                    } else return 0;
+                    }
+                    return 0;
                 });
                 console.log('[rec]: ', recommendations);
                 NewLightboxView.SetRecs(recommendations);
@@ -138,12 +141,12 @@ export const RecommendationList = (props: IRecommendationList) => {
 
     return (
         <div
-            className={`recommendationlist-container`}
+            className="recommendationlist-container"
             onPointerDown={e => {
                 e.stopPropagation();
             }}>
-            <div className={`header`}>
-                <div className={`title`}>Recommendations</div>
+            <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{' '}
@@ -153,65 +156,58 @@ export const RecommendationList = (props: IRecommendationList) => {
                         . The following keywords are used to fetch the recommendations.
                     </div>
                 )}
-                <div className={`lb-label`}>Keywords</div>
+                <div className="lb-label">Keywords</div>
                 {loadingKeywords ? (
-                    <div className={`keywords`}>
+                    <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 className={`keywords`}>
+                    <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>
-                                );
-                            })}
+                            keywordsLoc.map((word, ind) => (
+                                <div className="keyword">
+                                    {word}
+                                    <IconButton
+                                        type={Type.PRIM}
+                                        size={Size.XSMALL}
+                                        color={Colors.DARK_GRAY}
+                                        icon={<GrClose />}
+                                        onClick={() => {
+                                            const kw = keywordsLoc;
+                                            kw.splice(ind);
+                                            NewLightboxView.SetKeywords(kw);
+                                        }}
+                                    />
+                                </div>
+                            ))}
                     </div>
                 )}
                 {!showMore ? (
                     <div
-                        className={`lb-caret`}
+                        className="lb-caret"
                         onClick={() => {
                             setShowMore(true);
                         }}>
                         More <FaCaretDown />
                     </div>
                 ) : (
-                    <div className={`more`}>
+                    <div className="more">
                         <div
-                            className={`lb-caret`}
+                            className="lb-caret"
                             onClick={() => {
                                 setShowMore(false);
                             }}>
                             Less <FaCaretUp />
                         </div>
-                        <div className={`lb-label`}>Type</div>
-                        <div className={`lb-label`}>Sources</div>
+                        <div className="lb-label">Type</div>
+                        <div className="lb-label">Sources</div>
                     </div>
                 )}
             </div>
-            <div className={`recommendations`}>
-                {recs &&
-                    recs.map((rec: IRecommendation) => {
-                        return <Recommendation {...rec} />;
-                    })}
-            </div>
+            <div className="recommendations">{recs && recs.map((rec: IRecommendation) => <Recommendation {...rec} />)}</div>
         </div>
     );
-};
+}
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
index 685a5aca4..62c4cc61a 100644
--- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
@@ -296,9 +296,9 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
                     <div style={{ position: 'absolute', width: this.PanelWidth(), height: this.PanelHeight(), background: 'lightGreen' }} />
                 ) : (
                     <DocumentView
-                        parent={this}
                         // eslint-disable-next-line react/jsx-props-no-spreading
                         {...OmitKeys(this._props,this.WrapperKeys.map(val => val.lower)).omit} // prettier-ignore
+                        parent={this}
                         DataTransition={this.DataTransition}
                         LocalRotation={this.localRotation}
                         CollectionFreeFormDocumentView={this.returnThis}
diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx
index 474d54119..e1d16549c 100644
--- a/src/client/views/nodes/ComparisonBox.tsx
+++ b/src/client/views/nodes/ComparisonBox.tsx
@@ -13,7 +13,7 @@ import { Docs } from '../../documents/Documents';
 import { DragManager } from '../../util/DragManager';
 import { dropActionType } from '../../util/DropActionTypes';
 import { undoBatch } from '../../util/UndoManager';
-import { ViewBoxAnnotatableComponent, ViewBoxInterface } from '../DocComponent';
+import { ViewBoxAnnotatableComponent } from '../DocComponent';
 import { PinDocView, PinProps } from '../PinFuncs';
 import { StyleProp } from '../StyleProp';
 import './ComparisonBox.scss';
diff --git a/src/client/views/nodes/DataVizBox/DataVizBox.tsx b/src/client/views/nodes/DataVizBox/DataVizBox.tsx
index 15187b4e4..9ca63194c 100644
--- a/src/client/views/nodes/DataVizBox/DataVizBox.tsx
+++ b/src/client/views/nodes/DataVizBox/DataVizBox.tsx
@@ -18,7 +18,7 @@ import { DocumentType } from '../../../documents/DocumentTypes';
 import { Docs } from '../../../documents/Documents';
 import { UndoManager, undoable } from '../../../util/UndoManager';
 import { ContextMenu } from '../../ContextMenu';
-import { ViewBoxAnnotatableComponent, ViewBoxInterface } from '../../DocComponent';
+import { ViewBoxAnnotatableComponent } from '../../DocComponent';
 import { MarqueeAnnotator } from '../../MarqueeAnnotator';
 import { PinProps } from '../../PinFuncs';
 import { SidebarAnnos } from '../../SidebarAnnos';
@@ -41,7 +41,7 @@ export enum DataVizView {
 }
 
 @observer
-export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implements ViewBoxInterface {
+export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
     private _mainCont: React.RefObject<HTMLDivElement> = React.createRef();
     private _marqueeref = React.createRef<MarqueeAnnotator>();
     private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef();
diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx
index ec9db8480..18529a429 100644
--- a/src/client/views/nodes/DocumentContentsView.tsx
+++ b/src/client/views/nodes/DocumentContentsView.tsx
@@ -2,7 +2,6 @@
 import { computed, makeObservable } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
-import JsxParser from 'react-jsx-parser';
 import * as XRegExp from 'xregexp';
 import { OmitKeys } from '../../../ClientUtils';
 import { Without, emptyPath } from '../../../Utils';
@@ -11,56 +10,15 @@ import { AclPrivate, DocData } from '../../../fields/DocSymbols';
 import { ScriptField } from '../../../fields/ScriptField';
 import { Cast, DocCast, StrCast } from '../../../fields/Types';
 import { GetEffectiveAcl, TraceMobx } from '../../../fields/util';
-import { InkingStroke } from '../InkingStroke';
-import { ObservableReactComponent } from '../ObservableReactComponent';
-import { CollectionCalendarView } from '../collections/CollectionCalendarView';
-import { CollectionDockingView } from '../collections/CollectionDockingView';
-import { CollectionView } from '../collections/CollectionView';
-import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView';
-import { CollectionSchemaView } from '../collections/collectionSchema/CollectionSchemaView';
-import { SchemaRowBox } from '../collections/collectionSchema/SchemaRowBox';
-import { SearchBox } from '../search/SearchBox';
-import { AudioBox } from './AudioBox';
-import { ComparisonBox } from './ComparisonBox';
-import { DataVizBox } from './DataVizBox/DataVizBox';
+import { ObservableReactComponent, ObserverJsxParser } from '../ObservableReactComponent';
 import './DocumentView.scss';
-import { EquationBox } from './EquationBox';
-import { FieldView, FieldViewProps } from './FieldView';
-import { FontIconBox } from './FontIconBox/FontIconBox';
-import { FunctionPlotBox } from './FunctionPlotBox';
-import { ImageBox } from './ImageBox';
-import { KeyValueBox } from './KeyValueBox';
-import { LabelBox } from './LabelBox';
-import { LinkBox } from './LinkBox';
-import { LoadingBox } from './LoadingBox';
-import { MapBox } from './MapBox/MapBox';
-import { MapPushpinBox } from './MapBox/MapPushpinBox';
-import { PDFBox } from './PDFBox';
-import { PhysicsSimulationBox } from './PhysicsBox/PhysicsSimulationBox';
-import { RecordingBox } from './RecordingBox';
-import { ScreenshotBox } from './ScreenshotBox';
-import { ScriptingBox } from './ScriptingBox';
-import { VideoBox } from './VideoBox';
-import { WebBox } from './WebBox';
-import { FormattedTextBox } from './formattedText/FormattedTextBox';
-import { ImportElementBox } from './importBox/ImportElementBox';
-import { PresBox } from './trails/PresBox';
-import { PresElementBox } from './trails/PresElementBox';
+import { FieldViewProps } from './FieldView';
 
 type BindingProps = Without<FieldViewProps, 'fieldKey'>;
 export interface JsxBindings {
     props: BindingProps;
 }
 
-class ObserverJsxParser1 extends JsxParser {
-    constructor(props: any) {
-        super(props);
-        observer(this as any);
-    }
-}
-
-export const ObserverJsxParser: typeof JsxParser = ObserverJsxParser1 as any;
-
 interface HTMLtagProps {
     Document: Doc;
     htmltag: string;
@@ -116,6 +74,15 @@ export interface DocumentContentsViewProps extends FieldViewProps {
 }
 @observer
 export class DocumentContentsView extends ObservableReactComponent<DocumentContentsViewProps> {
+    private static DefaultLayoutString: string;
+    /**
+     * Set of all available rendering componets for Docs (e.g., ImageBox, CollectionFreeFormView, etc)
+     */
+    private static Components: { [key: string]: any };
+    public static Init(defaultLayoutString: string, components:{ [key: string]: any}) {
+        DocumentContentsView.DefaultLayoutString = defaultLayoutString;
+        DocumentContentsView.Components = components;
+    }
     constructor(props: any) {
         super(props);
         makeObservable(this);
@@ -124,11 +91,11 @@ export class DocumentContentsView extends ObservableReactComponent<DocumentConte
         TraceMobx();
         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());
+        if (this._props.layoutFieldKey === 'layout_keyValue') return StrCast(this._props.Document.layout_keyValue, DocumentContentsView.DefaultLayoutString);
         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 (layout === undefined) return this._props.Document.data ? "<FieldView {...props} fieldKey='data' />" : DocumentContentsView.DefaultLayoutString;
         if (typeof layout === 'string') return layout;
         return '<p>Loading layout</p>';
     }
@@ -223,42 +190,7 @@ export class DocumentContentsView extends ObservableReactComponent<DocumentConte
                 key={42}
                 blacklistedAttrs={emptyPath}
                 renderInWrapper={false}
-                components={{
-                    FormattedTextBox,
-                    ImageBox,
-                    FontIconBox,
-                    LabelBox,
-                    EquationBox,
-                    FieldView,
-                    CollectionFreeFormView,
-                    CollectionDockingView,
-                    CollectionSchemaView,
-                    CollectionCalendarView,
-                    CollectionView,
-                    WebBox,
-                    KeyValueBox,
-                    PDFBox,
-                    VideoBox,
-                    AudioBox,
-                    RecordingBox,
-                    PresBox,
-                    PresElementBox,
-                    SearchBox,
-                    FunctionPlotBox,
-                    InkingStroke,
-                    LinkBox,
-                    ScriptingBox,
-                    MapBox,
-                    ScreenshotBox,
-                    DataVizBox,
-                    HTMLtag,
-                    ComparisonBox,
-                    LoadingBox,
-                    PhysicsSimulationBox,
-                    SchemaRowBox,
-                    ImportElementBox,
-                    MapPushpinBox,
-                }}
+                components={DocumentContentsView.Components}
                 bindings={bindings}
                 jsx={layoutFrame}
                 showWarnings
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index dc597e5ff..8df28a770 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -8,7 +8,7 @@ import { observer } from 'mobx-react';
 import * as React from 'react';
 import { Bounce, Fade, Flip, JackInTheBox, Roll, Rotate, Zoom } from 'react-awesome-reveal';
 import { ClientUtils, DivWidth, isTargetChildOf as isParentOf, lightOrDark, returnFalse, returnVal, simulateMouseClick } from '../../../ClientUtils';
-import { Utils, emptyFunction, emptyPath } from '../../../Utils';
+import { Utils, emptyFunction } from '../../../Utils';
 import { Doc, DocListCast, Field, FieldType, Opt, StrListCast } from '../../../fields/Doc';
 import { AclAdmin, AclEdit, AclPrivate, Animation, AudioPlay, DocData, DocViews } from '../../../fields/DocSymbols';
 import { Id } from '../../../fields/FieldSymbols';
@@ -35,20 +35,21 @@ import { SnappingManager } from '../../util/SnappingManager';
 import { UndoManager, undoBatch, undoable } from '../../util/UndoManager';
 import { ContextMenu } from '../ContextMenu';
 import { ContextMenuProps } from '../ContextMenuItem';
-import { DocComponent, ViewBoxInterface } from '../DocComponent';
+import { DocComponent } from '../DocComponent';
 import { EditableView } from '../EditableView';
 import { FieldsDropdown } from '../FieldsDropdown';
-import { LightboxView } from '../LightboxView';
+import { ObserverJsxParser } from '../ObservableReactComponent';
 import { PinProps } from '../PinFuncs';
 import { StyleProp } from '../StyleProp';
-import { DocumentContentsView, ObserverJsxParser } from './DocumentContentsView';
+import { ViewBoxInterface } from '../ViewBoxInterface';
+import { DocumentContentsView } from './DocumentContentsView';
 import { DocumentLinksButton } from './DocumentLinksButton';
 import './DocumentView.scss';
 import { FieldViewProps, FieldViewSharedProps } from './FieldView';
 import { FocusViewOptions } from './FocusViewOptions';
-import { OpenWhere } from './OpenWhere';
+import { OpenWhere, OpenWhereMod } from './OpenWhere';
 import { FormattedTextBox } from './formattedText/FormattedTextBox';
-import { PresEffect, PresEffectDirection } from './trails';
+import { PresEffect, PresEffectDirection } from './trails/PresEnums';
 
 export interface DocumentViewProps extends FieldViewSharedProps {
     hideDecorations?: boolean; // whether to suppress all DocumentDecorations when doc is selected
@@ -77,7 +78,7 @@ export interface DocumentViewProps extends FieldViewSharedProps {
     dragStarting?: () => void;
     dragEnding?: () => void;
 
-    parent?: any;
+    parent?: any; // parent React component view (see CollectionFreeFormDocumentView)
 }
 @observer
 export class DocumentViewInternal extends DocComponent<FieldViewProps & DocumentViewProps>() {
@@ -320,7 +321,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
                 if (this.onDoubleClickHandler?.script) {
                     UndoManager.RunInBatch(() => this.onDoubleClickHandler.script.run(scriptProps, console.log).result?.select && this._props.select(false), 'on double click: ' + this.Document.title);
                 } else if (!Doc.IsSystem(this.Document) && defaultDblclick !== 'ignore') {
-                    UndoManager.RunInBatch(() => LightboxView.Instance.AddDocTab(this.Document, OpenWhere.lightbox), 'double tap');
+                    UndoManager.RunInBatch(() => this._props.addDocTab(this.Document, OpenWhere.lightbox), 'double tap');
                     DocumentView.DeselectAll();
                     Doc.UnBrushDoc(this.Document);
                 } else {
@@ -561,7 +562,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
             const appearanceItems: ContextMenuProps[] = appearance && 'subitems' in appearance ? appearance.subitems : [];
 
             if (this._props.renderDepth === 0) {
-                appearanceItems.splice(0, 0, { description: 'Open in Lightbox', event: () => LightboxView.Instance.SetLightboxDoc(this.Document), icon: 'external-link-alt' });
+                appearanceItems.splice(0, 0, { description: 'Open in Lightbox', event: () => DocumentView.SetLightboxDoc(this.Document), icon: 'external-link-alt' });
             }
             appearanceItems.push({ description: 'Pin', event: () => this._props.pinToPres(this.Document, {}), icon: 'eye' });
             !Doc.noviceMode && templateDoc && appearanceItems.push({ description: 'Open Template   ', event: () => this._props.addDocTab(templateDoc, OpenWhere.addRight), icon: 'eye' });
@@ -980,6 +981,29 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
 @observer
 export class DocumentView extends DocComponent<DocumentViewProps>() {
     public static ROOT_DIV = 'documentView-effectsWrapper';
+    public static addSplit: (Doc: Doc, where: OpenWhereMod) => void;
+    // Lightbox
+    public static _lightboxDoc: () => Doc | undefined;
+    public static _lightboxContains: (view?: DocumentView) => boolean | undefined;
+    public static _setLightboxDoc: (doc: Opt<Doc>, target?: Doc, future?: Doc[], layoutTemplate?: Doc | string) => boolean;
+    /**
+     * @returns The Doc, if any, being displayed in the lightbox
+     */
+    public static readonly LightboxDoc = () => DocumentView._lightboxDoc?.();
+    /**
+     * @param view
+     * @returns whether 'view' is anywhere in the rendering hierarchy of the lightbox
+     */
+    public static readonly LightboxContains = (view?: DocumentView) => DocumentView._lightboxContains?.(view);
+    /**
+     * Sets the root Doc to render in the lightbox view.
+     * @param doc
+     * @param target a Doc within 'doc' to focus on (useful for freeform collections)
+     * @param future a list of Docs to step through with the arrow buttons of the lightbox
+     * @param layoutTemplate a template to apply to 'doc' to render it.
+     * @returns success flag which is currently always true
+     */
+    public static readonly SetLightboxDoc = (doc: Opt<Doc>, target?: Doc, future?: Doc[], layoutTemplate?: Doc | string) => DocumentView._setLightboxDoc(doc, target, future, layoutTemplate);
     // Sharing Manager
     public static ShareOpen: (target?: DocumentView, targetDoc?: Doc) => void;
     // LinkFollower
@@ -998,6 +1022,7 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
     public static addView: (dv: DocumentView) => void | undefined;
     public static removeView: (dv: DocumentView) => void | undefined;
     public static addViewRenderedCb: (doc: Opt<Doc>, func: (dv: DocumentView) => any) => boolean;
+    public static getViews = (doc?: Doc) => Array.from(doc?.[DocViews] ?? []) as DocumentView[];
     public static getFirstDocumentView: (toFind: Doc) => DocumentView | undefined;
     public static getDocumentView: (target: Doc | undefined, preferredCollection?: DocumentView) => Opt<DocumentView>;
     public static getContextPath: (doc: Opt<Doc>, includeExistingViews?: boolean) => Doc[];
@@ -1032,8 +1057,8 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
     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]);
+    public ViewGuid = DocumentView.UniquifyId(DocumentView.LightboxContains(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(DocumentView.LightboxContains(this), this.Document[Id]);
 
     constructor(props: DocumentViewProps) {
         super(props);
@@ -1401,6 +1426,7 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
                         }}>
                         <DocumentViewInternal
                             {...this._props}
+                            parent={undefined}
                             fieldKey={this.LayoutFieldKey}
                             DataTransition={this.DataTransition}
                             DocumentView={this.selfView}
@@ -1451,58 +1477,52 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
         const docId = ClientUtils.CurrentUserEmail() + Doc.GetProto(linkAnchor)[Id] + '-pivotish';
         // prettier-ignore
         DocServer.GetRefField(docId).then(docx =>
-            LightboxView.Instance.SetLightboxDoc(
+            DocumentView.SetLightboxDoc(
                 (docx as Doc) ?? // reuse existing pivot view of documents, or else create a new collection
                 Docs.Create.StackingDocument([], { title: linkAnchor.title + '-pivot', _width: 500, _height: 500, target: linkAnchor, onViewMounted: ScriptField.MakeScript('updateLinkCollection(this, this.target)') }, docId)
             )
         );
     }
-}
-
-export function returnEmptyDocViewList() {
-    return emptyPath;
-}
-
-// eslint-disable-next-line default-param-last
-export function DocFocusOrOpen(docIn: Doc, optionsIn: FocusViewOptions = { willZoomCentered: true, zoomScale: 0, openLocation: OpenWhere.toggleRight }, containingDoc?: Doc) {
-    let doc = docIn;
-    const options = optionsIn;
-    const func = () => {
-        const cv = DocumentView.getDocumentView(containingDoc);
-        const dv = DocumentView.getDocumentView(doc, cv);
-        if (dv && (!containingDoc || dv.containerViewPath?.().lastElement()?.Document === containingDoc)) {
-            DocumentView.showDocumentView(dv, options).then(() => dv && Doc.linkFollowHighlight(dv.Document));
-        } else {
-            const container = DocCast(containingDoc ?? doc.embedContainer ?? Doc.BestEmbedding(doc));
-            const showDoc = !Doc.IsSystem(container) && !cv ? container : doc;
-            options.toggleTarget = undefined;
-            DocumentView.showDocument(showDoc, options, () => DocumentView.showDocument(doc, { ...options, openLocation: undefined })).then(() => {
-                const cvFound = DocumentView.getDocumentView(containingDoc);
-                const dvFound = DocumentView.getDocumentView(doc, cvFound);
-                dvFound && Doc.linkFollowHighlight(dvFound.Document);
-            });
+    // eslint-disable-next-line default-param-last
+    public static FocusOrOpen(docIn: Doc, optionsIn: FocusViewOptions = { willZoomCentered: true, zoomScale: 0, openLocation: OpenWhere.toggleRight }, containingDoc?: Doc) {
+        let doc = docIn;
+        const options = optionsIn;
+        const func = () => {
+            const cv = DocumentView.getDocumentView(containingDoc);
+            const dv = DocumentView.getDocumentView(doc, cv);
+            if (dv && (!containingDoc || dv.containerViewPath?.().lastElement()?.Document === containingDoc)) {
+                DocumentView.showDocumentView(dv, options).then(() => dv && Doc.linkFollowHighlight(dv.Document));
+            } else {
+                const container = DocCast(containingDoc ?? doc.embedContainer ?? Doc.BestEmbedding(doc));
+                const showDoc = !Doc.IsSystem(container) && !cv ? container : doc;
+                options.toggleTarget = undefined;
+                DocumentView.showDocument(showDoc, options, () => DocumentView.showDocument(doc, { ...options, openLocation: undefined })).then(() => {
+                    const cvFound = DocumentView.getDocumentView(containingDoc);
+                    const dvFound = DocumentView.getDocumentView(doc, cvFound);
+                    dvFound && Doc.linkFollowHighlight(dvFound.Document);
+                });
+            }
+        };
+        if (Doc.IsDataProto(doc) && Doc.GetEmbeddings(doc).some(embed => embed.hidden && [AclAdmin, AclEdit].includes(GetEffectiveAcl(embed)))) {
+            doc = Doc.GetEmbeddings(doc).find(embed => embed.hidden && [AclAdmin, AclEdit].includes(GetEffectiveAcl(embed)))!;
         }
-    };
-    if (Doc.IsDataProto(doc) && Doc.GetEmbeddings(doc).some(embed => embed.hidden && [AclAdmin, AclEdit].includes(GetEffectiveAcl(embed)))) {
-        doc = Doc.GetEmbeddings(doc).find(embed => embed.hidden && [AclAdmin, AclEdit].includes(GetEffectiveAcl(embed)))!;
+        if (doc.hidden) {
+            doc.hidden = false;
+            options.toggleTarget = false;
+            setTimeout(func);
+        } else func();
     }
-    if (doc.hidden) {
-        doc.hidden = false;
-        options.toggleTarget = false;
-        setTimeout(func);
-    } else func();
 }
-ScriptingGlobals.add(DocFocusOrOpen);
 
 // eslint-disable-next-line prefer-arrow-callback
-ScriptingGlobals.add(function deiconifyView(documentView: DocumentView) {
-    documentView.iconify();
-    documentView.select(false);
+ScriptingGlobals.add(function DocFocusOrOpen(docIn: Doc, optionsIn?: FocusViewOptions, containingDoc?: Doc) {
+    return DocumentView.FocusOrOpen(docIn, optionsIn, containingDoc);
 });
 
 // eslint-disable-next-line prefer-arrow-callback
-ScriptingGlobals.add(function deiconifyViewToLightbox(documentView: DocumentView) {
-    LightboxView.Instance.AddDocTab(documentView.Document, OpenWhere.lightbox, 'layout'); // , 0);
+ScriptingGlobals.add(function deiconifyView(documentView: DocumentView) {
+    documentView.iconify();
+    documentView.select(false);
 });
 
 // eslint-disable-next-line prefer-arrow-callback
diff --git a/src/client/views/nodes/EquationBox.tsx b/src/client/views/nodes/EquationBox.tsx
index 32d08fbe7..1f5c9b84b 100644
--- a/src/client/views/nodes/EquationBox.tsx
+++ b/src/client/views/nodes/EquationBox.tsx
@@ -11,7 +11,7 @@ import { DocumentType } from '../../documents/DocumentTypes';
 import { Docs } from '../../documents/Documents';
 import { undoBatch } from '../../util/UndoManager';
 import { ViewBoxBaseComponent } from '../DocComponent';
-import { LightboxView } from '../LightboxView';
+import { DocumentView } from './DocumentView';
 import './EquationBox.scss';
 import { FieldView, FieldViewProps } from './FieldView';
 import EquationEditor from './formattedText/EquationEditor';
@@ -30,7 +30,7 @@ export class EquationBox extends ViewBoxBaseComponent<FieldViewProps>() {
 
     componentDidMount() {
         this._props.setContentViewBox?.(this);
-        if (Doc.SelectOnLoad === this.Document && (!LightboxView.LightboxDoc || LightboxView.Contains(this.DocumentView?.()))) {
+        if (Doc.SelectOnLoad === this.Document && (!DocumentView.LightboxDoc() || DocumentView.LightboxContains(this.DocumentView?.()))) {
             this._props.select(false);
 
             this._ref.current!.mathField.focus();
diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx
index 66b134889..3f351a072 100644
--- a/src/client/views/nodes/FieldView.tsx
+++ b/src/client/views/nodes/FieldView.tsx
@@ -10,8 +10,8 @@ import { ScriptField } from '../../../fields/ScriptField';
 import { WebField } from '../../../fields/URLField';
 import { dropActionType } from '../../util/DropActionTypes';
 import { Transform } from '../../util/Transform';
-import { ViewBoxInterface } from '../DocComponent';
 import { PinProps } from '../PinFuncs';
+import { ViewBoxInterface } from '../ViewBoxInterface';
 import { DocumentView } from './DocumentView';
 import { FocusViewOptions } from './FocusViewOptions';
 import { OpenWhere } from './OpenWhere';
diff --git a/src/client/views/nodes/KeyValuePair.tsx b/src/client/views/nodes/KeyValuePair.tsx
index 397cc15ed..0956be3e9 100644
--- a/src/client/views/nodes/KeyValuePair.tsx
+++ b/src/client/views/nodes/KeyValuePair.tsx
@@ -13,8 +13,7 @@ import { undoBatch } from '../../util/UndoManager';
 import { ContextMenu } from '../ContextMenu';
 import { EditableView } from '../EditableView';
 import { ObservableReactComponent } from '../ObservableReactComponent';
-import { DefaultStyleProvider } from '../StyleProvider';
-import { returnEmptyDocViewList } from './DocumentView';
+import { DefaultStyleProvider, returnEmptyDocViewList } from '../StyleProvider';
 import './KeyValueBox.scss';
 import './KeyValuePair.scss';
 import { OpenWhere } from './OpenWhere';
diff --git a/src/client/views/nodes/LinkBox.tsx b/src/client/views/nodes/LinkBox.tsx
index 6caa38a7f..8d6ae9f73 100644
--- a/src/client/views/nodes/LinkBox.tsx
+++ b/src/client/views/nodes/LinkBox.tsx
@@ -16,7 +16,6 @@ import { DocumentType } from '../../documents/DocumentTypes';
 import { SnappingManager } from '../../util/SnappingManager';
 import { ViewBoxBaseComponent } from '../DocComponent';
 import { EditableView } from '../EditableView';
-import { LightboxView } from '../LightboxView';
 import { StyleProp } from '../StyleProp';
 import { ComparisonBox } from './ComparisonBox';
 import { DocumentView } from './DocumentView';
@@ -52,7 +51,7 @@ export class LinkBox extends ViewBoxBaseComponent<FieldViewProps>() {
     componentDidMount() {
         this._props.setContentViewBox?.(this);
         this._disposers.deleting = reaction(
-            () => !this.anchor1 && !this.anchor2 && this.DocumentView?.() && (!LightboxView.LightboxDoc || LightboxView.Contains(this.DocumentView!())),
+            () => !this.anchor1 && !this.anchor2 && this.DocumentView?.() && (!DocumentView.LightboxDoc() || DocumentView.LightboxContains(this.DocumentView!())),
             empty => {
                 if (empty) {
                     this._hackToSeeIfDeleted = setTimeout(() => {
@@ -110,7 +109,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(DocumentView.UniquifyId(LightboxView.Contains(this.DocumentView?.()), doc[Id]));
+                const ele = document.getElementById(DocumentView.UniquifyId(DocumentView.LightboxContains(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(el => el?.getBoundingClientRect().width);
diff --git a/src/client/views/nodes/MapBox/MapBox.tsx b/src/client/views/nodes/MapBox/MapBox.tsx
index ac8010f11..d7687e03e 100644
--- a/src/client/views/nodes/MapBox/MapBox.tsx
+++ b/src/client/views/nodes/MapBox/MapBox.tsx
@@ -23,7 +23,7 @@ import { DocumentType } from '../../../documents/DocumentTypes';
 import { Docs } from '../../../documents/Documents';
 import { DragManager } from '../../../util/DragManager';
 import { UndoManager, undoable } from '../../../util/UndoManager';
-import { ViewBoxAnnotatableComponent, ViewBoxInterface } from '../../DocComponent';
+import { ViewBoxAnnotatableComponent } from '../../DocComponent';
 import { PinDocView, PinProps } from '../../PinFuncs';
 import { SidebarAnnos } from '../../SidebarAnnos';
 import { MarqueeOptionsMenu } from '../../collections/collectionFreeForm';
@@ -63,7 +63,7 @@ type PopupInfo = {
 };
 
 @observer
-export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implements ViewBoxInterface {
+export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
     public static LayoutString(fieldKey: string) {
         return FieldView.LayoutString(MapBox, fieldKey);
     }
diff --git a/src/client/views/nodes/RecordingBox/RecordingBox.tsx b/src/client/views/nodes/RecordingBox/RecordingBox.tsx
index e46e40bfe..07381c7d0 100644
--- a/src/client/views/nodes/RecordingBox/RecordingBox.tsx
+++ b/src/client/views/nodes/RecordingBox/RecordingBox.tsx
@@ -50,9 +50,9 @@ export class RecordingBox extends ViewBoxBaseComponent<FieldViewProps>() {
         this.result = info;
         this.dataDoc.type = DocumentType.VID;
 
-        this.dataDoc.layout = VideoBox.LayoutString(this.fieldKey);
+        this.dataDoc[this.fieldKey + '_recorded'] = this.dataDoc.layout; // save the recording layout to allow re-recording later
+        this.dataDoc.layout = VideoBox.LayoutString(this.fieldKey); // then convert the recording box to a video
         this.dataDoc[this._props.fieldKey] = new VideoField(this.result.accessPaths.client);
-        this.dataDoc[this.fieldKey + '_recorded'] = true;
         // stringify the presentation and store it
         if (presentation?.movements) {
             const presCopy = { ...presentation };
@@ -143,18 +143,13 @@ export class RecordingBox extends ViewBoxBaseComponent<FieldViewProps>() {
 
     public static resumeWorkspaceReplaying(doc: Doc) {
         const docView = DocumentView.getDocumentView(doc);
-        if (docView?.ComponentView instanceof VideoBox) {
-            docView.ComponentView.Play();
-        }
+        docView?.ComponentView?.Play?.();
         Doc.UserDoc().workspaceReplayingState = mediaState.Playing;
     }
 
     public static pauseWorkspaceReplaying(doc: Doc) {
         const docView = DocumentView.getDocumentView(doc);
-        const videoBox = docView?.ComponentView as VideoBox;
-        if (videoBox) {
-            videoBox.Pause();
-        }
+        docView?.ComponentView?.Pause?.();
         Doc.UserDoc().workspaceReplayingState = mediaState.Paused;
     }
 
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index 3b1815f8a..fe7600fa3 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -17,7 +17,6 @@ import { Docs } from '../../documents/Documents';
 import { DocumentType } from '../../documents/DocumentTypes';
 import { DocUtils, FollowLinkScript } from '../../documents/DocUtils';
 import { dropActionType } from '../../util/DropActionTypes';
-import { ReplayMovements } from '../../util/ReplayMovements';
 import { undoBatch } from '../../util/UndoManager';
 import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView';
 import { CollectionStackedTimeline, TrimScope } from '../collections/CollectionStackedTimeline';
@@ -32,7 +31,6 @@ import { StyleProp } from '../StyleProp';
 import { DocumentView } from './DocumentView';
 import { FieldView, FieldViewProps } from './FieldView';
 import { FocusViewOptions } from './FocusViewOptions';
-import { RecordingBox } from './RecordingBox';
 import './VideoBox.scss';
 
 /**
@@ -101,18 +99,13 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
         return field?.url.href ?? vfield?.url.href ?? '';
     }
 
-    // returns the presentation data if it exists, null otherwise
-    @computed get presentation() {
-        const data = this.dataDoc[this.fieldKey + '_presentation'];
-        return data ? JSON.parse(StrCast(data)) : null;
-    }
-
     @computed private get timeline() {
         return this._stackedTimeline;
     }
     private get transition() {
         return this._clicking ? 'left 0.5s, width 0.5s, height 0.5s' : '';
     } // css transition for hiding/showing timeline
+
     public get player(): HTMLVideoElement | null {
         return this._videoRef;
     }
@@ -122,10 +115,6 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
         this._props.setContentViewBox?.(this); // this tells the DocumentView that this VideoBox is the "content" of the document.  this allows the DocumentView to indirectly call getAnchor() on the VideoBox when making a link.
         this.player && this.setPlayheadTime(this.timeline?.clipStart || 0);
         document.addEventListener('keydown', this.keyEvents, true);
-
-        if (this.presentation) {
-            ReplayMovements.Instance.setVideoBox(this);
-        }
     }
 
     componentWillUnmount() {
@@ -134,12 +123,14 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
         this.Pause();
         Object.keys(this._disposers).forEach(d => this._disposers[d]?.());
         document.removeEventListener('keydown', this.keyEvents, true);
-
-        if (this.presentation) {
-            ReplayMovements.Instance.removeVideoBox();
-        }
     }
 
+    override PlayerTime = () => this.player?.currentTime;
+    override Pause = (update: boolean = true) => {
+        this.pause(update);
+        !this._keepCurrentlyPlaying && this.removeCurrentlyPlaying();
+    };
+
     // handles key events, when timeline scrubs fade controls
     @action
     keyEvents = (e: KeyboardEvent) => {
@@ -230,10 +221,6 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
         }
         this._playRegionTimer = undefined;
     };
-    @action Pause = (update: boolean = true) => {
-        this.pause(update);
-        !this._keepCurrentlyPlaying && this.removeCurrentlyPlaying();
-    };
 
     // toggles video full screen
     @action public FullScreen = () => {
@@ -518,11 +505,11 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                 icon: 'expand-arrows-alt',
             });
             // if the videobox was turned from a recording box
-            if (this.dataDoc[this.fieldKey + '_recorded'] === true) {
+            if (this.dataDoc[this.fieldKey + '_recorded']) {
                 subitems.push({
                     description: 'Recreate recording',
                     event: () => {
-                        this.dataDoc.layout = RecordingBox.LayoutString(this.fieldKey);
+                        this.dataDoc.layout = StrCast(this.dataDoc[this.fieldKey + '_recorded']); // restore the saed recording layout
                         // delete assoicated video data
                         this.dataDoc[this._props.fieldKey] = '';
                         this.dataDoc[this.fieldKey + '_duration'] = '';
diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx
index b6ef36974..8835ea5e7 100644
--- a/src/client/views/nodes/WebBox.tsx
+++ b/src/client/views/nodes/WebBox.tsx
@@ -28,9 +28,8 @@ import { MarqueeOptionsMenu } from '../collections/collectionFreeForm';
 import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView';
 import { ContextMenu } from '../ContextMenu';
 import { ContextMenuProps } from '../ContextMenuItem';
-import { ViewBoxAnnotatableComponent, ViewBoxInterface } from '../DocComponent';
+import { ViewBoxAnnotatableComponent } from '../DocComponent';
 import { Colors } from '../global/globalEnums';
-import { LightboxView } from '../LightboxView';
 import { MarqueeAnnotator } from '../MarqueeAnnotator';
 import { AnchorMenu } from '../pdf/AnchorMenu';
 import { Annotation } from '../pdf/Annotation';
@@ -38,6 +37,7 @@ import { GPTPopup } from '../pdf/GPTPopup/GPTPopup';
 import { PinDocView, PinProps } from '../PinFuncs';
 import { SidebarAnnos } from '../SidebarAnnos';
 import { StyleProp } from '../StyleProp';
+import { ViewBoxInterface } from '../ViewBoxInterface';
 import { DocumentView } from './DocumentView';
 import { FieldView, FieldViewProps } from './FieldView';
 import { FocusViewOptions } from './FocusViewOptions';
@@ -623,7 +623,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
             clearStyleSheetRules(WebBox.webStyleSheet);
             this._scrollTimer = undefined;
             const newScrollTop = scrollTop > iframeHeight ? iframeHeight : scrollTop;
-            if (!LinkInfo.Instance?.LinkInfo && this._outerRef.current && newScrollTop !== this.layoutDoc.thumbScrollTop && (!LightboxView.LightboxDoc || LightboxView.Contains(this.DocumentView?.()))) {
+            if (!LinkInfo.Instance?.LinkInfo && this._outerRef.current && newScrollTop !== this.layoutDoc.thumbScrollTop && (!DocumentView.LightboxDoc() || DocumentView.LightboxContains(this.DocumentView?.()))) {
                 this.layoutDoc.thumb = undefined;
                 this.layoutDoc.thumbScrollTop = undefined;
                 this.layoutDoc.thumbNativeWidth = undefined;
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 321fdbb91..e354aedb7 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -1,3 +1,4 @@
+/* eslint-disable no-use-before-define */
 /* eslint-disable jsx-a11y/no-static-element-interactions */
 import { IconProp } from '@fortawesome/fontawesome-svg-core';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
@@ -10,7 +11,7 @@ import { inputRules } from 'prosemirror-inputrules';
 import { keymap } from 'prosemirror-keymap';
 import { Fragment, Mark, Node, Slice } from 'prosemirror-model';
 import { EditorState, NodeSelection, Plugin, Selection, TextSelection, Transaction } from 'prosemirror-state';
-import { EditorView } from 'prosemirror-view';
+import { EditorView, NodeViewConstructor } from 'prosemirror-view';
 import * as React from 'react';
 import { BsMarkdownFill } from 'react-icons/bs';
 import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, ClientUtils, DivWidth, returnFalse, returnZero, setupMoveUpEvents, smoothScroll, StopEvent } from '../../../../ClientUtils';
@@ -45,7 +46,6 @@ import { ContextMenu } from '../../ContextMenu';
 import { ContextMenuProps } from '../../ContextMenuItem';
 import { ViewBoxAnnotatableComponent } from '../../DocComponent';
 import { Colors } from '../../global/globalEnums';
-import { LightboxView } from '../../LightboxView';
 import { AnchorMenu } from '../../pdf/AnchorMenu';
 import { GPTPopup } from '../../pdf/GPTPopup/GPTPopup';
 import { PinDocView, PinProps } from '../../PinFuncs';
@@ -58,11 +58,6 @@ import { FieldView, FieldViewProps } from '../FieldView';
 import { FocusViewOptions } from '../FocusViewOptions';
 import { LinkInfo } from '../LinkDocPreview';
 import { OpenWhere } from '../OpenWhere';
-import { DashDocCommentView } from './DashDocCommentView';
-import { DashDocView } from './DashDocView';
-import { DashFieldView } from './DashFieldView';
-import { EquationView } from './EquationView';
-import { FootnoteView } from './FootnoteView';
 import './FormattedTextBox.scss';
 import { findLinkMark, FormattedTextBoxComment } from './FormattedTextBoxComment';
 import { buildKeymap, updateBullets } from './ProsemirrorExampleTransfer';
@@ -70,7 +65,6 @@ import { removeMarkWithAttrs } from './prosemirrorPatches';
 import { RichTextMenu, RichTextMenuPlugin } from './RichTextMenu';
 import { RichTextRules } from './RichTextRules';
 import { schema } from './schema_rts';
-import { SummaryView } from './SummaryView';
 // import * as applyDevTools from 'prosemirror-dev-tools';
 
 export interface FormattedTextBoxProps extends FieldViewProps {
@@ -82,10 +76,15 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
     public static LayoutString(fieldStr: string) {
         return FieldView.LayoutString(FormattedTextBox, fieldStr);
     }
-    public static blankState = () => EditorState.create(FormattedTextBox.Instance.config);
-    // eslint-disable-next-line no-use-before-define
-    public static Instance: FormattedTextBox;
-    public static LiveTextUndo: UndoManager.Batch | undefined;
+    private static nodeViews: (self: FormattedTextBox) => { [key: string]: NodeViewConstructor };
+    /**
+     * Initialize the class with all the plugin node view components
+     * @param nodeViews prosemirror plugins that render a custom UI for specific node types
+     */
+    public static Init(nodeViews: (self: FormattedTextBox) => { [key: string]: NodeViewConstructor }) {
+        FormattedTextBox.nodeViews = nodeViews;
+    }
+    public static LiveTextUndo: UndoManager.Batch | undefined; // undo batch when typing a new text note into a collection
     static _globalHighlightsCache: string = '';
     static _globalHighlights = new ObservableSet<string>(['Audio Tags', 'Text from Others', 'Todo Items', 'Important Items', 'Disagree Items', 'Ignore Items']);
     static _highlightStyleSheet: any = addStyleSheet();
@@ -189,7 +188,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
     constructor(props: FormattedTextBoxProps) {
         super(props);
         makeObservable(this);
-        FormattedTextBox.Instance = this;
         this._recordingStart = Date.now();
     }
 
@@ -1435,14 +1433,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
                     return true;
                 },
                 dispatchTransaction: this.dispatchTransaction,
-                nodeViews: {
-                    dashComment(node: any, view: any, getPos: any) { return new DashDocCommentView(node, view, getPos); }, // prettier-ignore
-                    dashDoc(node: any, view: any, getPos: any)     { return new DashDocView(node, view, getPos, self); }, // prettier-ignore
-                    dashField(node: any, view: any, getPos: any)   { return new DashFieldView(node, view, getPos, self); }, // prettier-ignore
-                    equation(node: any, view: any, getPos: any)    { return new EquationView(node, view, getPos, self); }, // prettier-ignore
-                    summary(node: any, view: any, getPos: any)     { return new SummaryView(node, view, getPos); }, // prettier-ignore
-                    footnote(node: any, view: any, getPos: any)    { return new FootnoteView(node, view, getPos); }, // prettier-ignore
-                },
+                nodeViews: FormattedTextBox.nodeViews(this),
                 clipboardTextSerializer: this.clipboardTextSerializer,
                 handlePaste: this.handlePaste,
             });
@@ -1463,7 +1454,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
             (this._editorView as any).TextView = this;
         }
 
-        const selectOnLoad = Doc.AreProtosEqual(this._props.TemplateDataDocument ?? this.Document, Doc.SelectOnLoad) && (!LightboxView.LightboxDoc || LightboxView.Contains(this.DocumentView?.()));
+        const selectOnLoad = Doc.AreProtosEqual(this._props.TemplateDataDocument ?? this.Document, Doc.SelectOnLoad) && (!DocumentView.LightboxDoc() || DocumentView.LightboxContains(this.DocumentView?.()));
         const selLoadChar = FormattedTextBox.SelectOnLoadChar;
         if (selectOnLoad) {
             Doc.SetSelectOnLoad(undefined);
diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx
index 6b4f5e073..75492d2f9 100644
--- a/src/client/views/nodes/trails/PresBox.tsx
+++ b/src/client/views/nodes/trails/PresBox.tsx
@@ -24,14 +24,12 @@ import { ScriptingGlobals } from '../../../util/ScriptingGlobals';
 import { SerializationHelper } from '../../../util/SerializationHelper';
 import { SnappingManager } from '../../../util/SnappingManager';
 import { undoBatch, UndoManager } from '../../../util/UndoManager';
-import { CollectionDockingView } from '../../collections/CollectionDockingView';
 import { CollectionFreeFormView } from '../../collections/collectionFreeForm';
 import { CollectionFreeFormPannableContents } from '../../collections/collectionFreeForm/CollectionFreeFormPannableContents';
 import { CollectionView } from '../../collections/CollectionView';
 import { TreeView } from '../../collections/TreeView';
 import { ViewBoxBaseComponent } from '../../DocComponent';
 import { Colors } from '../../global/globalEnums';
-import { LightboxView } from '../../LightboxView';
 import { pinDataTypes as dataTypes } from '../../PinFuncs';
 import { DocumentView } from '../DocumentView';
 import { FieldView, FieldViewProps } from '../FieldView';
@@ -46,6 +44,10 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
     public static LayoutString(fieldKey: string) {
         return FieldView.LayoutString(PresBox, fieldKey);
     }
+    private static _getTabDocs: () => Doc[];
+    public static Init(tabDocs: () => Doc[]) {
+        PresBox._getTabDocs = tabDocs;
+    }
     static navigateToDocScript: ScriptField;
 
     constructor(props: FieldViewProps) {
@@ -679,16 +681,16 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         if (activeItem.presentation_openInLightbox) {
             const context = DocCast(targetDoc.annotationOn) ?? targetDoc;
             if (!DocumentView.getLightboxDocumentView(context)) {
-                LightboxView.Instance.SetLightboxDoc(context);
+                DocumentView.SetLightboxDoc(context);
             }
         }
         if (targetDoc) {
             if (activeItem.presentation_targetDoc instanceof Doc) activeItem.presentation_targetDoc[Animation] = undefined;
 
-            DocumentView.addViewRenderedCb(LightboxView.LightboxDoc, () => {
+            DocumentView.addViewRenderedCb(DocumentView.LightboxDoc(), () => {
                 // if target or the doc it annotates is not in the lightbox, then close the lightbox
                 if (!DocumentView.getLightboxDocumentView(DocCast(targetDoc.annotationOn) ?? targetDoc)) {
-                    LightboxView.Instance.SetLightboxDoc(undefined);
+                    DocumentView.SetLightboxDoc(undefined);
                 }
                 DocumentView.showDocument(targetDoc, options, finished);
             });
@@ -789,7 +791,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                         default:
                     }
                 });
-            LightboxView.Instance.SetLightboxDoc(undefined);
+            DocumentView.SetLightboxDoc(undefined);
             Doc.RemFromMyOverlay(this.Document);
             return PresStatus.Edit;
         };
@@ -892,7 +894,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
     exitMinimize = () => {
         if (Doc.IsInMyOverlay(this.layoutDoc)) {
             Doc.RemFromMyOverlay(this.Document);
-            CollectionDockingView.AddSplit(this.Document, OpenWhereMod.right);
+            DocumentView.addSplit(this.Document, OpenWhereMod.right);
         }
         return PresStatus.Edit;
     };
@@ -2202,9 +2204,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         if (freeform && layout) doc = this.createTemplate(layout, title);
         if (!freeform && !layout) doc = Docs.Create.TextDocument('', { _nativeWidth: 400, _width: 225, title: title });
         if (doc) {
-            const tabMap = CollectionDockingView.Instance?.tabMap;
-            const docTab = tabMap && Array.from(tabMap).find(tab => tab.DashDoc.type === DocumentType.COL)?.DashDoc;
-            const presCollection = DocumentView.getContextPath(this.activeItem).reverse().lastElement().presentation_targetDoc ?? docTab;
+            const docTab = PresBox._getTabDocs().find(tdoc => tdoc.type === DocumentType.COL);
+            const presCollection = DocCast(DocumentView.getContextPath(this.activeItem).reverse().lastElement().presentation_targetDoc, docTab);
             const data = Cast(presCollection?.data, listSpec(Doc));
             const configData = Cast(this.Document.data, listSpec(Doc));
             if (data && configData) {
diff --git a/src/client/views/search/SearchBox.tsx b/src/client/views/search/SearchBox.tsx
index 56552c952..ae0838dd5 100644
--- a/src/client/views/search/SearchBox.tsx
+++ b/src/client/views/search/SearchBox.tsx
@@ -18,8 +18,8 @@ import { undoBatch } from '../../util/UndoManager';
 import { ViewBoxBaseComponent } from '../DocComponent';
 import { ObservableReactComponent } from '../ObservableReactComponent';
 import { CollectionDockingView } from '../collections/CollectionDockingView';
-import { IRecommendation, Recommendation } from '../newlightbox/components';
-import { fetchRecommendations } from '../newlightbox/utils';
+// import { IRecommendation, Recommendation } from '../newlightbox/components';
+// import { fetchRecommendations } from '../newlightbox/utils';
 import { DocumentView } from '../nodes/DocumentView';
 import { FieldView, FieldViewProps } from '../nodes/FieldView';
 import './SearchBox.scss';
@@ -142,7 +142,7 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() {
     @observable _searchString = '';
     @observable _docTypeString = 'all';
     @observable _results: Map<Doc, string[]> = new Map<Doc, string[]>();
-    @observable _recommendations: IRecommendation[] = [];
+    // @observable _recommendations: IRecommendation[] = [];
     @observable _pageRanks: Map<Doc, number> = new Map<Doc, number>();
     @observable _linkedDocsOut: Map<Doc, Set<Doc>> = new Map<Doc, Set<Doc>>();
     @observable _linkedDocsIn: Map<Doc, Set<Doc>> = new Map<Doc, Set<Doc>>();
@@ -375,29 +375,29 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() {
 
         if (query) {
             this.searchCollection(query);
-            const response = await fetchRecommendations('', query, [], true);
-            const recs = response.recommendations as any[];
-            const recommendations: IRecommendation[] = [];
-            recs.forEach(rec => {
-                const { title, url, type, text, transcript, previewUrl, embedding, distance, source, related_concepts: relatedConcepts, doc_id: docId } = rec;
-                recommendations.push({
-                    title,
-                    data: url,
-                    type,
-                    text,
-                    transcript,
-                    previewUrl,
-                    embedding,
-                    distance: Math.round(distance * 100) / 100,
-                    source: source,
-                    related_concepts: relatedConcepts,
-                    docId,
-                });
-            });
-            const setRecommendations = action(() => {
-                this._recommendations = recommendations;
-            });
-            setRecommendations();
+            // const response = await fetchRecommendations('', query, [], true);
+            // const recs = response.recommendations as any[];
+            // const recommendations: IRecommendation[] = [];
+            // recs.forEach(rec => {
+            //     const { title, url, type, text, transcript, previewUrl, embedding, distance, source, related_concepts: relatedConcepts, doc_id: docId } = rec;
+            //     recommendations.push({
+            //         title,
+            //         data: url,
+            //         type,
+            //         text,
+            //         transcript,
+            //         previewUrl,
+            //         embedding,
+            //         distance: Math.round(distance * 100) / 100,
+            //         source: source,
+            //         related_concepts: relatedConcepts,
+            //         docId,
+            //     });
+            // });
+            // const setRecommendations = action(() => {
+            //     this._recommendations = recommendations;
+            // });
+            // setRecommendations();
         }
     };
 
@@ -470,7 +470,7 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() {
         });
 
         // eslint-disable-next-line react/jsx-props-no-spreading
-        const recommendationsJSX: JSX.Element[] = this._recommendations.map(props => <Recommendation {...props} />);
+        const recommendationsJSX: JSX.Element[] = []; // this._recommendations.map(props => <Recommendation {...props} />);
 
         return (
             <div className="searchBox-container" style={{ pointerEvents: 'all', color: SnappingManager.userColor, background: SnappingManager.userBackgroundColor }}>
diff --git a/src/client/views/topbar/TopBar.tsx b/src/client/views/topbar/TopBar.tsx
index 9f97efcce..e558e14e3 100644
--- a/src/client/views/topbar/TopBar.tsx
+++ b/src/client/views/topbar/TopBar.tsx
@@ -23,9 +23,9 @@ import { CollectionDockingView } from '../collections/CollectionDockingView';
 import { CollectionLinearView } from '../collections/collectionLinear';
 import { DashboardView } from '../DashboardView';
 import { Colors } from '../global/globalEnums';
-import { DocumentView, DocumentViewInternal, returnEmptyDocViewList } from '../nodes/DocumentView';
+import { DocumentView, DocumentViewInternal } from '../nodes/DocumentView';
 import { ObservableReactComponent } from '../ObservableReactComponent';
-import { DefaultStyleProvider } from '../StyleProvider';
+import { DefaultStyleProvider, returnEmptyDocViewList } from '../StyleProvider';
 import './TopBar.scss';
 
 /**
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index 725221a66..fe044c035 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -102,7 +102,7 @@ export namespace Field {
     export function Copy(field: any) {
         return field instanceof ObjectField ? ObjectField.MakeCopy(field) : field;
     }
-    UndoManager.SetFieldPrinter(toJavascriptString);
+    UndoManager.SetFieldPrinter(toString);
 }
 export type FieldType = number | string | boolean | ObjectField | RefField;
 export type Opt<T> = T | undefined;
diff --git a/src/fields/InkField.ts b/src/fields/InkField.ts
index c4f5f7a24..46704eb2b 100644
--- a/src/fields/InkField.ts
+++ b/src/fields/InkField.ts
@@ -89,6 +89,17 @@ export class InkField extends ObjectField {
     [ToString]() {
         return 'InkField';
     }
+
+    public static getBounds(stroke: InkData, pad?: boolean) {
+        const padding = pad ? [-20000, 20000] : [];
+        const xs = [...padding, ...stroke.map(p => p.X)];
+        const ys = [...padding, ...stroke.map(p => p.Y)];
+        const right = Math.max(...xs);
+        const left = Math.min(...xs);
+        const bottom = Math.max(...ys);
+        const top = Math.min(...ys);
+        return { right, left, bottom, top, width: right - left, height: bottom - top };
+    }
 }
 
 ScriptingGlobals.add('InkField', InkField);
diff --git a/src/fields/Proxy.ts b/src/fields/Proxy.ts
index 4f8058ce4..83b5672b3 100644
--- a/src/fields/Proxy.ts
+++ b/src/fields/Proxy.ts
@@ -45,7 +45,7 @@ export class ProxyField<T extends RefField> extends ObjectField {
         return Field.toScriptString(this[ToValue]()?.value); // not sure this is quite right since it doesn't recreate a proxy field, but better than 'invalid' ?
     }
     [ToString]() {
-        return 'ProxyField';
+        return Field.toString(this[ToValue]()?.value);
     }
 
     @serializable(primitive())
diff --git a/src/fields/RichTextUtils.ts b/src/fields/RichTextUtils.ts
index 5eb60a2f8..3763dcd2c 100644
--- a/src/fields/RichTextUtils.ts
+++ b/src/fields/RichTextUtils.ts
@@ -164,7 +164,7 @@ export namespace RichTextUtils {
             const inlineObjectMap = await parseInlineObjects(document);
             const title = document.title!;
             const { text, paragraphs } = GoogleApiClientUtils.Docs.Utils.extractText(document);
-            let state = FormattedTextBox.blankState();
+            let state = EditorState.create(new FormattedTextBox({} as any).config);
             const structured = parseLists(paragraphs);
 
             let position = 3;
diff --git a/src/fields/util.ts b/src/fields/util.ts
index 9361430cb..a6499c3e3 100644
--- a/src/fields/util.ts
+++ b/src/fields/util.ts
@@ -443,7 +443,7 @@ export function containedFieldChangedHandler(container: List<FieldType> | Doc, p
                             });
                             lastValue = ObjectField.MakeCopy((container as any)[prop as any]);
                         },
-                        prop: 'remove ' + (diff.items?.length ?? 0) + ' items from list',
+                        prop: 'remove ' + (diff.items?.length ?? 0) + ' items from list(' + ((container as any)?.title ?? '') + ':' + prop + ')',
                     },
                     diff?.items
                 );
-- 
cgit v1.2.3-70-g09d2


From 6a332d5c1e248d976c44eecc67161b4d1b401a0e Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Tue, 14 May 2024 23:16:04 -0400
Subject: post merge fixes for sarah-ai-visualizatoin

---
 src/client/views/DocComponent.tsx                         |  2 +-
 src/client/views/nodes/DataVizBox/utils/D3Utils.ts        |  5 ++++-
 src/client/views/nodes/formattedText/FormattedTextBox.tsx | 10 ++++++++--
 src/client/views/nodes/generativeFill/GenerativeFill.tsx  |  6 +++---
 4 files changed, 16 insertions(+), 7 deletions(-)

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

diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx
index 94e84e647..e5752dcd2 100644
--- a/src/client/views/DocComponent.tsx
+++ b/src/client/views/DocComponent.tsx
@@ -233,7 +233,7 @@ export function ViewBoxAnnotatableComponent<P extends FieldViewProps>() {
 
                     const annoDocs = Doc.Get(targetDataDoc, annotationKey ?? this.annotationKey, true) as List<Doc>; // get the dataDoc directly ... when using templates there may be some default items already there, but we can't change them, so we copy them below (should really be some kind of inheritance since the template contents could change)
                     if (annoDocs instanceof List) annoDocs.push(...added.filter(add => !annoDocs.includes(add)));
-                    else targetDataDoc[annotationKey ?? this.annotationKey] = new List<Doc>([...added, ...(annoDocs === undefined ? DocListCast(targetDataDoc[annotationKey ?? this.annotationKey]) : [])]);
+                    else targetDataDoc[annotationKey || this.annotationKey] = new List<Doc>([...added, ...(annoDocs === undefined ? DocListCast(targetDataDoc[annotationKey ?? this.annotationKey]) : [])]);
                     targetDataDoc[(annotationKey ?? this.annotationKey) + '_modificationDate'] = new DateField();
                 }
             }
diff --git a/src/client/views/nodes/DataVizBox/utils/D3Utils.ts b/src/client/views/nodes/DataVizBox/utils/D3Utils.ts
index be05c3529..ffc859c92 100644
--- a/src/client/views/nodes/DataVizBox/utils/D3Utils.ts
+++ b/src/client/views/nodes/DataVizBox/utils/D3Utils.ts
@@ -1,6 +1,9 @@
 import * as d3 from 'd3';
-import { DataPoint } from '../components/LineChart';
 
+export interface DataPoint {
+    x: number;
+    y: number;
+}
 // TODO: nda - implement function that can handle range for strings
 
 export const minMaxRange = (dataPts: DataPoint[][]) => {
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index e62a639c8..93cb67491 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -955,7 +955,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
             icon: 'star',
         });
         optionItems.push({ description: `Generate Dall-E Image`, event: () => this.generateImage(), icon: 'star' });
-        optionItems.push({ description: `Ask GPT-3`, event: () => this.askGPT(), icon: 'lightbulb' });
+        optionItems.push({ description: `Ask GPT-3`, event: this.askGPT, icon: 'lightbulb' });
         this._props.renderDepth &&
             optionItems.push({
                 description: !this.Document._createDocOnCR ? 'Create New Doc on Carriage Return' : 'Allow Carriage Returns',
@@ -989,6 +989,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
 
     askGPT = action(async () => {
         try {
+            GPTPopup.Instance.setSidebarId(this.SidebarKey);
+            GPTPopup.Instance.addDoc = this.sidebarAddDocument;
             const res = await gptAPICall((this.dataDoc.text as RichTextField)?.Text, GPTCallType.COMPLETION);
             if (!res) {
                 this.animateRes(0, 'Something went wrong.');
@@ -1573,7 +1575,11 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
             e.preventDefault();
         }
     };
-    onSelectEnd = (): void => document.removeEventListener('pointerup', this.onSelectEnd);
+    onSelectEnd = () => {
+        GPTPopup.Instance.setSidebarId(this.SidebarKey);
+        GPTPopup.Instance.addDoc = this.sidebarAddDocument;
+        document.removeEventListener('pointerup', this.onSelectEnd);
+    };
     onPointerUp = (e: React.PointerEvent): void => {
         const state = this.EditorView?.state;
         if (state && this.ProseRef?.children[0].className.includes('-focused') && this._props.isContentActive() && !e.button) {
diff --git a/src/client/views/nodes/generativeFill/GenerativeFill.tsx b/src/client/views/nodes/generativeFill/GenerativeFill.tsx
index 25289967e..6d8ba9222 100644
--- a/src/client/views/nodes/generativeFill/GenerativeFill.tsx
+++ b/src/client/views/nodes/generativeFill/GenerativeFill.tsx
@@ -535,14 +535,14 @@ const GenerativeFill = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addD
                             key={i}
                             alt="image edits"
                             width={75}
-                            src={edit[0] as string}
+                            src={edit.url}
                             style={{ cursor: 'pointer' }}
                             onClick={async () => {
                                 const img = new Image();
-                                img.src = edit[0] as string;
+                                img.src = edit.url;
                                 ImageUtility.drawImgToCanvas(img, canvasRef, canvasDims.width, canvasDims.height);
                                 currImg.current = img;
-                                parentDoc.current = edit[1] as Doc;
+                                parentDoc.current = edit.saveRes ?? null;
                             }}
                         />
                     ))}
-- 
cgit v1.2.3-70-g09d2

