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

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

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

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';
-- 
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/PreviewCursor.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 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/PreviewCursor.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

