From b1376d401e709515cee078cc08b05fd3fb89caeb Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Wed, 24 Apr 2024 18:12:30 -0400
Subject: completing eslint pass

---
 src/client/util/GroupMemberView.tsx | 32 +++++++++++++++++++++-----------
 1 file changed, 21 insertions(+), 11 deletions(-)

(limited to 'src/client/util/GroupMemberView.tsx')

diff --git a/src/client/util/GroupMemberView.tsx b/src/client/util/GroupMemberView.tsx
index 894583711..c703c3f98 100644
--- a/src/client/util/GroupMemberView.tsx
+++ b/src/client/util/GroupMemberView.tsx
@@ -1,4 +1,7 @@
+/* eslint-disable jsx-a11y/no-static-element-interactions */
+/* eslint-disable jsx-a11y/click-events-have-key-events */
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { Button, IconButton, Size, Type } from 'browndash-components';
 import { action, observable } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
@@ -8,7 +11,6 @@ import { StrCast } from '../../fields/Types';
 import { MainViewModal } from '../views/MainViewModal';
 import { GroupManager, UserOptions } from './GroupManager';
 import './GroupMemberView.scss';
-import { Button, IconButton, Size, Type } from 'browndash-components';
 import { SettingsManager } from './SettingsManager';
 
 interface GroupMemberViewProps {
@@ -38,20 +40,23 @@ export class GroupMemberView extends React.Component<GroupMemberViewProps> {
                         className="group-title"
                         style={{ marginLeft: !hasEditAccess ? '-14%' : 0 }}
                         value={StrCast(this.group.title || this.group.groupName)}
-                        onChange={e => (this.group.title = e.currentTarget.value)}
-                        disabled={!hasEditAccess}></input>
-                    <div className={'memberView-closeButton'}>
-                        <Button icon={<FontAwesomeIcon icon={'times'} size={'lg'} />} onClick={action(this.props.onCloseButtonClick)} color={StrCast(Doc.UserDoc().userColor)} />
+                        onChange={e => {
+                            this.group.title = e.currentTarget.value;
+                        }}
+                        disabled={!hasEditAccess}
+                    />
+                    <div className="memberView-closeButton">
+                        <Button icon={<FontAwesomeIcon icon="times" size="lg" />} onClick={action(this.props.onCloseButtonClick)} color={StrCast(Doc.UserDoc().userColor)} />
                     </div>
                     {GroupManager.Instance.hasEditAccess(this.group) ? (
                         <div className="group-buttons">
                             <div style={{ border: StrCast(Doc.UserDoc().userColor) }}>
                                 <Select
                                     className="add-member-dropdown"
-                                    isSearchable={true}
+                                    isSearchable
                                     options={options}
                                     onChange={selectedOption => GroupManager.Instance.addMemberToGroup(this.group, (selectedOption as UserOptions).value)}
-                                    placeholder={'Add members'}
+                                    placeholder="Add members"
                                     value={null}
                                     styles={{
                                         control: () => ({
@@ -80,7 +85,12 @@ export class GroupMemberView extends React.Component<GroupMemberViewProps> {
                             </div>
                         </div>
                     ) : null}
-                    <div className="sort-emails" style={{ paddingTop: hasEditAccess ? 0 : 35 }} onClick={action(() => (this.memberSort = this.memberSort === 'ascending' ? 'descending' : this.memberSort === 'descending' ? 'none' : 'ascending'))}>
+                    <div
+                        className="sort-emails"
+                        style={{ paddingTop: hasEditAccess ? 0 : 35 }}
+                        onClick={action(() => {
+                            this.memberSort = this.memberSort === 'ascending' ? 'descending' : this.memberSort === 'descending' ? 'none' : 'ascending';
+                        })}>
                         Emails {this.memberSort === 'ascending' ? '↑' : this.memberSort === 'descending' ? '↓' : ''} {/* → */}
                     </div>
                 </div>
@@ -90,8 +100,8 @@ export class GroupMemberView extends React.Component<GroupMemberViewProps> {
                         <div className="editing-row" key={member}>
                             <div className="user-email">{member}</div>
                             {hasEditAccess ? (
-                                <div className={'remove-button'} onClick={() => GroupManager.Instance.removeMemberFromGroup(this.group, member)}>
-                                    <IconButton icon={<FontAwesomeIcon icon={'trash-alt'} />} size={Size.XSMALL} color={StrCast(Doc.UserDoc().userColor)} onClick={() => GroupManager.Instance.removeMemberFromGroup(this.group, member)} />
+                                <div className="remove-button" onClick={() => GroupManager.Instance.removeMemberFromGroup(this.group, member)}>
+                                    <IconButton icon={<FontAwesomeIcon icon="trash-alt" />} size={Size.XSMALL} color={StrCast(Doc.UserDoc().userColor)} onClick={() => GroupManager.Instance.removeMemberFromGroup(this.group, member)} />
                                 </div>
                             ) : null}
                         </div>
@@ -102,6 +112,6 @@ export class GroupMemberView extends React.Component<GroupMemberViewProps> {
     }
 
     render() {
-        return <MainViewModal isDisplayed={true} interactive={true} contents={this.editingInterface} dialogueBoxStyle={{ width: 400, height: 250 }} closeOnExternalClick={this.props.onCloseButtonClick} />;
+        return <MainViewModal isDisplayed interactive contents={this.editingInterface} dialogueBoxStyle={{ width: 400, height: 250 }} closeOnExternalClick={this.props.onCloseButtonClick} />;
     }
 }
-- 
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/util/GroupMemberView.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

