From 42afc0250de658fc3e924864bfae5afb4edec335 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Sun, 14 May 2023 12:03:40 -0400
Subject: major overhaul of field naming conventions.

---
 .../views/collections/CollectionDockingView.tsx    | 22 +++++++++++-----------
 1 file changed, 11 insertions(+), 11 deletions(-)

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

diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index bb1f788d4..3a4691484 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -127,12 +127,12 @@ export class CollectionDockingView extends CollectionSubView() {
         SelectionManager.DeselectAll();
         const instance = CollectionDockingView.Instance;
         if (instance) {
-            if (doc._viewType === CollectionViewType.Docking && doc.layoutKey === 'layout') {
+            if (doc._viewType === CollectionViewType.Docking && doc.layout_fieldKey === 'layout') {
                 return DashboardView.openDashboard(doc);
             }
             const newItemStackConfig = {
                 type: 'stack',
-                content: [CollectionDockingView.makeDocumentConfig(Doc.MakeAlias(doc))],
+                content: [CollectionDockingView.makeDocumentConfig(Doc.MakeEmbedding(doc))],
             };
             const docconfig = instance._goldenLayout.root.layoutManager.createContentItem(newItemStackConfig, instance._goldenLayout);
             instance._goldenLayout.root.contentItems[0].addChild(docconfig);
@@ -411,8 +411,8 @@ export class CollectionDockingView extends CollectionSubView() {
             const _height = Number(getComputedStyle(content).height.replace('px', ''));
             return CollectionFreeFormView.UpdateIcon(this.layoutDoc[Id] + '-icon' + new Date().getTime(), content, _width, _height, _width, _height, 0, 1, true, this.layoutDoc[Id] + '-icon', (iconFile, _nativeWidth, _nativeHeight) => {
                 const proto = this.dataDoc; // Cast(img.proto, Doc, null)!;
-                proto['thumb-nativeWidth'] = _width;
-                proto['thumb-nativeHeight'] = _height;
+                proto['thumb_nativeWidth'] = _width;
+                proto['thumb_nativeHeight'] = _height;
                 proto.thumb = new ImageField(iconFile);
             });
         }
@@ -434,11 +434,11 @@ export class CollectionDockingView extends CollectionSubView() {
             .map(f => f as Doc);
         const newtabs = origtabs.map(origtab => {
             const origtabdocs = DocListCast(origtab.data);
-            const newtab = origtabdocs.length ? Doc.MakeCopy(origtab, true, undefined, true) : Doc.MakeAlias(origtab);
-            const newtabdocs = origtabdocs.map(origtabdoc => Doc.MakeAlias(origtabdoc));
+            const newtab = origtabdocs.length ? Doc.MakeCopy(origtab, true, undefined, true) : Doc.MakeEmbedding(origtab);
+            const newtabdocs = origtabdocs.map(origtabdoc => Doc.MakeEmbedding(origtabdoc));
             if (newtabdocs.length) {
                 Doc.GetProto(newtab).data = new List<Doc>(newtabdocs);
-                newtabdocs.forEach(ntab => (ntab.context = newtab));
+                newtabdocs.forEach(ntab => (ntab.embedContainer = newtab));
             }
             json = json.replace(origtab[Id], newtab[Id]);
             return newtab;
@@ -498,8 +498,8 @@ export class CollectionDockingView extends CollectionSubView() {
                 const docToAdd = Docs.Create.FreeformDocument([], {
                     _width: this.props.PanelWidth(),
                     _height: this.props.PanelHeight(),
-                    _backgroundGridShow: true,
-                    _fitWidth: true,
+                    _freeform_backgroundGrid: true,
+                    _layout_fitWidth: true,
                     title: `Untitled Tab ${NumCast(dashboard['pane-count'])}`,
                 });
                 this.props.Document.isShared && inheritParentAcls(this.props.Document, docToAdd);
@@ -541,8 +541,8 @@ export class CollectionDockingView extends CollectionSubView() {
                         const docToAdd = Docs.Create.FreeformDocument([], {
                             _width: this.props.PanelWidth(),
                             _height: this.props.PanelHeight(),
-                            _fitWidth: true,
-                            _backgroundGridShow: true,
+                            _layout_fitWidth: true,
+                            _freeform_backgroundGrid: true,
                             title: `Untitled Tab ${NumCast(dashboard['pane-count'])}`,
                         });
                         this.props.Document.isShared && inheritParentAcls(this.props.Document, docToAdd);
-- 
cgit v1.2.3-70-g09d2


From df7257d1b39f51a7e00a495f0d4a2366f0e21f7d Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Sun, 14 May 2023 22:04:36 -0400
Subject: fixed webpage link following by adding a presData for the current URL
 to all embedded docs.  fixed getView() in showDocuments to not get called
 with the proper anchors. changed unrendered MARKERs to CONFIGs. changed
 anchors to Configs or Markers as appropriate.

---
 src/client/documents/DocumentTypes.ts              |   2 +-
 src/client/documents/Documents.ts                  | 101 +++++++++--------
 src/client/util/CurrentUserUtils.ts                |   8 +-
 src/client/util/DictationManager.ts                |   2 +-
 src/client/util/DocumentManager.ts                 |  11 +-
 src/client/util/LinkFollower.ts                    |  14 +--
 src/client/util/SelectionManager.ts                |  12 +--
 src/client/util/TrackMovements.ts                  |   2 +-
 src/client/views/DashboardView.tsx                 |   6 +-
 src/client/views/InkingStroke.tsx                  |   8 +-
 src/client/views/MainView.tsx                      |   9 +-
 src/client/views/MarqueeAnnotator.tsx              |   2 +-
 src/client/views/MetadataEntryMenu.tsx             | 119 ++++++++++++---------
 src/client/views/PropertiesButtons.tsx             |  12 +--
 src/client/views/SidebarAnnos.tsx                  |   4 +-
 src/client/views/StyleProvider.tsx                 |  18 ++--
 .../views/collections/CollectionCarousel3DView.tsx |   8 +-
 .../views/collections/CollectionCarouselView.tsx   |   8 +-
 .../views/collections/CollectionDockingView.tsx    |   4 +-
 src/client/views/collections/CollectionMenu.tsx    |  12 +--
 .../views/collections/CollectionStackingView.tsx   |   4 +-
 src/client/views/collections/CollectionSubView.tsx |   6 +-
 .../views/collections/CollectionTimeView.tsx       |   4 +-
 src/client/views/collections/CollectionView.tsx    |  10 +-
 src/client/views/collections/TabDocView.tsx        |   8 +-
 src/client/views/collections/TreeView.tsx          |  10 +-
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  19 ++--
 .../collections/collectionFreeForm/MarqueeView.tsx |   4 +-
 .../collectionLinear/CollectionLinearView.tsx      |   2 +-
 src/client/views/nodes/ComparisonBox.tsx           |   7 +-
 src/client/views/nodes/DataVizBox/DataVizBox.tsx   |   3 +-
 .../nodes/DataVizBox/components/LineChart.tsx      |   5 +-
 src/client/views/nodes/DocumentView.tsx            |  20 ++--
 src/client/views/nodes/FunctionPlotBox.tsx         |   5 +-
 src/client/views/nodes/ImageBox.tsx                |   3 +-
 src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx |   2 +-
 src/client/views/nodes/PDFBox.tsx                  |   3 +-
 src/client/views/nodes/VideoBox.tsx                |   1 +
 src/client/views/nodes/WebBox.tsx                  |  11 +-
 src/client/views/nodes/button/FontIconBox.tsx      |   4 +-
 .../views/nodes/formattedText/DashFieldView.tsx    |   2 +-
 .../views/nodes/formattedText/FormattedTextBox.tsx |  14 ++-
 src/client/views/nodes/trails/PresBox.tsx          |  60 +++++------
 src/client/views/nodes/trails/PresElementBox.tsx   |   2 +-
 src/client/views/pdf/PDFViewer.tsx                 |   2 +-
 src/client/views/search/SearchBox.tsx              |   4 +-
 src/fields/Doc.ts                                  |   2 +-
 src/fields/util.ts                                 |   2 +-
 src/mobile/MobileInterface.tsx                     |  18 ++--
 49 files changed, 328 insertions(+), 271 deletions(-)

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

diff --git a/src/client/documents/DocumentTypes.ts b/src/client/documents/DocumentTypes.ts
index 2da3a24fd..e0eb20faa 100644
--- a/src/client/documents/DocumentTypes.ts
+++ b/src/client/documents/DocumentTypes.ts
@@ -19,7 +19,7 @@ export enum DocumentType {
     LABEL = 'label', // simple text label
     BUTTON = 'button', // onClick button
     WEBCAM = 'webcam', // webcam
-    MARKER = 'marker', // generic marker document not intended to be viewed independently of its context (e.g., for text selections in PDF/Web/RTF)
+    CONFIG = 'config', // configuration document intended to specify a view layout configuration, but not be directly rendered (e.g., for saving the page# of a PDF, or view transform of a collection)
     DATE = 'date', // calendar view of a date
     SCRIPTING = 'script', // script editor
     EQUATION = 'equation', // equation editor
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 849a5d6ae..2151d0ec7 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -184,16 +184,17 @@ export class DocumentOptions {
     _stayInCollection?: boolean; // whether the document should remain in its collection when someone tries to drag and drop it elsewhere
     _raiseWhenDragged?: boolean; // whether a document is brought to front when dragged.
     _hideContextMenu?: boolean; // whether the context menu can be shown
-    _viewType?: string; // sub type of a collection
-    viewType?: string; // sub type of a collection
+    _type_collection?: string; // sub type of a collection
+    type_collection?: string; // sub type of a collection
     _gridGap?: number; // gap between items in masonry view
+    freeform?: string; // placeholder just so that the key value pane has a divider to render for freeform view data
     _freeform_scale?: number; // how much a freeform view has been scaled (zoomed)
     _overflow?: string; // set overflow behavior
     _xMargin?: number; // gap between left edge of document and start of masonry/stacking layouts
     _yMargin?: number; // gap between top edge of dcoument and start of masonry/stacking layouts
     _xPadding?: number;
     _yPadding?: number;
-    _itemIndex?: number; // which item index the carousel viewer is showing
+    _carousel_index?: number; // which item index the carousel viewer is showing
     _layout_showSidebar?: boolean; //whether an annotationsidebar should be displayed for text docuemnts
     _singleLine?: boolean; // whether label box is restricted to one line of text
     _createDocOnCR?: boolean; // whether carriage returns and tabs create new text documents
@@ -225,7 +226,7 @@ export class DocumentOptions {
     _layout_fieldKey?: string;
     fieldValues?: List<any>; // possible field values used by fieldInfos
     fieldType?: string; // type of afield used by fieldInfos
-    unrendered?: boolean; // denotes an annotation that is not rendered with a DocumentView (e.g, rtf/pdf text selections and links to scroll locations in web/pdf)
+    layout_unrendered?: boolean; // denotes an annotation that is not rendered with a DocumentView (e.g, rtf/pdf text selections and links to scroll locations in web/pdf)
     'acl-Public'?: string; // public permissions
     '_acl-Public'?: string; // public permissions
     version?: string; // version identifier for a document
@@ -362,7 +363,7 @@ export class DocumentOptions {
     stroke_width?: number;
     freezeChildren?: string; // whether children are now allowed to be added and or removed from a collection
     treeViewHideTitle?: boolean; // whether to hide the top document title of a tree view
-    treeViewHideUnrendered?: boolean; // tells tree view not to display documents that have an 'unrendered' tag unless they also have a treeViewFieldKey tag (presBox)
+    treeViewHideUnrendered?: boolean; // tells tree view not to display documents that have an 'layout_unrendered' tag unless they also have a treeViewFieldKey tag (presBox)
     treeViewHideHeaderIfTemplate?: boolean; // whether to hide the header for a document in a tree view only if a childLayoutTemplate is provided (presBox)
     treeViewHideHeader?: boolean; // whether to hide the header for a document in a tree view
     treeViewHideHeaderFields?: boolean; // whether to hide the drop down options for tree view items.
@@ -625,10 +626,10 @@ export namespace Docs {
                 },
             ],
             [
-                DocumentType.MARKER,
+                DocumentType.CONFIG,
                 {
                     layout: { view: CollectionView, dataField: defaultDataKey },
-                    options: { layout_hideLinkButton: true, pointerEvents: 'none' },
+                    options: { layout_hideLinkButton: true, pointerEvents: 'none', layout_unrendered: true },
                 },
             ],
             [
@@ -860,7 +861,7 @@ export namespace Docs {
                 viewDoc = Doc.assign(Doc.MakeDelegate(dataDoc, delegId), viewFirstProps, true, true);
             }
             Doc.assign(viewDoc, viewProps, true, true);
-            if (![DocumentType.LINK, DocumentType.MARKER, DocumentType.LABEL].includes(viewDoc.type as any)) {
+            if (![DocumentType.LINK, DocumentType.CONFIG, DocumentType.LABEL].includes(viewDoc.type as any)) {
                 DocUtils.MakeLinkToActiveAudio(() => viewDoc);
             }
 
@@ -1028,81 +1029,92 @@ export namespace Docs {
             return InstanceFromProto(Prototypes.get(DocumentType.MAP), new List(documents), options);
         }
 
-        export function MapMarkerDocument(lat: number, lng: number, infoWindowOpen: boolean, documents: Array<Doc>, options: DocumentOptions, id?: string) {
-            return InstanceFromProto(Prototypes.get(DocumentType.MARKER), new List(documents), { lat, lng, infoWindowOpen, ...options }, id);
-        }
-
         // shouldn't ever need to create a KVP document-- instead set the LayoutTemplateString to be a KeyValueBox for the DocumentView (see addDocTab in TabDocView)
         // export function KVPDocument(document: Doc, options: DocumentOptions = {}) {
         //     return InstanceFromProto(Prototypes.get(DocumentType.KVP), document, { title: document.title + '.kvp', ...options });
         // }
 
         export function FreeformDocument(documents: Array<Doc>, options: DocumentOptions, id?: string) {
-            const inst = InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { _xPadding: 20, _yPadding: 20, ...options, _viewType: CollectionViewType.Freeform }, id);
-            documents.map(d => (d.embedContainer = inst));
+            const inst = InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { _xPadding: 20, _yPadding: 20, freeform: '', ...options, _type_collection: CollectionViewType.Freeform }, id);
+            documents.forEach(d => (d.embedContainer = inst));
             return inst;
         }
 
-        export function WebanchorDocument(options: DocumentOptions = {}, id?: string) {
-            return InstanceFromProto(Prototypes.get(DocumentType.MARKER), undefined, options, id);
+        export function WebConfigDocument(options: DocumentOptions = {}, id?: string) {
+            return InstanceFromProto(Prototypes.get(DocumentType.CONFIG), undefined, options, id);
         }
-
-        export function CollectionAnchorDocument(options: DocumentOptions = {}, id?: string) {
-            return InstanceFromProto(Prototypes.get(DocumentType.MARKER), options?.data, options, id);
+        export function CollectionConfigDocument(options: DocumentOptions = {}, id?: string) {
+            return InstanceFromProto(Prototypes.get(DocumentType.CONFIG), options?.data, options, id);
         }
-        export function TextanchorDocument(options: DocumentOptions = {}, id?: string) {
-            return InstanceFromProto(Prototypes.get(DocumentType.MARKER), options?.data, options, id);
+        export function PdfConfigDocument(options: DocumentOptions = {}, id?: string) {
+            return InstanceFromProto(Prototypes.get(DocumentType.CONFIG), options?.data, options, id);
         }
-
-        export function ImageanchorDocument(options: DocumentOptions = {}, id?: string) {
-            return InstanceFromProto(Prototypes.get(DocumentType.MARKER), options?.data, options, id);
+        export function TextConfigDocument(options: DocumentOptions = {}, id?: string) {
+            return InstanceFromProto(Prototypes.get(DocumentType.CONFIG), options?.data, options, id);
         }
-
-        export function InkAnchorDocument(options: DocumentOptions, id?: string) {
-            return InstanceFromProto(Prototypes.get(DocumentType.MARKER), options?.data, options, id);
+        export function FunctionPlotConfigDocument(options: DocumentOptions = {}, id?: string) {
+            return InstanceFromProto(Prototypes.get(DocumentType.CONFIG), options?.data, options, id);
+        }
+        export function DataVizConfigDocument(options: DocumentOptions = {}, id?: string) {
+            return InstanceFromProto(Prototypes.get(DocumentType.CONFIG), options?.data, options, id);
+        }
+        export function LineChartConfigDocument(options: DocumentOptions = {}, id?: string) {
+            return InstanceFromProto(Prototypes.get(DocumentType.CONFIG), options?.data, options, id);
+        }
+        export function ComparisonConfigDocument(options: DocumentOptions = {}, id?: string) {
+            return InstanceFromProto(Prototypes.get(DocumentType.CONFIG), options?.data, options, id);
+        }
+        export function ImageConfigDocument(options: DocumentOptions = {}, id?: string) {
+            return InstanceFromProto(Prototypes.get(DocumentType.CONFIG), options?.data, options, id);
+        }
+        export function InkConfigDocument(options: DocumentOptions, id?: string) {
+            return InstanceFromProto(Prototypes.get(DocumentType.CONFIG), options?.data, options, id);
         }
 
-        export function HTMLAnchorDocument(documents: Array<Doc>, options: DocumentOptions, id?: string) {
-            return InstanceFromProto(Prototypes.get(DocumentType.MARKER), new List(documents), options, id);
+        export function HTMLMarkerDocument(documents: Array<Doc>, options: DocumentOptions, id?: string) {
+            return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _type_collection: CollectionViewType.Freeform }, id);
+        }
+        export function MapMarkerDocument(lat: number, lng: number, infoWindowOpen: boolean, documents: Array<Doc>, options: DocumentOptions, id?: string) {
+            return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { lat, lng, infoWindowOpen, ...options, _type_collection: CollectionViewType.Freeform }, id);
         }
 
         export function PileDocument(documents: Array<Doc>, options: DocumentOptions, id?: string) {
-            return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { _overflow: 'visible', enableDragWhenActive: true, _forceActive: true, _noAutoscroll: true, ...options, _viewType: CollectionViewType.Pile }, id);
+            return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { _overflow: 'visible', enableDragWhenActive: true, _forceActive: true, _noAutoscroll: true, ...options, _type_collection: CollectionViewType.Pile }, id);
         }
 
         export function LinearDocument(documents: Array<Doc>, options: DocumentOptions, id?: string) {
-            return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _viewType: CollectionViewType.Linear }, id);
+            return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _type_collection: CollectionViewType.Linear }, id);
         }
 
         export function MapCollectionDocument(documents: Array<Doc>, options: DocumentOptions = {}) {
-            return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _viewType: CollectionViewType.Map });
+            return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _type_collection: CollectionViewType.Map });
         }
 
         export function CarouselDocument(documents: Array<Doc>, options: DocumentOptions) {
-            return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _viewType: CollectionViewType.Carousel });
+            return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _type_collection: CollectionViewType.Carousel });
         }
 
         export function Carousel3DDocument(documents: Array<Doc>, options: DocumentOptions) {
-            return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _viewType: CollectionViewType.Carousel3D });
+            return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _type_collection: CollectionViewType.Carousel3D });
         }
 
         export function SchemaDocument(schemaHeaders: SchemaHeaderField[], documents: Array<Doc>, options: DocumentOptions) {
-            return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { schemaHeaders: new List(schemaHeaders), ...options, _viewType: CollectionViewType.Schema });
+            return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { schemaHeaders: new List(schemaHeaders), ...options, _type_collection: CollectionViewType.Schema });
         }
 
         export function TreeDocument(documents: Array<Doc>, options: DocumentOptions, id?: string, protoId?: string) {
-            return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { _xMargin: 5, _yMargin: 5, ...options, _viewType: CollectionViewType.Tree }, id, undefined, protoId);
+            return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { _xMargin: 5, _yMargin: 5, ...options, _type_collection: CollectionViewType.Tree }, id, undefined, protoId);
         }
 
         export function StackingDocument(documents: Array<Doc>, options: DocumentOptions, id?: string, protoId?: string) {
-            return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _viewType: CollectionViewType.Stacking }, id, undefined, protoId);
+            return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _type_collection: CollectionViewType.Stacking }, id, undefined, protoId);
         }
 
         export function NoteTakingDocument(documents: Array<Doc>, options: DocumentOptions, id?: string, protoId?: string) {
             return InstanceFromProto(
                 Prototypes.get(DocumentType.COL),
                 new List(documents),
-                { columnHeaders: new List<SchemaHeaderField>([new SchemaHeaderField('Untitled')]), ...options, _viewType: CollectionViewType.NoteTaking },
+                { columnHeaders: new List<SchemaHeaderField>([new SchemaHeaderField('Untitled')]), ...options, _type_collection: CollectionViewType.NoteTaking },
                 id,
                 undefined,
                 protoId
@@ -1110,14 +1122,14 @@ export namespace Docs {
         }
 
         export function MulticolumnDocument(documents: Array<Doc>, options: DocumentOptions) {
-            return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _viewType: CollectionViewType.Multicolumn });
+            return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _type_collection: CollectionViewType.Multicolumn });
         }
         export function MultirowDocument(documents: Array<Doc>, options: DocumentOptions) {
-            return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _viewType: CollectionViewType.Multirow });
+            return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _type_collection: CollectionViewType.Multirow });
         }
 
         export function MasonryDocument(documents: Array<Doc>, options: DocumentOptions) {
-            return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _viewType: CollectionViewType.Masonry });
+            return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _type_collection: CollectionViewType.Masonry });
         }
 
         export function LabelDocument(options?: DocumentOptions) {
@@ -1156,7 +1168,12 @@ export namespace Docs {
         }
 
         export function DockDocument(documents: Array<Doc>, config: string, options: DocumentOptions, id?: string) {
-            return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { freezeChildren: 'remove|add', ...options, viewType: CollectionViewType.Docking, _viewType: CollectionViewType.Docking, dockingConfig: config }, id);
+            return InstanceFromProto(
+                Prototypes.get(DocumentType.COL),
+                new List(documents),
+                { freezeChildren: 'remove|add', ...options, type_collection: CollectionViewType.Docking, _type_collection: CollectionViewType.Docking, dockingConfig: config },
+                id
+            );
         }
 
         export function DirectoryImportDocument(options: DocumentOptions = {}) {
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index 5cfd77937..26fe8f440 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -277,9 +277,9 @@ export class CurrentUserUtils {
             {key: "Script",     creator: opts => Docs.Create.ScriptingDocument(null, opts), opts: { _width: 200, _height: 250, }},
             {key: "DataViz",    creator: opts => Docs.Create.DataVizDocument("/users/rz/Downloads/addresses.csv", opts),   opts: { _width: 300, _height: 300 }},
             {key: "Header",     creator: headerTemplate,                                    opts: { _width: 300, _height: 70, _headerPointerEvents: "all", _headerHeight: 12, _headerFontSize: 9, _layout_autoHeight: true, treeViewHideUnrendered: true}},
-            {key: "Trail",      creator: Docs.Create.PresDocument,                          opts: { _width: 400, _height: 30, _viewType: CollectionViewType.Stacking, targetDropAction: "embed" as any, treeViewHideTitle: true, _layout_fitWidth:true, _chromeHidden: true, boxShadow: "0 0" }},
+            {key: "Trail",      creator: Docs.Create.PresDocument,                          opts: { _width: 400, _height: 30, _type_collection: CollectionViewType.Stacking, targetDropAction: "embed" as any, treeViewHideTitle: true, _layout_fitWidth:true, _chromeHidden: true, boxShadow: "0 0" }},
             {key: "Tab",        creator: opts => Docs.Create.FreeformDocument([], opts),    opts: { _width: 500, _height: 800, _layout_fitWidth: true, _freeform_backgroundGrid: true, }},
-            {key: "Slide",      creator: opts => Docs.Create.TreeDocument([], opts),        opts: { _width: 300, _height: 200, _viewType: CollectionViewType.Tree, 
+            {key: "Slide",      creator: opts => Docs.Create.TreeDocument([], opts),        opts: { _width: 300, _height: 200, _type_collection: CollectionViewType.Tree, 
                                                                                                 treeViewHasOverlay: true, _fontSize: "20px", _layout_autoHeight: true, 
                                                                                                 allowOverlayDrop: true, treeViewType: TreeViewType.outline, 
                                                                                                 backgroundColor: "white", _xMargin: 0, _yMargin: 0, _createDocOnCR: true 
@@ -455,7 +455,7 @@ export class CurrentUserUtils {
     static setupSearcher(doc: Doc, field:string) {
         return DocUtils.AssignDocField(doc, field, (opts, items) => Docs.Create.SearchDocument(opts), {
                 dontRegisterView: true, backgroundColor: "dimgray", ignoreClick: true, title: "Search Panel", isSystem: true, childDropAction: "embed", 
-                _lockedPosition: true, _viewType: CollectionViewType.Schema, _searchDoc: true,  });
+                _lockedPosition: true, _type_collection: CollectionViewType.Schema, _searchDoc: true,  });
     }
 
     /// Initializes the panel of draggable tools that is opened from the left sidebar.
@@ -774,7 +774,7 @@ export class CurrentUserUtils {
     // The sharing document also stores the user's color value which helps distinguish shared documents from personal documents
     static setupSharedDocs(doc: Doc, sharingDocumentId: string) {
         const addToDashboards = ScriptField.MakeScript(`addToDashboards(self)`);
-        const dashboardFilter = ScriptField.MakeFunction(`doc._viewType === '${CollectionViewType.Docking}'`, { doc: Doc.name });
+        const dashboardFilter = ScriptField.MakeFunction(`doc._type_collection === '${CollectionViewType.Docking}'`, { doc: Doc.name });
         const dblClkScript = "{scriptContext.openLevel(documentView); addDocToList(scriptContext.props.treeView.props.Document, 'viewed', documentView.rootDoc);}";
         
         const sharedScripts = {  treeViewChildDoubleClick: dblClkScript,  }
diff --git a/src/client/util/DictationManager.ts b/src/client/util/DictationManager.ts
index 1cce21034..6c710728b 100644
--- a/src/client/util/DictationManager.ts
+++ b/src/client/util/DictationManager.ts
@@ -372,7 +372,7 @@ export namespace DictationManager {
                 expression: /view as (freeform|stacking|masonry|schema|tree)/g,
                 action: (target: DocumentView, matches: RegExpExecArray) => {
                     const mode = matches[1];
-                    mode && (target.props.Document._viewType = mode);
+                    mode && (target.props.Document._type_collection = mode);
                 },
                 restrictTo: [DocumentType.COL],
             }
diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts
index 6eed4dc86..ea5770b6a 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -143,7 +143,7 @@ export class DocumentManager {
             (pass, toReturn) =>
                 toReturn ??
                 docViewArray.filter(view => view.rootDoc === doc).find(view => !pass || view.props.docViewPath().lastElement() === preferredCollection) ??
-                docViewArray.filter(view => Doc.GetProto(view.rootDoc) === doc).find(view => !pass || view.props.docViewPath().lastElement() === preferredCollection),
+                docViewArray.filter(view => Doc.AreProtosEqual(view.rootDoc, doc)).find(view => !pass || view.props.docViewPath().lastElement() === preferredCollection),
             undefined as Opt<DocumentView>
         );
     }
@@ -189,7 +189,7 @@ export class DocumentManager {
         while (
             containerDocContext.length &&
             containerDocContext[0]?.embedContainer &&
-            DocCast(containerDocContext[0].embedContainer)?.viewType !== CollectionViewType.Docking &&
+            DocCast(containerDocContext[0].embedContainer)?.type_collection !== CollectionViewType.Docking &&
             (includeExistingViews || !DocumentManager.Instance.getDocumentView(containerDocContext[0]))
         ) {
             containerDocContext = [Cast(containerDocContext[0].embedContainer, Doc, null), ...containerDocContext];
@@ -248,7 +248,10 @@ export class DocumentManager {
         if (docContextPath.some(doc => doc.hidden)) options.toggleTarget = false;
         let rootContextView = await new Promise<DocumentView>(res => {
             const viewIndex = docContextPath.findIndex(doc => this.getDocumentView(doc));
-            if (viewIndex !== -1) return res(this.getDocumentView(docContextPath[viewIndex])!);
+            if (viewIndex !== -1) {
+                viewIndex && docContextPath.splice(0, viewIndex);
+                return res(this.getDocumentView(docContextPath[0])!);
+            }
             options.didMove = true;
             docContextPath.some(doc => TabDocView.Activate(doc)) || DocumentViewInternal.addDocTabFunc(docContextPath[0], options.openLocation ?? OpenWhere.addRight);
             this.AddViewRenderedCb(docContextPath[0], dv => res(dv));
@@ -264,7 +267,7 @@ export class DocumentManager {
         docContextPath.shift();
         const childViewIterator = async (docView: DocumentView) => {
             const innerDoc = docContextPath.shift();
-            return { viewSpec: innerDoc, childDocView: innerDoc && !innerDoc.unrendered ? (await docView.ComponentView?.getView?.(innerDoc)) ?? this.getDocumentView(innerDoc) : undefined };
+            return { viewSpec: innerDoc, childDocView: innerDoc && !innerDoc.layout_unrendered ? (await docView.ComponentView?.getView?.(innerDoc)) ?? this.getDocumentView(innerDoc) : undefined };
         };
         const target = await this.focusViewsInPath(rootContextView, options, childViewIterator);
         this.restoreDocView(target.viewSpec, target.docView, options, target.contextView ?? target.docView, targetDoc);
diff --git a/src/client/util/LinkFollower.ts b/src/client/util/LinkFollower.ts
index 246f5bb8c..2812d6c88 100644
--- a/src/client/util/LinkFollower.ts
+++ b/src/client/util/LinkFollower.ts
@@ -42,13 +42,15 @@ export class LinkFollower {
     };
 
     public static traverseLink(link: Opt<Doc>, sourceDoc: Doc, finished?: () => void, traverseBacklink?: boolean) {
+        const getView = (doc: Doc) => DocumentManager.Instance.getFirstDocumentView(DocCast(doc.layout_unrendered ? doc.annotationOn : doc));
+        const isAnchor = (sourceDoc: Doc, anchor: Doc) => Doc.AreProtosEqual(anchor, sourceDoc) || Doc.AreProtosEqual(anchor.annotationOn as Doc, sourceDoc);
         const linkDocs = link ? [link] : LinkManager.Links(sourceDoc);
-        const firstDocs = linkDocs.filter(linkDoc => Doc.AreProtosEqual(linkDoc.link_anchor_1 as Doc, sourceDoc) || Doc.AreProtosEqual((linkDoc.link_anchor_1 as Doc).annotationOn as Doc, sourceDoc)); // link docs where 'doc' is link_anchor_1
-        const secondDocs = linkDocs.filter(linkDoc => Doc.AreProtosEqual(linkDoc.link_anchor_2 as Doc, sourceDoc) || Doc.AreProtosEqual((linkDoc.link_anchor_2 as Doc).annotationOn as Doc, sourceDoc)); // link docs where 'doc' is link_anchor_2
-        const fwdLinkWithoutTargetView = firstDocs.find(d => DocumentManager.Instance.getDocumentViews((d.link_anchor_2 as Doc).type === DocumentType.MARKER ? DocCast((d.link_anchor_2 as Doc).annotationOn) : (d.link_anchor_2 as Doc)).length === 0);
-        const backLinkWithoutTargetView = secondDocs.find(d => DocumentManager.Instance.getDocumentViews((d.link_anchor_1 as Doc).type === DocumentType.MARKER ? DocCast((d.link_anchor_1 as Doc).annotationOn) : (d.link_anchor_1 as Doc)).length === 0);
-        const linkWithoutTargetDoc = traverseBacklink === undefined ? fwdLinkWithoutTargetView || backLinkWithoutTargetView : traverseBacklink ? backLinkWithoutTargetView : fwdLinkWithoutTargetView;
-        const linkDocList = linkWithoutTargetDoc && !sourceDoc.followAllLinks ? [linkWithoutTargetDoc] : traverseBacklink === undefined ? firstDocs.concat(secondDocs) : traverseBacklink ? secondDocs : firstDocs;
+        const fwdLinks = linkDocs.filter(l => isAnchor(sourceDoc, l.link_anchor_1 as Doc)); // link docs where 'sourceDoc' is link_anchor_1
+        const backLinks = linkDocs.filter(l => isAnchor(sourceDoc, l.link_anchor_2 as Doc)); // link docs where 'sourceDoc' is link_anchor_2
+        const fwdLinkWithoutTargetView = fwdLinks.find(l => !getView(DocCast(l.link_anchor_2)));
+        const backLinkWithoutTargetView = backLinks.find(l => !getView(DocCast(l.link_anchor_1)));
+        const linkWithoutTargetDoc = traverseBacklink === undefined ? fwdLinkWithoutTargetView ?? backLinkWithoutTargetView : traverseBacklink ? backLinkWithoutTargetView : fwdLinkWithoutTargetView;
+        const linkDocList = linkWithoutTargetDoc && !sourceDoc.followAllLinks ? [linkWithoutTargetDoc] : traverseBacklink === undefined ? fwdLinks.concat(backLinks) : traverseBacklink ? backLinks : fwdLinks;
         const followLinks = sourceDoc.followLinkToggle || sourceDoc.followAllLinks ? linkDocList : linkDocList.slice(0, 1);
         var count = 0;
         const allFinished = () => ++count === followLinks.length && finished?.();
diff --git a/src/client/util/SelectionManager.ts b/src/client/util/SelectionManager.ts
index bfad93334..fba0a4f76 100644
--- a/src/client/util/SelectionManager.ts
+++ b/src/client/util/SelectionManager.ts
@@ -2,7 +2,7 @@ import { action, observable, ObservableMap } from 'mobx';
 import { computedFn } from 'mobx-utils';
 import { Doc, Opt } from '../../fields/Doc';
 import { DocCast } from '../../fields/Types';
-import { CollectionViewType, DocumentType } from '../documents/DocumentTypes';
+import { CollectionViewType } from '../documents/DocumentTypes';
 import { DocumentView } from '../views/nodes/DocumentView';
 import { LinkManager } from './LinkManager';
 import { ScriptingGlobals } from './ScriptingGlobals';
@@ -21,7 +21,7 @@ export namespace SelectionManager {
         @action
         SelectView(docView: DocumentView, ctrlPressed: boolean): void {
             // if doc is not in SelectedDocuments, add it
-            if (!manager.SelectedViewsMap.get(docView) && docView.props.Document.type !== DocumentType.MARKER) {
+            if (!manager.SelectedViewsMap.get(docView)) {
                 if (!ctrlPressed) {
                     if (LinkManager.currentLink && !LinkManager.Links(docView.rootDoc).includes(LinkManager.currentLink) && docView.rootDoc !== LinkManager.currentLink) {
                         LinkManager.currentLink = undefined;
@@ -103,14 +103,14 @@ export namespace SelectionManager {
 
     export function Views(): Array<DocumentView> {
         return manager.SelectedViews;
-        // Array.from(manager.SelectedViewsMap.keys()); //.filter(dv => manager.SelectedViews.get(dv)?._viewType !== CollectionViewType.Docking);
+        // Array.from(manager.SelectedViewsMap.keys()); //.filter(dv => manager.SelectedViews.get(dv)?._type_collection !== CollectionViewType.Docking);
     }
     export function SelectedSchemaDoc(): Doc | undefined {
         return manager.SelectedSchemaDocument;
     }
     export function Docs(): Doc[] {
-        return manager.SelectedViews.map(dv => dv.rootDoc).filter(doc => doc?._viewType !== CollectionViewType.Docking);
-        // Array.from(manager.SelectedViewsMap.values()).filter(doc => doc?._viewType !== CollectionViewType.Docking);
+        return manager.SelectedViews.map(dv => dv.rootDoc).filter(doc => doc?._type_collection !== CollectionViewType.Docking);
+        // Array.from(manager.SelectedViewsMap.values()).filter(doc => doc?._type_collection !== CollectionViewType.Docking);
     }
 }
 ScriptingGlobals.add(function SelectionManager_selectedDocType(type: string, expertMode: boolean, checkContext?: boolean) {
@@ -119,5 +119,5 @@ ScriptingGlobals.add(function SelectionManager_selectedDocType(type: string, exp
         return SelectionManager.Views().lastElement()?.props.renderDepth === 0;
     }
     let selected = (sel => (checkContext ? DocCast(sel?.context) : sel))(SelectionManager.SelectedSchemaDoc() ?? SelectionManager.Docs().lastElement());
-    return selected?.type === type || selected?.viewType === type || !type;
+    return selected?.type === type || selected?.type_collection === type || !type;
 });
diff --git a/src/client/util/TrackMovements.ts b/src/client/util/TrackMovements.ts
index a755c5dc9..f83b6af0e 100644
--- a/src/client/util/TrackMovements.ts
+++ b/src/client/util/TrackMovements.ts
@@ -89,7 +89,7 @@ export class TrackMovements {
         if (this.recordingFFViews === null) return;
 
         // so that the size comparisons are correct, we must filter to only the FFViews
-        const isFFView = (doc: Doc) => doc && 'viewType' in doc && doc.viewType === 'freeform';
+        const isFFView = (doc: Doc) => doc && 'type_collection' in doc && doc.type_collection === 'freeform';
         const tabbedFFViews = new Set<Doc>();
         for (const DashDoc of tabbedDocs) {
             if (isFFView(DashDoc)) tabbedFFViews.add(DashDoc);
diff --git a/src/client/views/DashboardView.tsx b/src/client/views/DashboardView.tsx
index 02c5085a2..b51c77cb2 100644
--- a/src/client/views/DashboardView.tsx
+++ b/src/client/views/DashboardView.tsx
@@ -66,18 +66,18 @@ export class DashboardView extends React.Component {
         if (this.selectedDashboardGroup === DashboardGroup.MyDashboards) {
             return allDashboards.filter(dashboard => Doc.GetProto(dashboard).author === Doc.CurrentUserEmail);
         } else {
-            const sharedDashboards = DocListCast(Doc.MySharedDocs.data).filter(doc => doc._viewType === CollectionViewType.Docking);
+            const sharedDashboards = DocListCast(Doc.MySharedDocs.data).filter(doc => doc._type_collection === CollectionViewType.Docking);
             return sharedDashboards;
         }
     };
 
     isUnviewedSharedDashboard = (dashboard: Doc): boolean => {
-        // const sharedDashboards = DocListCast(Doc.MySharedDocs.data).filter(doc => doc._viewType === CollectionViewType.Docking);
+        // const sharedDashboards = DocListCast(Doc.MySharedDocs.data).filter(doc => doc._type_collection === CollectionViewType.Docking);
         return !DocListCast(Doc.MySharedDocs.viewed).includes(dashboard);
     };
 
     getSharedDashboards = () => {
-        const sharedDashs = DocListCast(Doc.MySharedDocs.data).filter(doc => doc._viewType === CollectionViewType.Docking);
+        const sharedDashs = DocListCast(Doc.MySharedDocs.data).filter(doc => doc._type_collection === CollectionViewType.Docking);
         return sharedDashs.filter(dashboard => !DocListCast(Doc.MySharedDocs.viewed).includes(dashboard));
     };
 
diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx
index 14c5dc58d..3a8cec07e 100644
--- a/src/client/views/InkingStroke.tsx
+++ b/src/client/views/InkingStroke.tsx
@@ -87,7 +87,13 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() {
 
         if (!addAsAnnotation && !pinProps) return this.rootDoc;
 
-        const anchor = Docs.Create.InkAnchorDocument({ title: 'Ink anchor:' + this.rootDoc.title, presDuration: 1100, presTransition: 1000, unrendered: true, annotationOn: this.rootDoc });
+        const anchor = Docs.Create.InkConfigDocument({
+            title: 'Ink anchor:' + this.rootDoc.title,
+            // set presentation timing for restoring shape
+            presDuration: 1100,
+            presTransition: 1000,
+            annotationOn: this.rootDoc,
+        });
         if (anchor) {
             anchor.backgroundColor = 'transparent';
             // /* addAsAnnotation &&*/ this.addDocument(anchor);
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index ce29b63cd..853f9cace 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -81,7 +81,7 @@ export class MainView extends React.Component {
     @observable private _sidebarContent: any = Doc.MyLeftSidebarPanel;
     @observable private _leftMenuFlyoutWidth: number = 0;
     @computed get _hideUI() {
-        return this.mainDoc && this.mainDoc._viewType !== CollectionViewType.Docking;
+        return this.mainDoc && this.mainDoc._type_collection !== CollectionViewType.Docking;
     }
 
     @computed private get dashboardTabHeight() {
@@ -158,6 +158,7 @@ export class MainView extends React.Component {
                 'viewTransition',
                 'treeViewOpen',
                 'layout_showSidebar',
+                'carousel_index',
                 'itemIndex', // for changing slides in presentations
                 'layout_sidebarWidthPercent',
                 'layout_currentTimecode',
@@ -177,7 +178,7 @@ export class MainView extends React.Component {
                 'layout_scrollTop',
                 'hidden',
                 'layout_curPage',
-                'viewType',
+                'type_collection',
                 'chromeHidden',
                 'currentFrame',
                 'width',
@@ -236,7 +237,7 @@ export class MainView extends React.Component {
             if (pathname.length > 1 && pathname[0] === 'doc') {
                 DocServer.GetRefField(pathname[1]).then(
                     action(field => {
-                        if (field instanceof Doc && field._viewType !== CollectionViewType.Docking) {
+                        if (field instanceof Doc && field._type_collection !== CollectionViewType.Docking) {
                             Doc.GuestTarget = field;
                         }
                     })
@@ -693,7 +694,7 @@ export class MainView extends React.Component {
     sidebarScreenToLocal = () => new Transform(0, -this.topOfSidebarDoc, 1);
     mainContainerXf = () => this.sidebarScreenToLocal().translate(-this.leftScreenOffsetOfMainDocView, 0);
     static addDocTabFunc_impl = (doc: Doc, location: OpenWhere): boolean => {
-        const whereFields = doc._viewType === CollectionViewType.Docking ? [OpenWhere.dashboard] : location.split(':');
+        const whereFields = doc._type_collection === CollectionViewType.Docking ? [OpenWhere.dashboard] : location.split(':');
         const keyValue = whereFields[1]?.includes('KeyValue');
         const whereMods: OpenWhereMod = whereFields.length > 1 ? (whereFields[1].replace('KeyValue', '') as OpenWhereMod) : OpenWhereMod.none;
         if (doc.dockingConfig) return DashboardView.openDashboard(doc);
diff --git a/src/client/views/MarqueeAnnotator.tsx b/src/client/views/MarqueeAnnotator.tsx
index eb0fd0c7a..1d6c94c13 100644
--- a/src/client/views/MarqueeAnnotator.tsx
+++ b/src/client/views/MarqueeAnnotator.tsx
@@ -162,7 +162,7 @@ export class MarqueeAnnotator extends React.Component<MarqueeAnnotatorProps> {
             return marqueeAnno;
         }
 
-        const textRegionAnno = Docs.Create.HTMLAnchorDocument([], {
+        const textRegionAnno = Docs.Create.HTMLMarkerDocument([], {
             annotationOn: this.props.rootDoc,
             text: this.props.selectionText(),
             backgroundColor: 'transparent',
diff --git a/src/client/views/MetadataEntryMenu.tsx b/src/client/views/MetadataEntryMenu.tsx
index 82ec5a5b3..bcbdd3ccb 100644
--- a/src/client/views/MetadataEntryMenu.tsx
+++ b/src/client/views/MetadataEntryMenu.tsx
@@ -1,5 +1,5 @@
 import * as React from 'react';
-import "./MetadataEntryMenu.scss";
+import './MetadataEntryMenu.scss';
 import { observer } from 'mobx-react';
 import { observable, action, runInAction, trace, computed, IReactionDisposer, reaction } from 'mobx';
 import { KeyValueBox } from './nodes/KeyValueBox';
@@ -16,9 +16,9 @@ export interface MetadataEntryProps {
 }
 
 @observer
-export class MetadataEntryMenu extends React.Component<MetadataEntryProps>{
-    @observable private _currentKey: string = "";
-    @observable private _currentValue: string = "";
+export class MetadataEntryMenu extends React.Component<MetadataEntryProps> {
+    @observable private _currentKey: string = '';
+    @observable private _currentValue: string = '';
     private _addChildren: boolean = false;
     @observable _allSuggestions: string[] = [];
     _suggestionDispser: IReactionDisposer | undefined;
@@ -32,7 +32,7 @@ export class MetadataEntryMenu extends React.Component<MetadataEntryProps>{
         if (!this.userModified) {
             this.previewValue();
         }
-    }
+    };
 
     previewValue = async () => {
         let field: Field | undefined | null = null;
@@ -45,30 +45,30 @@ export class MetadataEntryMenu extends React.Component<MetadataEntryProps>{
             if (field === null) {
                 field = v;
             } else if (v !== field) {
-                value = "multiple values";
+                value = 'multiple values';
             }
         }
         if (value === undefined) {
             if (field !== null && field !== undefined) {
-                value = (onProto ? "" : "= ") + Field.toScriptString(field);
+                value = (onProto ? '' : '= ') + Field.toScriptString(field);
             } else {
-                value = "";
+                value = '';
             }
         }
         const s = value;
-        runInAction(() => this._currentValue = s);
-    }
+        runInAction(() => (this._currentValue = s));
+    };
 
     @action
     onValueChange = (e: React.ChangeEvent<HTMLInputElement>) => {
         this._currentValue = e.target.value;
-        this.userModified = e.target.value.trim() !== "";
-    }
+        this.userModified = e.target.value.trim() !== '';
+    };
 
     @undoBatch
     @action
     onValueKeyDown = async (e: React.KeyboardEvent) => {
-        if (e.key === "Enter") {
+        if (e.key === 'Enter') {
             e.stopPropagation();
             const script = KeyValueBox.CompileKVPScript(this._currentValue);
             if (!script) return;
@@ -95,18 +95,18 @@ export class MetadataEntryMenu extends React.Component<MetadataEntryProps>{
                 this.clearInputs();
             }
         }
-    }
+    };
 
     @action
     clearInputs = () => {
-        this._currentKey = "";
-        this._currentValue = "";
+        this._currentKey = '';
+        this._currentValue = '';
         this.userModified = false;
         if (this.autosuggestRef.current) {
             const input: HTMLInputElement = (this.autosuggestRef.current as any).input;
             input && input.focus();
         }
-    }
+    };
 
     getKeySuggestions = (value: string) => {
         value = value.toLowerCase();
@@ -114,16 +114,18 @@ export class MetadataEntryMenu extends React.Component<MetadataEntryProps>{
         const keys = new Set<string>();
         docs.forEach(doc => Doc.allKeys(doc).forEach(key => keys.add(key)));
         return Array.from(keys).filter(key => key.toLowerCase().startsWith(value));
-    }
+    };
     getSuggestionValue = (suggestion: string) => suggestion;
 
     renderSuggestion = (suggestion: string) => {
-        return (null);
-    }
+        return null;
+    };
     componentDidMount() {
-        this._suggestionDispser = reaction(() => this._currentKey,
-            () => this._allSuggestions = this.getKeySuggestions(this._currentKey),
-            { fireImmediately: true });
+        this._suggestionDispser = reaction(
+            () => this._currentKey,
+            () => (this._allSuggestions = this.getKeySuggestions(this._currentKey)),
+            { fireImmediately: true }
+        );
     }
     componentWillUnmount() {
         this._suggestionDispser && this._suggestionDispser();
@@ -131,49 +133,64 @@ export class MetadataEntryMenu extends React.Component<MetadataEntryProps>{
 
     onClick = (e: React.ChangeEvent<HTMLInputElement>) => {
         this._addChildren = !this._addChildren;
-    }
+    };
 
     private get considerChildOptions() {
-        if (!this.props.docs.every(doc => doc._viewType !== undefined)) {
+        if (!this.props.docs.every(doc => doc._type_collection !== undefined)) {
             return null;
         }
         return (
-            <div style={{ display: "flex" }}>
+            <div style={{ display: 'flex' }}>
                 Children:
-                <input type="checkbox" onChange={this.onClick} ></input>
+                <input type="checkbox" onChange={this.onClick}></input>
             </div>
         );
     }
 
     _ref = React.createRef<HTMLInputElement>();
     render() {
-        return (<div className="metadataEntry-outerDiv" id="metadataEntry-outer" onPointerDown={e => e.stopPropagation()}>
-            <div className="metadataEntry-inputArea">
-                <div style={{ display: "flex", flexDirection: "row" }}>
-                    <span>Key:</span>
-                    <div className="metadataEntry-autoSuggester" onClick={e => this.autosuggestRef.current!.input?.focus()}  >
-                        <Autosuggest inputProps={{ value: this._currentKey, onChange: this.onKeyChange }}
-                            getSuggestionValue={this.getSuggestionValue}
-                            suggestions={emptyPath}
-                            alwaysRenderSuggestions={false}
-                            renderSuggestion={this.renderSuggestion}
-                            onSuggestionsFetchRequested={emptyFunction}
-                            onSuggestionsClearRequested={emptyFunction}
-                            ref={this.autosuggestRef} />
+        return (
+            <div className="metadataEntry-outerDiv" id="metadataEntry-outer" onPointerDown={e => e.stopPropagation()}>
+                <div className="metadataEntry-inputArea">
+                    <div style={{ display: 'flex', flexDirection: 'row' }}>
+                        <span>Key:</span>
+                        <div className="metadataEntry-autoSuggester" onClick={e => this.autosuggestRef.current!.input?.focus()}>
+                            <Autosuggest
+                                inputProps={{ value: this._currentKey, onChange: this.onKeyChange }}
+                                getSuggestionValue={this.getSuggestionValue}
+                                suggestions={emptyPath}
+                                alwaysRenderSuggestions={false}
+                                renderSuggestion={this.renderSuggestion}
+                                onSuggestionsFetchRequested={emptyFunction}
+                                onSuggestionsClearRequested={emptyFunction}
+                                ref={this.autosuggestRef}
+                            />
+                        </div>
                     </div>
+                    <div style={{ display: 'flex', flexDirection: 'row' }}>
+                        <span>Value:</span>
+                        <input className="metadataEntry-input" ref={this._ref} value={this._currentValue} onClick={e => this._ref.current!.focus()} onChange={this.onValueChange} onKeyDown={this.onValueKeyDown} />
+                    </div>
+                    {this.considerChildOptions}
                 </div>
-                <div style={{ display: "flex", flexDirection: "row" }}>
-                    <span>Value:</span>
-                    <input className="metadataEntry-input" ref={this._ref} value={this._currentValue} onClick={e => this._ref.current!.focus()} onChange={this.onValueChange} onKeyDown={this.onValueKeyDown} />
+                <div className="metadataEntry-keys">
+                    <ul>
+                        {this._allSuggestions
+                            .slice()
+                            .sort()
+                            .map(s => (
+                                <li
+                                    key={s}
+                                    onClick={action(() => {
+                                        this._currentKey = s;
+                                        this.previewValue();
+                                    })}>
+                                    {s}
+                                </li>
+                            ))}
+                    </ul>
                 </div>
-                {this.considerChildOptions}
-            </div>
-            <div className="metadataEntry-keys" >
-                <ul>
-                    {this._allSuggestions.slice().sort().map(s => <li key={s} onClick={action(() => { this._currentKey = s; this.previewValue(); })} >{s}</li>)}
-                </ul>
             </div>
-        </div>
         );
     }
-}
\ No newline at end of file
+}
diff --git a/src/client/views/PropertiesButtons.tsx b/src/client/views/PropertiesButtons.tsx
index 86230ca78..a37447505 100644
--- a/src/client/views/PropertiesButtons.tsx
+++ b/src/client/views/PropertiesButtons.tsx
@@ -281,11 +281,11 @@ export class PropertiesButtons extends React.Component<{}, {}> {
 
     @undoBatch
     handlePerspectiveChange = (e: any) => {
-        this.selectedDoc && (this.selectedDoc._viewType = e.target.value);
+        this.selectedDoc && (this.selectedDoc._type_collection = e.target.value);
         SelectionManager.Views()
             .filter(dv => dv.docView)
             .map(dv => dv.docView!)
-            .forEach(docView => (docView.layoutDoc._viewType = e.target.value));
+            .forEach(docView => (docView.layoutDoc._type_collection = e.target.value));
     };
 
     @undoBatch
@@ -373,7 +373,7 @@ export class PropertiesButtons extends React.Component<{}, {}> {
         const makeLabel = (value: string, label: string) => (
             <div className="radio" key={label}>
                 <label>
-                    <input type="radio" value={value} checked={(this.selectedDoc?._viewType ?? 'invalid') === value} onChange={this.handlePerspectiveChange} />
+                    <input type="radio" value={value} checked={(this.selectedDoc?._type_collection ?? 'invalid') === value} onChange={this.handlePerspectiveChange} />
                     {label}
                 </label>
             </div>
@@ -395,9 +395,9 @@ export class PropertiesButtons extends React.Component<{}, {}> {
         const isMap = this.selectedDoc?.type === DocumentType.MAP;
         const isCollection = this.selectedDoc?.type === DocumentType.COL;
         //TODO: will likely need to create separate note-taking view type here
-        const isStacking = this.selectedDoc?._viewType === CollectionViewType.Stacking || this.selectedDoc?._viewType === CollectionViewType.NoteTaking;
-        const isFreeForm = this.selectedDoc?._viewType === CollectionViewType.Freeform;
-        const isTree = this.selectedDoc?._viewType === CollectionViewType.Tree;
+        const isStacking = this.selectedDoc?._type_collection === CollectionViewType.Stacking || this.selectedDoc?._type_collection === CollectionViewType.NoteTaking;
+        const isFreeForm = this.selectedDoc?._type_collection === CollectionViewType.Freeform;
+        const isTree = this.selectedDoc?._type_collection === CollectionViewType.Tree;
         const isTabView = this.selectedTabView;
         const toggle = (ele: JSX.Element | null, style?: React.CSSProperties) => (
             <div className="propertiesButtons-button" style={style}>
diff --git a/src/client/views/SidebarAnnos.tsx b/src/client/views/SidebarAnnos.tsx
index 741e87644..e12621f35 100644
--- a/src/client/views/SidebarAnnos.tsx
+++ b/src/client/views/SidebarAnnos.tsx
@@ -141,7 +141,7 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> {
         return target;
     };
     makeDocUnfiltered = (doc: Doc) => {
-        if (DocListCast(this.props.rootDoc[this.sidebarKey]).find(anno => Doc.AreProtosEqual(doc.unrendered ? DocCast(doc.annotationOn) : doc, anno))) {
+        if (DocListCast(this.props.rootDoc[this.sidebarKey]).find(anno => Doc.AreProtosEqual(doc.layout_unrendered ? DocCast(doc.annotationOn) : doc, anno))) {
             if (this.props.layoutDoc[this.filtersKey]) {
                 this.props.layoutDoc[this.filtersKey] = new List<string>();
             }
@@ -245,7 +245,7 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> {
                         addDocument={this.addDocument}
                         ScreenToLocalTransform={this.screenToLocalTransform}
                         renderDepth={this.props.renderDepth + 1}
-                        viewType={CollectionViewType.Stacking}
+                        type_collection={CollectionViewType.Stacking}
                         fieldKey={this.sidebarKey}
                         pointerEvents={returnAll}
                     />
diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx
index e852d9297..28c61a11e 100644
--- a/src/client/views/StyleProvider.tsx
+++ b/src/client/views/StyleProvider.tsx
@@ -112,7 +112,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps
             const allSorts: { [key: string]: { color: string; label: string } | undefined } = {};
             allSorts[TreeSort.Down] = { color: 'blue', label: '↓' };
             allSorts[TreeSort.Up] = { color: 'crimson', label: '↑' };
-            if (doc?._viewType === CollectionViewType.Freeform) allSorts[TreeSort.Zindex] = { color: 'green', label: 'z' };
+            if (doc?._type_collection === CollectionViewType.Freeform) allSorts[TreeSort.Zindex] = { color: 'green', label: 'z' };
             allSorts[TreeSort.None] = { color: 'darkgray', label: '\u00A0\u00A0\u00A0' };
             return allSorts;
         case StyleProp.Highlighting:
@@ -173,7 +173,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps
             if (!backColor) return undefined;
             return lightOrDark(backColor);
         case StyleProp.BorderRounding:
-            return StrCast(doc?.[fieldKey + 'borderRounding'], StrCast(doc?.borderRounding, doc?._viewType === CollectionViewType.Pile ? '50%' : ''));
+            return StrCast(doc?.[fieldKey + 'borderRounding'], StrCast(doc?.borderRounding, doc?._type_collection === CollectionViewType.Pile ? '50%' : ''));
         case StyleProp.TitleHeight:
             return 15;
         case StyleProp.BorderPath:
@@ -195,9 +195,9 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps
         case StyleProp.JitterRotation:
             return Doc.IsComicStyle(doc) ? random(-1, 1, NumCast(doc?.x), NumCast(doc?.y)) * ((props?.PanelWidth() || 0) > (props?.PanelHeight() || 0) ? 5 : 10) : 0;
         case StyleProp.showCaption:
-            return doc?._viewType === CollectionViewType.Carousel || props?.hideCaptions ? undefined : StrCast(doc?._layout_showCaption);
+            return doc?._type_collection === CollectionViewType.Carousel || props?.hideCaptions ? undefined : StrCast(doc?._layout_showCaption);
         case StyleProp.HeaderMargin:
-            return ([CollectionViewType.Stacking, CollectionViewType.NoteTaking, CollectionViewType.Masonry, CollectionViewType.Tree].includes(doc?._viewType as any) ||
+            return ([CollectionViewType.Stacking, CollectionViewType.NoteTaking, CollectionViewType.Masonry, CollectionViewType.Tree].includes(doc?._type_collection as any) ||
                 (doc?.type === DocumentType.RTF && !layout_showTitle()?.includes('noMargin')) ||
                 doc?.type === DocumentType.LABEL) &&
                 layout_showTitle() &&
@@ -265,7 +265,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps
                             ? undefined
                             : Cast((props?.renderDepth || 0) > 0 ? Doc.UserDoc().activeCollectionNestedBackground : Doc.UserDoc().activeCollectionBackground, 'string') ?? (darkScheme() ? Colors.BLACK : 'linear-gradient(#065fff, #85c1f9)'));
                     break;
-                //if (doc._viewType !== CollectionViewType.Freeform && doc._viewType !== CollectionViewType.Time) return "rgb(62,62,62)";
+                //if (doc._type_collection !== CollectionViewType.Freeform && doc._type_collection !== CollectionViewType.Time) return "rgb(62,62,62)";
                 default:
                     docColor = docColor || (darkScheme() ? Colors.DARK_GRAY : Colors.WHITE);
                     break;
@@ -281,7 +281,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps
                 case DocumentType.COL:
                     return StrCast(
                         doc?.boxShadow,
-                        doc?._viewType === CollectionViewType.Pile
+                        doc?._type_collection === CollectionViewType.Pile
                             ? '4px 4px 10px 2px'
                             : lockedPosition() || doc?._isGroup || docProps?.LayoutTemplateString
                             ? undefined // groups have no drop shadow -- they're supposed to be "invisible".  LayoutString's imply collection is being rendered as something else (e.g., title of a Slide)
@@ -305,7 +305,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps
         case StyleProp.PointerEvents:
             const isInk = doc && StrCast(Doc.Layout(doc).layout).includes(InkingStroke.name) && !props?.LayoutTemplateString;
             if (docProps?.DocumentView?.().ComponentView?.overridePointerEvents?.() !== undefined) return docProps?.DocumentView?.().ComponentView?.overridePointerEvents?.();
-            if (MainView.Instance._exploreMode || doc?.unrendered) return isInk ? 'visiblePainted' : 'all';
+            if (MainView.Instance._exploreMode || doc?.layout_unrendered) return isInk ? 'visiblePainted' : 'all';
             if (doc?.pointerEvents) return StrCast(doc.pointerEvents);
             if (props?.contentPointerEvents) return StrCast(props.contentPointerEvents);
             if (props?.pointerEvents?.() === 'none') return 'none';
@@ -314,7 +314,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps
             return undefined; // fixes problem with tree view elements getting pointer events when the tree view is not active
         case StyleProp.Decorations:
             const lock = () => {
-                if (props?.docViewPath().lastElement()?.rootDoc?._viewType === CollectionViewType.Freeform) {
+                if (props?.docViewPath().lastElement()?.rootDoc?._type_collection === CollectionViewType.Freeform) {
                     return doc?.pointerEvents !== 'none' ? null : (
                         <div className="styleProvider-lock" onClick={() => toggleLockedPosition(doc)}>
                             <FontAwesomeIcon icon={'lock'} style={{ color: 'red' }} size="lg" />
@@ -378,7 +378,7 @@ export function DashboardToggleButton(doc: Doc, field: string, onIcon: IconProp,
  */
 export function DashboardStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps | DocumentViewProps>, property: string) {
     if (doc && property.split(':')[0] === StyleProp.Decorations) {
-        return doc._viewType === CollectionViewType.Docking ? null : (
+        return doc._type_collection === CollectionViewType.Docking ? null : (
             <>
                 {DashboardToggleButton(doc, 'hidden', 'eye-slash', 'eye', () => {
                     DocFocusOrOpen(doc, { toggleTarget: true, willZoomCentered: true, zoomScale: 0 }, DocCast(doc?.embedContainer ?? doc?.annotationOn));
diff --git a/src/client/views/collections/CollectionCarousel3DView.tsx b/src/client/views/collections/CollectionCarousel3DView.tsx
index a266c9207..b10b453bf 100644
--- a/src/client/views/collections/CollectionCarousel3DView.tsx
+++ b/src/client/views/collections/CollectionCarousel3DView.tsx
@@ -39,7 +39,7 @@ export class CollectionCarousel3DView extends CollectionSubView() {
     isChildContentActive = () => (this.isContentActive() ? true : false);
 
     @computed get content() {
-        const currentIndex = NumCast(this.layoutDoc._itemIndex);
+        const currentIndex = NumCast(this.layoutDoc._carousel_index);
         const displayDoc = (childPair: { layout: Doc; data: Doc }) => {
             return (
                 <DocumentView
@@ -75,7 +75,7 @@ export class CollectionCarousel3DView extends CollectionSubView() {
     }
 
     changeSlide = (direction: number) => {
-        this.layoutDoc._itemIndex = (NumCast(this.layoutDoc._itemIndex) + direction + this.childLayoutPairs.length) % this.childLayoutPairs.length;
+        this.layoutDoc._carousel_index = (NumCast(this.layoutDoc._carousel_index) + direction + this.childLayoutPairs.length) % this.childLayoutPairs.length;
     };
 
     onArrowClick = (e: React.MouseEvent, direction: number) => {
@@ -139,11 +139,11 @@ export class CollectionCarousel3DView extends CollectionSubView() {
     }
 
     @computed get dots() {
-        return this.childLayoutPairs.map((_child, index) => <div key={Utils.GenerateGuid()} className={`dot${index === NumCast(this.layoutDoc._itemIndex) ? '-active' : ''}`} onClick={() => (this.layoutDoc._itemIndex = index)} />);
+        return this.childLayoutPairs.map((_child, index) => <div key={Utils.GenerateGuid()} className={`dot${index === NumCast(this.layoutDoc._carousel_index) ? '-active' : ''}`} onClick={() => (this.layoutDoc._carousel_index = index)} />);
     }
 
     render() {
-        const index = NumCast(this.layoutDoc._itemIndex);
+        const index = NumCast(this.layoutDoc._carousel_index);
         const translateX = this.panelWidth() * (1 - index);
 
         return (
diff --git a/src/client/views/collections/CollectionCarouselView.tsx b/src/client/views/collections/CollectionCarouselView.tsx
index 9ec78155c..0eb61a0b2 100644
--- a/src/client/views/collections/CollectionCarouselView.tsx
+++ b/src/client/views/collections/CollectionCarouselView.tsx
@@ -4,7 +4,7 @@ import { observer } from 'mobx-react';
 import * as React from 'react';
 import { Doc, Opt } from '../../../fields/Doc';
 import { NumCast, ScriptCast, StrCast } from '../../../fields/Types';
-import { emptyFunction, returnFalse, returnZero, StopEvent } from '../../../Utils';
+import { returnFalse, returnZero, StopEvent } from '../../../Utils';
 import { DragManager } from '../../util/DragManager';
 import { DocumentView, DocumentViewProps } from '../nodes/DocumentView';
 import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox';
@@ -30,11 +30,11 @@ export class CollectionCarouselView extends CollectionSubView() {
 
     advance = (e: React.MouseEvent) => {
         e.stopPropagation();
-        this.layoutDoc._itemIndex = (NumCast(this.layoutDoc._itemIndex) + 1) % this.childLayoutPairs.length;
+        this.layoutDoc._carousel_index = (NumCast(this.layoutDoc._carousel_index) + 1) % this.childLayoutPairs.length;
     };
     goback = (e: React.MouseEvent) => {
         e.stopPropagation();
-        this.layoutDoc._itemIndex = (NumCast(this.layoutDoc._itemIndex) - 1 + this.childLayoutPairs.length) % this.childLayoutPairs.length;
+        this.layoutDoc._carousel_index = (NumCast(this.layoutDoc._carousel_index) - 1 + this.childLayoutPairs.length) % this.childLayoutPairs.length;
     };
     captionStyleProvider = (doc: Doc | undefined, captionProps: Opt<DocumentViewProps>, property: string): any => {
         // first look for properties on the document in the carousel, then fallback to properties on the container
@@ -45,7 +45,7 @@ export class CollectionCarouselView extends CollectionSubView() {
     onContentDoubleClick = () => ScriptCast(this.layoutDoc.onChildDoubleClick);
     onContentClick = () => ScriptCast(this.layoutDoc.onChildClick);
     @computed get content() {
-        const index = NumCast(this.layoutDoc._itemIndex);
+        const index = NumCast(this.layoutDoc._carousel_index);
         const curDoc = this.childLayoutPairs?.[index];
         const captionProps = { ...this.props, fieldKey: 'caption', setHeight: undefined };
         const marginX = NumCast(this.layoutDoc['caption_xMargin']);
diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index 3a4691484..cce21a3aa 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -127,7 +127,7 @@ export class CollectionDockingView extends CollectionSubView() {
         SelectionManager.DeselectAll();
         const instance = CollectionDockingView.Instance;
         if (instance) {
-            if (doc._viewType === CollectionViewType.Docking && doc.layout_fieldKey === 'layout') {
+            if (doc._type_collection === CollectionViewType.Docking && doc.layout_fieldKey === 'layout') {
                 return DashboardView.openDashboard(doc);
             }
             const newItemStackConfig = {
@@ -183,7 +183,7 @@ export class CollectionDockingView extends CollectionSubView() {
     @undoBatch
     @action
     public static AddSplit(document: Doc, pullSide: OpenWhereMod, stack?: any, panelName?: string, keyValue?: boolean) {
-        if (document?._viewType === CollectionViewType.Docking) return DashboardView.openDashboard(document);
+        if (document?._type_collection === CollectionViewType.Docking) return DashboardView.openDashboard(document);
         if (!CollectionDockingView.Instance) return false;
         const tab = Array.from(CollectionDockingView.Instance.tabMap).find(tab => tab.DashDoc === document && !keyValue);
         if (tab) {
diff --git a/src/client/views/collections/CollectionMenu.tsx b/src/client/views/collections/CollectionMenu.tsx
index 615014c4a..570d9c8fb 100644
--- a/src/client/views/collections/CollectionMenu.tsx
+++ b/src/client/views/collections/CollectionMenu.tsx
@@ -172,7 +172,7 @@ export class CollectionMenu extends AntimodeMenu<CollectionMenuProps> {
         //     [<CollectionViewBaseChrome key="chrome"
         //         docView={this.SelectedCollection}
         //         fieldKey={this.SelectedCollection.LayoutFieldKey}
-        //         type={StrCast(this.SelectedCollection?.props.Document._viewType, CollectionViewType.Invalid) as CollectionViewType} />,
+        //         type={StrCast(this.SelectedCollection?.props.Document._type_collection, CollectionViewType.Invalid) as CollectionViewType} />,
         //         prop,
         //         /*button*/]);
     }
@@ -349,7 +349,7 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewMenu
     viewChanged = (e: React.ChangeEvent) => {
         const target = this.document !== Doc.MyLeftSidebarPanel ? this.document : (this.document.proto as Doc);
         //@ts-ignore
-        target._viewType = e.target.selectedOptions[0].value;
+        target._type_collection = e.target.selectedOptions[0].value;
     };
 
     commandChanged = (e: React.ChangeEvent) => {
@@ -440,8 +440,8 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewMenu
                 const c = {
                     params: ['target'],
                     title: vtype,
-                    script: `this.target._viewType = '${StrCast(this.props.type)}'`,
-                    immediate: (source: Doc[]) => (this.document._viewType = Doc.getDocTemplate(source?.[0])),
+                    script: `this.target._type_collection = '${StrCast(this.props.type)}'`,
+                    immediate: (source: Doc[]) => (this.document._type_collection = Doc.getDocTemplate(source?.[0])),
                     initialize: emptyFunction,
                 };
                 DragManager.StartButtonDrag([this._viewRef.current!], c.script, StrCast(c.title), { target: this.document }, c.params, c.initialize, e.clientX, e.clientY);
@@ -639,8 +639,8 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewMenu
                         key="float"
                         style={{
                             backgroundColor: this.props.docView.layoutDoc.z ? '121212' : undefined,
-                            pointerEvents: this.props.docView.props.docViewPath().lastElement()?.rootDoc?._viewType !== CollectionViewType.Freeform ? 'none' : undefined,
-                            color: this.props.docView.props.docViewPath().lastElement()?.rootDoc?._viewType !== CollectionViewType.Freeform ? 'dimgrey' : undefined,
+                            pointerEvents: this.props.docView.props.docViewPath().lastElement()?.rootDoc?._type_collection !== CollectionViewType.Freeform ? 'none' : undefined,
+                            color: this.props.docView.props.docViewPath().lastElement()?.rootDoc?._type_collection !== CollectionViewType.Freeform ? 'dimgrey' : undefined,
                         }}
                         onClick={undoBatch(() => this.props.docView.props.CollectionFreeFormDocumentView?.().float())}>
                         <FontAwesomeIcon icon={['fab', 'buffer']} size={'lg'} />
diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx
index 404ca5d65..e2e352857 100644
--- a/src/client/views/collections/CollectionStackingView.tsx
+++ b/src/client/views/collections/CollectionStackingView.tsx
@@ -36,7 +36,7 @@ export type collectionStackingViewProps = {
     sortFunc?: (a: Doc, b: Doc) => number;
     chromeHidden?: boolean;
     // view type is stacking
-    viewType?: CollectionViewType;
+    type_collection?: CollectionViewType;
     NativeWidth?: () => number;
     NativeHeight?: () => number;
 };
@@ -94,7 +94,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
     }
     // are we stacking or masonry?
     @computed get isStackingView() {
-        return (this.props.viewType ?? this.layoutDoc._viewType) === CollectionViewType.Stacking;
+        return (this.props.type_collection ?? this.layoutDoc._type_collection) === CollectionViewType.Stacking;
     }
     // this is the number of StackingViewFieldColumns that we have
     @computed get numGroupColumns() {
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index 008de1944..bcda13c8b 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -110,7 +110,7 @@ export function CollectionSubView<X>(moreProps?: X) {
                 rawdocs = rootDoc && !this.props.isAnnotationOverlay ? [Doc.GetProto(rootDoc)] : [];
             }
 
-            const childDocs = rawdocs.filter(d => !(d instanceof Promise) && GetEffectiveAcl(Doc.GetProto(d)) !== AclPrivate && (this.props.ignoreUnrendered || !d.unrendered)).map(d => d as Doc);
+            const childDocs = rawdocs.filter(d => !(d instanceof Promise) && GetEffectiveAcl(Doc.GetProto(d)) !== AclPrivate && (this.props.ignoreUnrendered || !d.layout_unrendered)).map(d => d as Doc);
 
             const childDocFilters = this.childDocFilters();
             const docRangeFilters = this.childDocRangeFilters();
@@ -213,7 +213,7 @@ export function CollectionSubView<X>(moreProps?: X) {
                     const movedDocs = docDragData.droppedDocuments.filter((d, i) => docDragData.draggedDocuments[i] === d);
                     const addedDocs = docDragData.droppedDocuments.filter((d, i) => docDragData.draggedDocuments[i] !== d);
                     if (movedDocs.length) {
-                        const canAdd = this.props.Document._viewType === CollectionViewType.Pile || de.embedKey || this.props.Document.allowOverlayDrop || Doc.AreProtosEqual(Cast(movedDocs[0].annotationOn, Doc, null), this.props.Document);
+                        const canAdd = this.props.Document._type_collection === CollectionViewType.Pile || de.embedKey || this.props.Document.allowOverlayDrop || Doc.AreProtosEqual(Cast(movedDocs[0].annotationOn, Doc, null), this.props.Document);
                         added = docDragData.moveDocument(movedDocs, this.props.Document, canAdd ? this.addDocument : returnFalse);
                     } else {
                         ScriptCast(this.props.Document.dropConverter)?.script.run({ dragData: docDragData });
@@ -448,7 +448,7 @@ export function CollectionSubView<X>(moreProps?: X) {
             }
             if (generatedDocuments.length) {
                 // Creating a dash document
-                const isFreeformView = this.props.Document._viewType === CollectionViewType.Freeform;
+                const isFreeformView = this.props.Document._type_collection === CollectionViewType.Freeform;
                 const set = !isFreeformView
                     ? generatedDocuments
                     : generatedDocuments.length > 1
diff --git a/src/client/views/collections/CollectionTimeView.tsx b/src/client/views/collections/CollectionTimeView.tsx
index 6135154c3..49a90d828 100644
--- a/src/client/views/collections/CollectionTimeView.tsx
+++ b/src/client/views/collections/CollectionTimeView.tsx
@@ -47,11 +47,11 @@ export class CollectionTimeView extends CollectionSubView() {
     }
 
     getAnchor = (addAsAnnotation: boolean) => {
-        const anchor = Docs.Create.HTMLAnchorDocument([], {
+        const anchor = Docs.Create.HTMLMarkerDocument([], {
             title: ComputedField.MakeFunction(`"${this.pivotField}"])`) as any,
             annotationOn: this.rootDoc,
         });
-        PresBox.pinDocView(anchor, { pinData: { viewType: true, pivot: true, filters: true } }, this.rootDoc);
+        PresBox.pinDocView(anchor, { pinData: { type_collection: true, pivot: true, filters: true } }, this.rootDoc);
 
         if (addAsAnnotation) {
             // when added as an annotation, links to anchors can be found as links to the document even if the anchors are not rendered
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index dd7bf1740..7913d3188 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -87,7 +87,7 @@ export class CollectionView extends ViewBoxAnnotatableComponent<ViewBoxAnnotatab
     }
 
     get collectionViewType(): CollectionViewType | undefined {
-        const viewField = StrCast(this.layoutDoc._viewType);
+        const viewField = StrCast(this.layoutDoc._type_collection);
         if (CollectionView._safeMode) {
             switch (viewField) {
                 case CollectionViewType.Freeform:
@@ -127,8 +127,8 @@ export class CollectionView extends ViewBoxAnnotatableComponent<ViewBoxAnnotatab
         }
     };
 
-    setupViewTypes(category: string, func: (viewType: CollectionViewType) => Doc) {
-        if (!Doc.IsSystem(this.rootDoc) && this.rootDoc._viewType !== CollectionViewType.Docking && !this.rootDoc.isGroup && !this.rootDoc.annotationOn) {
+    setupViewTypes(category: string, func: (type_collection: CollectionViewType) => Doc) {
+        if (!Doc.IsSystem(this.rootDoc) && this.rootDoc._type_collection !== CollectionViewType.Docking && !this.rootDoc.isGroup && !this.rootDoc.annotationOn) {
             // prettier-ignore
             const subItems: ContextMenuProps[] = [
                 { description: 'Freeform',    event: () => func(CollectionViewType.Freeform), icon: 'signature' },
@@ -160,7 +160,7 @@ export class CollectionView extends ViewBoxAnnotatableComponent<ViewBoxAnnotatab
             !Doc.noviceMode &&
                 this.setupViewTypes('UI Controls...', vtype => {
                     const newRendition = Doc.MakeEmbedding(this.rootDoc);
-                    newRendition._viewType = vtype;
+                    newRendition._type_collection = vtype;
                     this.props.addDocTab(newRendition, OpenWhere.addRight);
                     return newRendition;
                 });
@@ -244,7 +244,7 @@ export class CollectionView extends ViewBoxAnnotatableComponent<ViewBoxAnnotatab
             <div
                 className="collectionView"
                 onContextMenu={this.onContextMenu}
-                style={{ pointerEvents: this.props.DocumentView?.()?.props.docViewPath().lastElement()?.rootDoc?._viewType === CollectionViewType.Freeform && this.rootDoc._lockedPosition ? 'none' : undefined }}>
+                style={{ pointerEvents: this.props.DocumentView?.()?.props.docViewPath().lastElement()?.rootDoc?._type_collection === CollectionViewType.Freeform && this.rootDoc._lockedPosition ? 'none' : undefined }}>
                 {this.renderSubView(this.collectionViewType, props)}
             </div>
         );
diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx
index 69963736b..7e88959a4 100644
--- a/src/client/views/collections/TabDocView.tsx
+++ b/src/client/views/collections/TabDocView.tsx
@@ -349,7 +349,7 @@ export class TabDocView extends React.Component<TabDocViewProps> {
     //  lightbox - will add the document to any collection along the path from the document to the docking view that has a field isLightbox. if none is found, it adds to the full screen lightbox
     addDocTab = (doc: Doc, location: OpenWhere) => {
         SelectionManager.DeselectAll();
-        const whereFields = doc._viewType === CollectionViewType.Docking ? [OpenWhere.dashboard] : location.split(':');
+        const whereFields = doc._type_collection === CollectionViewType.Docking ? [OpenWhere.dashboard] : location.split(':');
         const keyValue = whereFields[1]?.includes('KeyValue');
         const whereMods: OpenWhereMod = whereFields.length > 1 ? (whereFields[1].replace('KeyValue', '') as OpenWhereMod) : OpenWhereMod.none;
         if (doc.dockingConfig) return DashboardView.openDashboard(doc);
@@ -383,7 +383,7 @@ export class TabDocView extends React.Component<TabDocViewProps> {
     };
 
     getCurrentFrame = () => {
-        return NumCast(Cast(PresBox.Instance.childDocs[PresBox.Instance.itemIndex].presentationTargetDoc, Doc, null)._currentFrame);
+        return NumCast(Cast(PresBox.Instance.activeItem.presentationTargetDoc, Doc, null)._currentFrame);
     };
     static Activate = (tabDoc: Doc) => {
         const tab = Array.from(CollectionDockingView.Instance?.tabMap!).find(tab => tab.DashDoc === tabDoc);
@@ -408,7 +408,7 @@ export class TabDocView extends React.Component<TabDocViewProps> {
     PanelHeight = () => this._panelHeight;
     miniMapColor = () => this.tabColor;
     tabView = () => this._view;
-    disableMinimap = () => !this._document || this._document.layout !== CollectionView.LayoutString(Doc.LayoutFieldKey(this._document)) || this._document?._viewType !== CollectionViewType.Freeform;
+    disableMinimap = () => !this._document || this._document.layout !== CollectionView.LayoutString(Doc.LayoutFieldKey(this._document)) || this._document?._type_collection !== CollectionViewType.Freeform;
     hideMinimap = () => this.disableMinimap() || BoolCast(this._document?.layout_hideMinimap);
 
     @computed get docView() {
@@ -453,7 +453,7 @@ export class TabDocView extends React.Component<TabDocViewProps> {
                     <div
                         className="miniMap-hidden"
                         style={{
-                            display: this.disableMinimap() || this._document._viewType !== 'freeform' ? 'none' : undefined,
+                            display: this.disableMinimap() || this._document._type_collection !== 'freeform' ? 'none' : undefined,
                             color: this._document.layout_hideMinimap ? Colors.BLACK : Colors.WHITE,
                             backgroundColor: this._document.layout_hideMinimap ? Colors.LIGHT_GRAY : Colors.MEDIUM_BLUE,
                             boxShadow: this._document.layout_hideMinimap ? Shadows.STANDARD_SHADOW : undefined,
diff --git a/src/client/views/collections/TreeView.tsx b/src/client/views/collections/TreeView.tsx
index 92b70cb5a..037148bb9 100644
--- a/src/client/views/collections/TreeView.tsx
+++ b/src/client/views/collections/TreeView.tsx
@@ -118,7 +118,7 @@ export class TreeView extends React.Component<TreeViewProps> {
         return 'TreeView(' + this.props.document.title + ')';
     } // this makes mobx trace() statements more descriptive
     get defaultExpandedView() {
-        return this.doc.viewType === CollectionViewType.Docking
+        return this.doc.type_collection === CollectionViewType.Docking
             ? this.fieldKey
             : this.props.treeView.dashboardMode
             ? this.fieldKey
@@ -312,7 +312,7 @@ export class TreeView extends React.Component<TreeViewProps> {
             title: '-title-',
             treeViewExpandedViewLock: true,
             treeViewExpandedView: 'data',
-            _viewType: CollectionViewType.Tree,
+            _type_collection: CollectionViewType.Tree,
             layout_hideLinkButton: true,
             _layout_showSidebar: true,
             _layout_fitWidth: true,
@@ -713,7 +713,7 @@ export class TreeView extends React.Component<TreeViewProps> {
         const data = () => (this.childDocs || this.props.treeView.dashboardMode ? this.fieldKey : '');
         const embeddings = () => (this.props.treeView.dashboardMode ? '' : 'embeddings');
         const fields = () => (Doc.noviceMode ? '' : 'fields');
-        const layout = Doc.noviceMode || this.doc.viewType === CollectionViewType.Docking ? [] : ['layout'];
+        const layout = Doc.noviceMode || this.doc.type_collection === CollectionViewType.Docking ? [] : ['layout'];
         return [data(), ...layout, ...(this.props.treeView.fileSysMode ? [embeddings(), links(), annos()] : []), fields()].filter(m => m);
     }
     @action
@@ -771,7 +771,7 @@ export class TreeView extends React.Component<TreeViewProps> {
                 ? []
                 : this.props.treeView.fileSysMode && this.doc === Doc.GetProto(this.doc)
                 ? [openEmbedding, makeFolder]
-                : this.doc.viewType === CollectionViewType.Docking
+                : this.doc.type_collection === CollectionViewType.Docking
                 ? []
                 : [deleteItem, openEmbedding, focusDoc]),
         ];
@@ -1030,7 +1030,7 @@ export class TreeView extends React.Component<TreeViewProps> {
 
     // renders the text version of a document as the header.  This is used in the file system mode and in other vanilla tree views.
     @computed get renderTitleAsHeader() {
-        return this.props.treeView.Document.treeViewHideUnrendered && this.doc.unrendered && !this.doc.treeViewFieldKey ? (
+        return this.props.treeView.Document.treeViewHideUnrendered && this.doc.layout_unrendered && !this.doc.treeViewFieldKey ? (
             <div></div>
         ) : (
             <>
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 4d18ebeea..5ac444147 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -1147,7 +1147,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     @action
     nudge = (x: number, y: number, nudgeTime: number = 500) => {
         const collectionDoc = this.props.docViewPath().lastElement().rootDoc;
-        if (collectionDoc?._viewType !== CollectionViewType.Freeform || collectionDoc._freeform_ !== undefined) {
+        if (collectionDoc?._type_collection !== CollectionViewType.Freeform || collectionDoc._freeform_ !== undefined) {
             this.setPan(
                 NumCast(this.layoutDoc[this.panXFieldKey]) + ((this.props.PanelWidth() / 2) * x) / this.zoomScaling(), // nudge x,y as a function of panel dimension and scale
                 NumCast(this.layoutDoc[this.panYFieldKey]) + ((this.props.PanelHeight() / 2) * -y) / this.zoomScaling(),
@@ -1541,8 +1541,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
 
     getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => {
         // create an anchor that saves information about the current state of the freeform view (pan, zoom, view type)
-        const anchor = Docs.Create.CollectionAnchorDocument({ title: 'ViewSpec - ' + StrCast(this.layoutDoc._viewType), unrendered: true, presTransition: 500, annotationOn: this.rootDoc });
-        PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), pannable: !this.Document._isGroup, viewType: true, filters: true } }, this.rootDoc);
+        const anchor = Docs.Create.CollectionConfigDocument({ title: 'ViewSpec - ' + StrCast(this.layoutDoc._type_collection), layout_unrendered: true, presTransition: 500, annotationOn: this.rootDoc });
+        PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), pannable: !this.Document._isGroup, type_collection: true, filters: true } }, this.rootDoc);
 
         if (addAsAnnotation) {
             if (Cast(this.dataDoc[this.props.fieldKey + '_annotations'], listSpec(Doc), null) !== undefined) {
@@ -1851,10 +1851,10 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
 
     incrementalRender = action(() => {
         if (!LightboxView.LightboxDoc || LightboxView.IsLightboxDocView(this.props.docViewPath())) {
-            const unrendered = this.childDocs.filter(doc => !this._renderCutoffData.get(doc[Id]));
+            const layout_unrendered = this.childDocs.filter(doc => !this._renderCutoffData.get(doc[Id]));
             const loadIncrement = 5;
-            for (var i = 0; i < Math.min(unrendered.length, loadIncrement); i++) {
-                this._renderCutoffData.set(unrendered[i][Id] + '', true);
+            for (var i = 0; i < Math.min(layout_unrendered.length, loadIncrement); i++) {
+                this._renderCutoffData.set(layout_unrendered[i][Id] + '', true);
             }
         }
         this.childDocs.some(doc => !this._renderCutoffData.get(doc[Id])) && setTimeout(this.incrementalRender, 1);
@@ -1991,12 +1991,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                 onDragOver={e => e.preventDefault()}
                 onContextMenu={this.onContextMenu}
                 style={{
-                    pointerEvents:
-                        this.props.Document.type === DocumentType.MARKER
-                            ? 'none' // bcz: ugh.. this is here to prevent markers, which render as freeform views, from grabbing events -- need a better approach.
-                            : SnappingManager.GetIsDragging() && this.childDocs.includes(DragManager.docsBeingDragged.lastElement())
-                            ? 'all'
-                            : (this.props.pointerEvents?.() as any),
+                    pointerEvents: SnappingManager.GetIsDragging() && this.childDocs.includes(DragManager.docsBeingDragged.lastElement()) ? 'all' : (this.props.pointerEvents?.() as any),
                     textAlign: this.isAnnotationOverlay ? 'initial' : undefined,
                     transform: `scale(${this.nativeDimScaling || 1})`,
                     width: `${100 / (this.nativeDimScaling || 1)}%`,
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index fdff13b54..47d7801e6 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -432,8 +432,8 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
         }
 
         const newCollection = this.getCollection(selected, (e as KeyboardEvent)?.key === 't' ? Docs.Create.StackingDocument : undefined, group);
-        newCollection._panX = this.Bounds.left + this.Bounds.width / 2;
-        newCollection._panY = this.Bounds.top + this.Bounds.height / 2;
+        newCollection._freeform_panX = this.Bounds.left + this.Bounds.width / 2;
+        newCollection._freeform_panY = this.Bounds.top + this.Bounds.height / 2;
         newCollection._currentFrame = activeFrame;
         this.props.addDocument?.(newCollection);
         this.props.selectDocuments([newCollection]);
diff --git a/src/client/views/collections/collectionLinear/CollectionLinearView.tsx b/src/client/views/collections/collectionLinear/CollectionLinearView.tsx
index efd73a927..9d82e3198 100644
--- a/src/client/views/collections/collectionLinear/CollectionLinearView.tsx
+++ b/src/client/views/collections/collectionLinear/CollectionLinearView.tsx
@@ -165,7 +165,7 @@ export class CollectionLinearView extends CollectionSubView() {
         if (doc.icon === 'linkui') return this.getLinkUI();
         if (doc.icon === 'currentlyplayingui') return this.getCurrentlyPlayingUI();
 
-        const nested = doc._viewType === CollectionViewType.Linear;
+        const nested = doc._type_collection === CollectionViewType.Linear;
         const hidden = doc.hidden === true;
 
         let dref: Opt<HTMLDivElement>;
diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx
index 3be2d9a77..3abe7a331 100644
--- a/src/client/views/nodes/ComparisonBox.tsx
+++ b/src/client/views/nodes/ComparisonBox.tsx
@@ -86,7 +86,12 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl
     };
 
     getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => {
-        const anchor = Docs.Create.ImageanchorDocument({ title: 'ImgAnchor:' + this.rootDoc.title, presTransition: 1000, unrendered: true, annotationOn: this.rootDoc });
+        const anchor = Docs.Create.ComparisonConfigDocument({
+            title: 'ImgAnchor:' + this.rootDoc.title,
+            // set presentation timing properties for restoring view
+            presTransition: 1000,
+            annotationOn: this.rootDoc,
+        });
         if (anchor) {
             if (!addAsAnnotation) anchor.backgroundColor = 'transparent';
             /* addAsAnnotation &&*/ this.addDocument(anchor);
diff --git a/src/client/views/nodes/DataVizBox/DataVizBox.tsx b/src/client/views/nodes/DataVizBox/DataVizBox.tsx
index 5876efb01..baa45e278 100644
--- a/src/client/views/nodes/DataVizBox/DataVizBox.tsx
+++ b/src/client/views/nodes/DataVizBox/DataVizBox.tsx
@@ -73,8 +73,7 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
     getAnchor = (addAsAnnotation?: boolean, pinProps?: PinProps) => {
         const anchor =
             this._chartRenderer?.getAnchor(pinProps) ??
-            Docs.Create.TextanchorDocument({
-                unrendered: true,
+            Docs.Create.DataVizConfigDocument({
                 // when we clear selection -> we should have it so chartBox getAnchor returns undefined
                 // this is for when we want the whole doc (so when the chartBox getAnchor returns without a marker)
                 /*put in some options*/
diff --git a/src/client/views/nodes/DataVizBox/components/LineChart.tsx b/src/client/views/nodes/DataVizBox/components/LineChart.tsx
index 11f62a61f..661061d51 100644
--- a/src/client/views/nodes/DataVizBox/components/LineChart.tsx
+++ b/src/client/views/nodes/DataVizBox/components/LineChart.tsx
@@ -163,7 +163,10 @@ export class LineChart extends React.Component<LineChartProps> {
 
     // create a document anchor that stores whatever is needed to reconstruct the viewing state (selection,zoom,etc)
     getAnchor = (pinProps?: PinProps) => {
-        const anchor = Docs.Create.TextanchorDocument({ title: 'line doc selection' + this._currSelected?.x, unrendered: true });
+        const anchor = Docs.Create.LineChartConfigDocument({
+            //
+            title: 'line doc selection' + this._currSelected?.x,
+        });
         PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: pinProps?.pinData }, this.props.dataDoc);
         anchor.presDataVizSelection = this._currSelected ? new List<number>([this._currSelected.x, this._currSelected.y]) : undefined;
         return anchor;
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index d8494e58e..359b72352 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -402,7 +402,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
     defaultRestoreTargetView = (docView: DocumentView, anchor: Doc, focusSpeed: number, options: DocFocusOptions) => {
         const targetMatch =
             Doc.AreProtosEqual(anchor, this.rootDoc) || // anchor is this document, so anchor's properties apply to this document
-            (DocCast(anchor)?.unrendered && Doc.AreProtosEqual(DocCast(anchor.annotationOn), this.rootDoc)) // the anchor is an unrendered annotation on this document, so anchor properties apply to this document
+            (DocCast(anchor)?.layout_unrendered && Doc.AreProtosEqual(DocCast(anchor.annotationOn), this.rootDoc)) // the anchor is an layout_unrendered annotation on this document, so anchor properties apply to this document
                 ? true
                 : false;
         return targetMatch && PresBox.restoreTargetDocView(docView, anchor, focusSpeed) ? focusSpeed : undefined;
@@ -522,7 +522,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
             ) {
                 e.stopPropagation();
                 // don't preventDefault anymore.  Goldenlayout, PDF text selection and RTF text selection all need it to go though
-                //if (this.props.isSelected(true) && this.rootDoc.type !== DocumentType.PDF && this.layoutDoc._viewType !== CollectionViewType.Docking) e.preventDefault();
+                //if (this.props.isSelected(true) && this.rootDoc.type !== DocumentType.PDF && this.layoutDoc._type_collection !== CollectionViewType.Docking) e.preventDefault();
 
                 // listen to move events if document content isn't active or document is draggable
                 if (!this.layoutDoc._lockedPosition && (!this.isContentActive() || this.props.enableDragWhenActive || this.rootDoc.enableDragWhenActive)) {
@@ -705,7 +705,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
             !Doc.noviceMode && templateDoc && appearanceItems.push({ description: 'Open Template   ', event: () => this.props.addDocTab(templateDoc, OpenWhere.addRight), icon: 'eye' });
             !appearance && appearanceItems.length && cm.addItem({ description: 'UI Controls...', subitems: appearanceItems, icon: 'compass' });
 
-            if (!Doc.IsSystem(this.rootDoc) && this.rootDoc.type !== DocumentType.PRES && ![CollectionViewType.Docking, CollectionViewType.Tree].includes(this.rootDoc._viewType as any)) {
+            if (!Doc.IsSystem(this.rootDoc) && this.rootDoc.type !== DocumentType.PRES && ![CollectionViewType.Docking, CollectionViewType.Tree].includes(this.rootDoc._type_collection as any)) {
                 const existingOnClick = cm.findByDescription('OnClick...');
                 const onClicks: ContextMenuProps[] = existingOnClick && 'subitems' in existingOnClick ? existingOnClick.subitems : [];
 
@@ -771,7 +771,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
         if (!Doc.IsSystem(this.rootDoc)) {
             constantItems.push({ description: 'Export as Zip file', icon: 'download', event: async () => Doc.Zip(this.props.Document) });
             constantItems.push({ description: 'Import Zipped file', icon: 'upload', event: ({ x, y }) => this.importDocument() });
-            (this.rootDoc._viewType !== CollectionViewType.Docking || !Doc.noviceMode) && constantItems.push({ description: 'Share', event: () => SharingManager.Instance.open(this.props.DocumentView()), icon: 'users' });
+            (this.rootDoc._type_collection !== CollectionViewType.Docking || !Doc.noviceMode) && constantItems.push({ description: 'Share', event: () => SharingManager.Instance.open(this.props.DocumentView()), icon: 'users' });
             if (this.props.removeDocument && Doc.ActiveDashboard !== this.props.Document) {
                 // need option to gray out menu items ... preferably with a '?' that explains why they're grayed out (eg., no permissions)
                 constantItems.push({ description: 'Close', event: this.deleteClicked, icon: 'times' });
@@ -863,7 +863,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
             LightboxView.LightboxDoc !== this.rootDoc &&
             this.thumb &&
             !Doc.AreProtosEqual(DocumentLinksButton.StartLink, this.rootDoc) &&
-            ((!childHighlighted() && !childOverlayed() && !Doc.isBrushedHighlightedDegree(this.rootDoc)) || this.rootDoc._viewType === CollectionViewType.Docking) &&
+            ((!childHighlighted() && !childOverlayed() && !Doc.isBrushedHighlightedDegree(this.rootDoc)) || this.rootDoc._type_collection === CollectionViewType.Docking) &&
             !this._componentView?.isAnyChildContentActive?.()
             ? true
             : false;
@@ -929,7 +929,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
         return this.props.styleProvider?.(doc, props, property);
     };
     // We need to use allrelatedLinks to get not just links to the document as a whole, but links to
-    // anchors that are not rendered as DocumentViews (marked as 'unrendered' with their 'annotationOn' set to this document).  e.g.,
+    // anchors that are not rendered as DocumentViews (marked as 'layout_unrendered' with their 'annotationOn' set to this document).  e.g.,
     //     - PDF text regions are rendered as an Annotations without generating a DocumentView, '
     //     - RTF selections are rendered via Prosemirror and have a mark which contains the Document ID for the annotation link
     //     - and links to PDF/Web docs at a certain scroll location never create an explicit view.
@@ -940,8 +940,8 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
             link =>
                 Doc.AreProtosEqual(link.link_anchor_1 as Doc, this.rootDoc) ||
                 Doc.AreProtosEqual(link.link_anchor_2 as Doc, this.rootDoc) ||
-                ((link.link_anchor_1 as Doc)?.unrendered && Doc.AreProtosEqual((link.link_anchor_1 as Doc)?.annotationOn as Doc, this.rootDoc)) ||
-                ((link.link_anchor_2 as Doc)?.unrendered && Doc.AreProtosEqual((link.link_anchor_2 as Doc)?.annotationOn as Doc, this.rootDoc))
+                ((link.link_anchor_1 as Doc)?.layout_unrendered && Doc.AreProtosEqual((link.link_anchor_1 as Doc)?.annotationOn as Doc, this.rootDoc)) ||
+                ((link.link_anchor_2 as Doc)?.layout_unrendered && Doc.AreProtosEqual((link.link_anchor_2 as Doc)?.annotationOn as Doc, this.rootDoc))
         );
     }
     @computed get allLinks() {
@@ -952,7 +952,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
     @computed get allLinkEndpoints() {
         // the small blue dots that mark the endpoints of links
         TraceMobx();
-        if (this.props.hideLinkAnchors || this.layoutDoc.layout_hideLinkAnchors || this.props.dontRegisterView || this.layoutDoc.unrendered) return null;
+        if (this.props.hideLinkAnchors || this.layoutDoc.layout_hideLinkAnchors || this.props.dontRegisterView || this.layoutDoc.layout_unrendered) return null;
         const filtered = DocUtils.FilterDocs(this.directLinks, this.props.docFilters?.() ?? [], []).filter(d => d.layout_linkDisplay);
         return filtered.map(link => (
             <div className="documentView-anchorCont" key={link[Id]}>
@@ -1345,7 +1345,7 @@ export class DocumentView extends React.Component<DocumentViewProps> {
         return this.docView?._componentView?.reverseNativeScaling?.() ? 0 : returnVal(this.props.NativeHeight?.(), Doc.NativeHeight(this.layoutDoc, this.props.DataDoc, !this.layout_fitWidth));
     }
     @computed get shouldNotScale() {
-        return (this.layout_fitWidth && !this.nativeWidth) || [CollectionViewType.Docking].includes(this.Document._viewType as any);
+        return (this.layout_fitWidth && !this.nativeWidth) || [CollectionViewType.Docking].includes(this.Document._type_collection as any);
     }
     @computed get effectiveNativeWidth() {
         return this.shouldNotScale ? 0 : this.nativeWidth || NumCast(this.layoutDoc.width);
diff --git a/src/client/views/nodes/FunctionPlotBox.tsx b/src/client/views/nodes/FunctionPlotBox.tsx
index db72209ab..1a78583f9 100644
--- a/src/client/views/nodes/FunctionPlotBox.tsx
+++ b/src/client/views/nodes/FunctionPlotBox.tsx
@@ -43,7 +43,10 @@ export class FunctionPlotBox extends ViewBoxAnnotatableComponent<FieldViewProps>
         );
     }
     getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => {
-        const anchor = Docs.Create.TextanchorDocument({ annotationOn: this.rootDoc, unrendered: true });
+        const anchor = Docs.Create.FunctionPlotConfigDocument({
+            //
+            annotationOn: this.rootDoc,
+        });
         PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), datarange: true } }, this.rootDoc);
         anchor.presXRange = new List<number>(Array.from(this._plot.options.xAxis.domain));
         anchor.presYRange = new List<number>(Array.from(this._plot.options.yAxis.domain));
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 7d1d50cc7..068d39391 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -77,13 +77,12 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
     getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => {
         const anchor =
             this._getAnchor?.(this._savedAnnotations, false) ?? // use marquee anchor, otherwise, save zoom/pan as anchor
-            Docs.Create.ImageanchorDocument({
+            Docs.Create.ImageConfigDocument({
                 title: 'ImgAnchor:' + this.rootDoc.title,
                 presPanX: NumCast(this.layoutDoc._freeform_panX),
                 presPanY: NumCast(this.layoutDoc._freeform_panY),
                 presViewScale: Cast(this.layoutDoc._freeform_scale, 'number', null),
                 presTransition: 1000,
-                unrendered: true,
                 annotationOn: this.rootDoc,
             });
         if (anchor) {
diff --git a/src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx b/src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx
index bc57148dc..6b26f494b 100644
--- a/src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx
+++ b/src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx
@@ -74,7 +74,7 @@ export class MapBoxInfoWindow extends React.Component<MapBoxInfoWindowProps & Vi
                             removeDocument={this.removeDoc}
                             addDocument={this.addDoc}
                             renderDepth={this.props.renderDepth + 1}
-                            viewType={CollectionViewType.Stacking}
+                            type_collection={CollectionViewType.Stacking}
                             pointerEvents={returnAll}
                         />
                     </div>
diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx
index edeaba322..95fbb274d 100644
--- a/src/client/views/nodes/PDFBox.tsx
+++ b/src/client/views/nodes/PDFBox.tsx
@@ -238,9 +238,8 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
             ele.append(this._pdfViewer.selectionContent()!);
         }
         const docAnchor = () => {
-            const anchor = Docs.Create.TextanchorDocument({
+            const anchor = Docs.Create.PdfConfigDocument({
                 title: StrCast(this.rootDoc.title + '@' + NumCast(this.layoutDoc._layout_scrollTop)?.toFixed(0)),
-                unrendered: true,
                 annotationOn: this.rootDoc,
             });
             return anchor;
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index e00cb8618..3cfc3d696 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -1083,6 +1083,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
                             ScreenToLocalTransform={this.screenToLocalTransform}
                             docFilters={this.timelineDocFilter}
                             select={emptyFunction}
+                            focus={emptyFunction}
                             NativeDimScaling={returnOne}
                             whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
                             removeDocument={this.removeDocument}
diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx
index 3da5d8f17..437079ca4 100644
--- a/src/client/views/nodes/WebBox.tsx
+++ b/src/client/views/nodes/WebBox.tsx
@@ -296,8 +296,10 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
 
     @action
     getView = (doc: Doc) => {
+        if (Doc.AreProtosEqual(doc, this.rootDoc)) return new Promise<Opt<DocumentView>>(res => res(this.props.DocumentView?.()));
         if (this.rootDoc.layout_fieldKey === 'layout_icon') this.props.DocumentView?.().iconify();
-        if (this._url && WebCast(doc.presData).url.href !== this._url) this.setData(WebCast(doc.presData).url.href);
+        const webUrl = WebCast(doc.presData)?.url;
+        if (this._url && webUrl && webUrl.href !== this._url) this.setData(webUrl.href);
         if (this._sidebarRef?.current?.makeDocUnfiltered(doc) && !this.SidebarShown) this.toggleSidebar(false);
         return new Promise<Opt<DocumentView>>(res => DocumentManager.Instance.AddViewRenderedCb(doc, dv => res(dv)));
     };
@@ -320,10 +322,9 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
         } catch (e) {}
         const anchor =
             this._getAnchor(this._savedAnnotations, false) ??
-            Docs.Create.WebanchorDocument({
+            Docs.Create.WebConfigDocument({
                 title: StrCast(this.rootDoc.title + ' ' + this.layoutDoc._layout_scrollTop),
                 y: NumCast(this.layoutDoc._layout_scrollTop),
-                unrendered: true,
                 annotationOn: this.rootDoc,
             });
         PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), scrollable: pinProps?.pinData ? true : false, pannable: true } }, this.rootDoc);
@@ -924,7 +925,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
                 whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
                 removeDocument={this.removeDocument}
                 moveDocument={this.moveDocument}
-                addDocument={this.addDocument}
+                addDocument={this.addDocumentWrapper}
                 childPointerEvents={this.props.isContentActive() ? 'all' : undefined}
                 pointerEvents={this.annotationPointerEvents}
             />
@@ -996,7 +997,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
     opaqueFilter = () => [...this.props.docFilters(), Utils.noDragsDocFilter, ...(DragManager.docsBeingDragged.length ? [] : [Utils.IsOpaqueFilter()])];
     childStyleProvider = (doc: Doc | undefined, props: Opt<DocumentViewProps>, property: string): any => {
         if (doc instanceof Doc && property === StyleProp.PointerEvents) {
-            if (doc.textInlineAnnotations) return 'none';
+            if (this.inlineTextAnnotations.includes(doc)) return 'none';
         }
         return this.props.styleProvider?.(doc, props, property);
     };
diff --git a/src/client/views/nodes/button/FontIconBox.tsx b/src/client/views/nodes/button/FontIconBox.tsx
index 5e615f2c1..57aa852ac 100644
--- a/src/client/views/nodes/button/FontIconBox.tsx
+++ b/src/client/views/nodes/button/FontIconBox.tsx
@@ -299,7 +299,7 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
                 const selected = SelectionManager.Docs().lastElement();
                 if (selected) {
                     if (StrCast(selected.type) === DocumentType.COL) {
-                        text = StrCast(selected._viewType);
+                        text = StrCast(selected._type_collection);
                     } else {
                         dropdown = false;
                         text = selected.type === DocumentType.RTF ? 'Text' : StrCast(selected.type);
@@ -579,7 +579,7 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
 // toggle: Set overlay status of selected document
 ScriptingGlobals.add(function setView(view: string) {
     const selected = SelectionManager.Docs().lastElement();
-    selected ? (selected._viewType = view) : console.log('[FontIconBox.tsx] changeView failed');
+    selected ? (selected._type_collection = view) : console.log('[FontIconBox.tsx] changeView failed');
 });
 
 // toggle: Set overlay status of selected document
diff --git a/src/client/views/nodes/formattedText/DashFieldView.tsx b/src/client/views/nodes/formattedText/DashFieldView.tsx
index 6c61f6709..2642bc144 100644
--- a/src/client/views/nodes/formattedText/DashFieldView.tsx
+++ b/src/client/views/nodes/formattedText/DashFieldView.tsx
@@ -144,7 +144,7 @@ export class DashFieldViewInternal extends React.Component<IDashFieldViewInterna
         let container = this.props.tbox.props.DocumentView?.().props.docViewPath().lastElement();
         if (container) {
             const embedding = Doc.MakeEmbedding(container.rootDoc);
-            embedding._viewType = CollectionViewType.Time;
+            embedding._type_collection = CollectionViewType.Time;
             const colHdrKey = '_' + container.LayoutFieldKey + '_columnHeaders';
             let list = Cast(embedding[colHdrKey], listSpec(SchemaHeaderField));
             if (!list) {
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 023814a9d..9c06aa7d8 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -243,7 +243,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
 
     getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => {
         if (!pinProps && this._editorView?.state.selection.empty) return this.rootDoc;
-        const anchor = Docs.Create.TextanchorDocument({ annotationOn: this.rootDoc, unrendered: true });
+        const anchor = Docs.Create.TextConfigDocument({ annotationOn: this.rootDoc });
         this.addDocument(anchor);
         this.makeLinkAnchor(anchor, OpenWhere.addRight, undefined, 'Anchored Selection', false, addAsAnnotation);
         PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), scrollable: true } }, this.rootDoc);
@@ -960,7 +960,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
         if (this._editorView && this._recordingStart) {
             if (this._break) {
                 const textanchorFunc = () => {
-                    const tanch = Docs.Create.TextanchorDocument({ title: 'dictation anchor', unrendered: true });
+                    const tanch = Docs.Create.TextConfigDocument({ title: 'dictation anchor' });
                     return this.addDocument(tanch) ? tanch : undefined;
                 };
                 const link = DocUtils.MakeLinkToActiveAudio(textanchorFunc, false).lastElement();
@@ -998,7 +998,13 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
             const splitter = state.schema.marks.splitter.create({ id: Utils.GenerateGuid() });
             let tr = state.tr.addMark(sel.from, sel.to, splitter);
             if (sel.from !== sel.to) {
-                const anchor = anchorDoc ?? Docs.Create.TextanchorDocument({ title: '#' + this._editorView?.state.doc.textBetween(sel.from, sel.to), annotationOn: this.dataDoc, unrendered: true });
+                const anchor =
+                    anchorDoc ??
+                    Docs.Create.TextConfigDocument({
+                        //
+                        title: '#' + this._editorView?.state.doc.textBetween(sel.from, sel.to),
+                        annotationOn: this.dataDoc,
+                    });
                 const href = targetHref ?? Doc.localServerPath(anchor);
                 if (anchor !== anchorDoc && addAsAnnotation) this.addDocument(anchor);
                 tr.doc.nodesBetween(sel.from, sel.to, (node: any, pos: number, parent: any) => {
@@ -1022,7 +1028,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
     }
 
     getView = async (doc: Doc) => {
-        if (DocListCast(this.rootDoc[this.SidebarKey]).find(anno => Doc.AreProtosEqual(doc.unrendered ? DocCast(doc.annotationOn) : doc, anno))) {
+        if (DocListCast(this.rootDoc[this.SidebarKey]).find(anno => Doc.AreProtosEqual(doc.layout_unrendered ? DocCast(doc.annotationOn) : doc, anno))) {
             !this.SidebarShown && this.toggleSidebar(false);
             setTimeout(() => this._sidebarRef?.current?.makeDocUnfiltered(doc));
         }
diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx
index 858d83b7a..3d1d11141 100644
--- a/src/client/views/nodes/trails/PresBox.tsx
+++ b/src/client/views/nodes/trails/PresBox.tsx
@@ -41,7 +41,7 @@ export interface pinDataTypes {
     scrollable?: boolean;
     dataviz?: number[];
     pannable?: boolean;
-    viewType?: boolean;
+    type_collection?: boolean;
     inkable?: boolean;
     filters?: boolean;
     pivot?: boolean;
@@ -103,10 +103,10 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
     @observable _presKeyEvents: boolean = false;
     @observable _forceKeyEvents: boolean = false;
     @computed get isTreeOrStack() {
-        return [CollectionViewType.Tree, CollectionViewType.Stacking].includes(StrCast(this.layoutDoc._viewType) as any);
+        return [CollectionViewType.Tree, CollectionViewType.Stacking].includes(StrCast(this.layoutDoc._type_collection) as any);
     }
     @computed get isTree() {
-        return this.layoutDoc._viewType === CollectionViewType.Tree;
+        return this.layoutDoc._type_collection === CollectionViewType.Tree;
     }
     @computed get presFieldKey() {
         return StrCast(this.layoutDoc.presFieldKey, 'data');
@@ -128,14 +128,14 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
     }
     public static targetRenderedDoc = (doc: Doc) => {
         const targetDoc = Cast(doc?.presentationTargetDoc, Doc, null);
-        return targetDoc?.unrendered ? DocCast(targetDoc.annotationOn) : targetDoc;
+        return targetDoc?.layout_unrendered ? DocCast(targetDoc.annotationOn) : targetDoc;
     };
     @computed get scrollable() {
-        if ([DocumentType.PDF, DocumentType.WEB, DocumentType.RTF].includes(this.targetDoc.type as DocumentType) || this.targetDoc._viewType === CollectionViewType.Stacking) return true;
+        if ([DocumentType.PDF, DocumentType.WEB, DocumentType.RTF].includes(this.targetDoc.type as DocumentType) || this.targetDoc._type_collection === CollectionViewType.Stacking) return true;
         return false;
     }
     @computed get panable() {
-        if ((this.targetDoc.type === DocumentType.COL && this.targetDoc._viewType === CollectionViewType.Freeform) || this.targetDoc.type === DocumentType.IMG) return true;
+        if ((this.targetDoc.type === DocumentType.COL && this.targetDoc._type_collection === CollectionViewType.Freeform) || this.targetDoc.type === DocumentType.IMG) return true;
         return false;
     }
     @computed get selectedDocumentView() {
@@ -274,7 +274,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         const targetList = PresBox.targetRenderedDoc(doc);
         if (doc.presIndexed !== undefined && targetList) {
             const listItems = (Cast(targetList[Doc.LayoutFieldKey(targetList)], listSpec(Doc), null)?.filter(d => d instanceof Doc) as Doc[]) ?? DocListCast(targetList[Doc.LayoutFieldKey(targetList) + '_annotations']);
-            return listItems.filter(doc => !doc.unrendered);
+            return listItems.filter(doc => !doc.layout_unrendered);
         }
     };
     // Called when the user activates 'next' - to move to the next part of the pres. trail
@@ -376,25 +376,25 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
     static pinDataTypes(target?: Doc): pinDataTypes {
         const targetType = target?.type as any;
         const inkable = [DocumentType.INK].includes(targetType);
-        const scrollable = [DocumentType.PDF, DocumentType.RTF, DocumentType.WEB].includes(targetType) || target?._viewType === CollectionViewType.Stacking;
-        const pannable = [DocumentType.IMG, DocumentType.PDF].includes(targetType) || (targetType === DocumentType.COL && target?._viewType === CollectionViewType.Freeform);
+        const scrollable = [DocumentType.PDF, DocumentType.RTF, DocumentType.WEB].includes(targetType) || target?._type_collection === CollectionViewType.Stacking;
+        const pannable = [DocumentType.IMG, DocumentType.PDF].includes(targetType) || (targetType === DocumentType.COL && target?._type_collection === CollectionViewType.Freeform);
         const temporal = [DocumentType.AUDIO, DocumentType.VID].includes(targetType);
         const clippable = [DocumentType.COMPARISON].includes(targetType);
         const datarange = [DocumentType.FUNCPLOT].includes(targetType);
         const dataview = [DocumentType.INK, DocumentType.COL, DocumentType.IMG, DocumentType.RTF].includes(targetType) && target?.activeFrame === undefined;
         const poslayoutview = [DocumentType.COL].includes(targetType) && target?.activeFrame === undefined;
-        const viewType = targetType === DocumentType.COL;
+        const type_collection = targetType === DocumentType.COL;
         const filters = true;
         const pivot = true;
         const dataannos = false;
-        return { scrollable, pannable, inkable, viewType, pivot, filters, temporal, clippable, dataview, datarange, poslayoutview, dataannos };
+        return { scrollable, pannable, inkable, type_collection, pivot, filters, temporal, clippable, dataview, datarange, poslayoutview, dataannos };
     }
 
     @action
     playAnnotation = (anno: AudioField) => {};
     @action
     static restoreTargetDocView(bestTargetView: Opt<DocumentView>, activeItem: Doc, transTime: number, pinDocLayout: boolean = BoolCast(activeItem.presPinLayout), pinDataTypes?: pinDataTypes, targetDoc?: Doc) {
-        const bestTarget = bestTargetView?.rootDoc ?? (targetDoc?.unrendered ? DocCast(targetDoc?.annotationOn) : targetDoc);
+        const bestTarget = bestTargetView?.rootDoc ?? (targetDoc?.layout_unrendered ? DocCast(targetDoc?.annotationOn) : targetDoc);
         if (!bestTarget || activeItem === bestTarget) return;
         let changed = false;
         if (pinDocLayout) {
@@ -479,9 +479,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                 changed = true;
             }
         }
-        if ((pinDataTypes?.viewType && activeItem.presViewType !== undefined) || (!pinDataTypes && activeItem.presViewType !== undefined)) {
-            if (bestTarget._viewType !== activeItem.presViewType) {
-                bestTarget._viewType = activeItem.presViewType;
+        if ((pinDataTypes?.type_collection && activeItem.presViewType !== undefined) || (!pinDataTypes && activeItem.presViewType !== undefined)) {
+            if (bestTarget._type_collection !== activeItem.presViewType) {
+                bestTarget._type_collection = activeItem.presViewType;
                 changed = true;
             }
         }
@@ -517,13 +517,13 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         }
         if (pinDataTypes?.dataannos || (!pinDataTypes && activeItem.presAnnotations !== undefined)) {
             const fkey = Doc.LayoutFieldKey(bestTarget);
-            const oldItems = DocListCast(bestTarget[fkey + '_annotations']).filter(doc => doc.unrendered);
+            const oldItems = DocListCast(bestTarget[fkey + '_annotations']).filter(doc => doc.layout_unrendered);
             const newItems = DocListCast(activeItem.presAnnotations).map(doc => {
                 doc.hidden = false;
                 return doc;
             });
             const hiddenItems = DocListCast(bestTarget[fkey + '_annotations'])
-                .filter(doc => !doc.unrendered && !newItems.includes(doc))
+                .filter(doc => !doc.layout_unrendered && !newItems.includes(doc))
                 .map(doc => {
                     doc.hidden = true;
                     return doc;
@@ -603,7 +603,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                 pinProps.pinData.scrollable ||
                 pinProps.pinData.temporal ||
                 pinProps.pinData.pannable ||
-                pinProps.pinData.viewType ||
+                pinProps.pinData.type_collection ||
                 pinProps.pinData.clippable ||
                 pinProps.pinData.datarange ||
                 pinProps.pinData.dataview ||
@@ -616,7 +616,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
             }
             if (pinProps.pinData.dataannos) {
                 const fkey = Doc.LayoutFieldKey(targetDoc);
-                pinDoc.presAnnotations = new List<Doc>(DocListCast(Doc.GetProto(targetDoc)[fkey + '_annotations']).filter(doc => !doc.unrendered));
+                pinDoc.presAnnotations = new List<Doc>(DocListCast(Doc.GetProto(targetDoc)[fkey + '_annotations']).filter(doc => !doc.layout_unrendered));
             }
             if (pinProps.pinData.inkable) {
                 pinDoc.presFillColor = targetDoc.fillColor;
@@ -649,7 +649,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                         })
                     )
                 );
-            if (pinProps.pinData.viewType) pinDoc.presViewType = targetDoc._viewType;
+            if (pinProps.pinData.type_collection) pinDoc.presViewType = targetDoc._type_collection;
             if (pinProps.pinData.filters) pinDoc.presDocFilters = ObjectField.MakeCopy(targetDoc.docFilters as ObjectField);
             if (pinProps.pinData.pivot) pinDoc.presPivotField = targetDoc._pivotField;
             if (pinProps.pinData.pannable) {
@@ -797,7 +797,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         const savedStates = docs.map(doc => {
             switch (doc.type) {
                 case DocumentType.COL:
-                    if (doc._viewType === CollectionViewType.Freeform) return { type: CollectionViewType.Freeform, doc, x: NumCast(doc.freeform_panX), y: NumCast(doc.freeform_panY), s: NumCast(doc.freeform_scale) };
+                    if (doc._type_collection === CollectionViewType.Freeform) return { type: CollectionViewType.Freeform, doc, x: NumCast(doc.freeform_panX), y: NumCast(doc.freeform_panY), s: NumCast(doc.freeform_scale) };
                     break;
                 case DocumentType.INK:
                     if (doc.data instanceof InkField) {
@@ -815,8 +815,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                         case CollectionViewType.Freeform:
                             {
                                 const { x, y, s, doc } = savedState!;
-                                doc._panX = x;
-                                doc._panY = y;
+                                doc._freeform_panX = x;
+                                doc._freeform_panY = y;
                                 doc._freeform_scale = s;
                             }
                             break;
@@ -954,11 +954,11 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
     @undoBatch
     viewChanged = action((e: React.ChangeEvent) => {
         //@ts-ignore
-        const viewType = e.target.selectedOptions[0].value as CollectionViewType;
-        this.layoutDoc.presFieldKey = this.fieldKey + (viewType === CollectionViewType.Tree ? '-linearized' : '');
+        const type_collection = e.target.selectedOptions[0].value as CollectionViewType;
+        this.layoutDoc.presFieldKey = this.fieldKey + (type_collection === CollectionViewType.Tree ? '-linearized' : '');
         // pivot field may be set by the user in timeline view (or some other way) -- need to reset it here
-        [CollectionViewType.Tree || CollectionViewType.Stacking].includes(viewType) && (this.rootDoc._pivotField = undefined);
-        this.rootDoc._viewType = viewType;
+        [CollectionViewType.Tree || CollectionViewType.Stacking].includes(type_collection) && (this.rootDoc._pivotField = undefined);
+        this.rootDoc._type_collection = type_collection;
         if (this.isTreeOrStack) {
             this.layoutDoc._gridGap = 0;
         }
@@ -2126,7 +2126,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
     @computed get toolbar() {
         const propIcon = SettingsManager.propertiesWidth > 0 ? 'angle-double-right' : 'angle-double-left';
         const propTitle = SettingsManager.propertiesWidth > 0 ? 'Close Presentation Panel' : 'Open Presentation Panel';
-        const mode = StrCast(this.rootDoc._viewType) as CollectionViewType;
+        const mode = StrCast(this.rootDoc._type_collection) as CollectionViewType;
         const isMini: boolean = this.toolbarWidth <= 100;
         const inOverlay = DocListCast(Doc.MyOverlayDocs?.data).includes(this.layoutDoc);
         const activeColor = Colors.LIGHT_BLUE;
@@ -2170,7 +2170,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
      * presentPanel: The button to start the presentation / open minimized view of the presentation
      */
     @computed get topPanel() {
-        const mode = StrCast(this.rootDoc._viewType) as CollectionViewType;
+        const mode = StrCast(this.rootDoc._type_collection) as CollectionViewType;
         const isMini: boolean = this.toolbarWidth <= 100;
         const inOverlay = DocListCast(Doc.MyOverlayDocs?.data).includes(this.layoutDoc);
         return (
@@ -2395,7 +2395,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
     render() {
         // needed to ensure that the childDocs are loaded for looking up fields
         this.childDocs.slice();
-        const mode = StrCast(this.rootDoc._viewType) as CollectionViewType;
+        const mode = StrCast(this.rootDoc._type_collection) as CollectionViewType;
         const presEnd = !this.layoutDoc.presLoop && this.itemIndex === this.childDocs.length - 1 && (this.activeItem.presIndexed === undefined || NumCast(this.activeItem.presIndexed) === (this.progressivizedItems(this.activeItem)?.length ?? 0));
         const presStart = !this.layoutDoc.presLoop && this.itemIndex === 0;
         const inOverlay = DocListCast(Doc.MyOverlayDocs?.data).includes(this.layoutDoc);
diff --git a/src/client/views/nodes/trails/PresElementBox.tsx b/src/client/views/nodes/trails/PresElementBox.tsx
index e48d2d674..4eb6aee25 100644
--- a/src/client/views/nodes/trails/PresElementBox.tsx
+++ b/src/client/views/nodes/trails/PresElementBox.tsx
@@ -192,7 +192,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
         const dragData = new DragManager.DocumentDragData(this.presBoxView?.sortArray() ?? []);
         if (!dragData.draggedDocuments.length) dragData.draggedDocuments.push(this.rootDoc);
         dragData.dropAction = 'move';
-        dragData.treeViewDoc = this.presBox?._viewType === CollectionViewType.Tree ? this.presBox : undefined; // this.props.DocumentView?.()?.props.treeViewDoc;
+        dragData.treeViewDoc = this.presBox?._type_collection === CollectionViewType.Tree ? this.presBox : undefined; // this.props.DocumentView?.()?.props.treeViewDoc;
         dragData.moveDocument = this.props.moveDocument;
         const dragItem: HTMLElement[] = [];
         if (dragArray.length === 1) {
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index 40b745cc4..ac43b2956 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -516,7 +516,7 @@ export class PDFViewer extends React.Component<IViewerProps> {
     opaqueFilter = () => [...this.props.docFilters(), Utils.noDragsDocFilter, ...(DragManager.docsBeingDragged.length ? [] : [Utils.IsOpaqueFilter()])];
     childStyleProvider = (doc: Doc | undefined, props: Opt<DocumentViewProps>, property: string): any => {
         if (doc instanceof Doc && property === StyleProp.PointerEvents) {
-            if (doc.textInlineAnnotations) return 'none';
+            if (this.inlineTextAnnotations.includes(doc)) return 'none';
             return 'all';
         }
         return this.props.styleProvider?.(doc, props, property);
diff --git a/src/client/views/search/SearchBox.tsx b/src/client/views/search/SearchBox.tsx
index 536ec7c75..1c1b41f73 100644
--- a/src/client/views/search/SearchBox.tsx
+++ b/src/client/views/search/SearchBox.tsx
@@ -211,7 +211,7 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() {
     }
     @action
     static staticSearchCollection(rootDoc: Opt<Doc>, query: string) {
-        const blockedTypes = [DocumentType.PRESELEMENT, DocumentType.MARKER, DocumentType.KVP, DocumentType.FILTER, DocumentType.SEARCH, DocumentType.SEARCHITEM, DocumentType.FONTICON, DocumentType.BUTTON, DocumentType.SCRIPTING];
+        const blockedTypes = [DocumentType.PRESELEMENT, DocumentType.CONFIG, DocumentType.KVP, DocumentType.FILTER, DocumentType.SEARCH, DocumentType.SEARCHITEM, DocumentType.FONTICON, DocumentType.BUTTON, DocumentType.SCRIPTING];
         const blockedKeys = [
             'x',
             'y',
@@ -240,7 +240,7 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() {
             'layout',
             'layout_keyValue',
             'layout_fitWidth',
-            'viewType',
+            'type_collection',
             'title_custom',
             'freeform_panX',
             'freeform_panY',
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index deda4c876..5312da009 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -238,7 +238,7 @@ export class Doc extends RefField {
             Doc.MyFileOrphans instanceof Doc &&
             Doc.IsDocDataProto(doc) &&
             !Doc.IsSystem(doc) &&
-            ![DocumentType.MARKER, DocumentType.KVP, DocumentType.LINK, DocumentType.LINKANCHOR].includes(doc.type as any) &&
+            ![DocumentType.CONFIG, DocumentType.KVP, DocumentType.LINK, DocumentType.LINKANCHOR].includes(doc.type as any) &&
             !doc.isFolder &&
             !doc.annotationOn
         ) {
diff --git a/src/fields/util.ts b/src/fields/util.ts
index 2a6caaaa3..f4fd3200c 100644
--- a/src/fields/util.ts
+++ b/src/fields/util.ts
@@ -248,7 +248,7 @@ export function distributeAcls(key: string, acl: SharingPermissions, target: Doc
     if (!visited) visited = [] as Doc[];
     if (!target || visited.includes(target)) return;
 
-    if ((target._viewType === CollectionViewType.Docking && visited.length > 1) || Doc.GetProto(visited[0]) !== Doc.GetProto(target)) {
+    if ((target._type_collection === CollectionViewType.Docking && visited.length > 1) || Doc.GetProto(visited[0]) !== Doc.GetProto(target)) {
         target[key] = acl;
         if (target !== Doc.GetProto(target)) {
             //apparently we can't call updateCachedAcls twice (once for the main dashboard, and again for the nested dashboard...???)
diff --git a/src/mobile/MobileInterface.tsx b/src/mobile/MobileInterface.tsx
index a543c2f70..471dc568b 100644
--- a/src/mobile/MobileInterface.tsx
+++ b/src/mobile/MobileInterface.tsx
@@ -260,7 +260,7 @@ export class MobileInterface extends React.Component {
     @action
     componentDidMount = () => {
         // if the home menu is in list view -> adjust the menu toggle appropriately
-        this._menuListView = this._homeDoc._viewType === 'stacking' ? true : false;
+        this._menuListView = this._homeDoc._type_collection === 'stacking' ? true : false;
         Doc.ActiveTool = InkTool.None; // ink should intially be set to none
         Doc.UserDoc().activeMobile = this._homeDoc; // active mobile set to home
         AudioBox.Enabled = true;
@@ -614,7 +614,7 @@ export class MobileInterface extends React.Component {
 
     // The static ink menu that appears at the top
     @computed get inkMenu() {
-        return this._activeDoc._viewType !== CollectionViewType.Docking || !this._ink ? null : <div className="colorSelector">{/* <CollectionFreeFormViewChrome /> */}</div>;
+        return this._activeDoc._type_collection !== CollectionViewType.Docking || !this._ink ? null : <div className="colorSelector">{/* <CollectionFreeFormViewChrome /> */}</div>;
     }
 
     // DocButton that uses UndoManager and handles the opacity change if CanUndo is true
@@ -657,7 +657,7 @@ export class MobileInterface extends React.Component {
 
     // DocButton for switching into ink mode
     @computed get drawInk() {
-        return !this.mainContainer || this._activeDoc._viewType !== CollectionViewType.Docking ? null : (
+        return !this.mainContainer || this._activeDoc._type_collection !== CollectionViewType.Docking ? null : (
             <div className="docButton" id="inkButton" title={Doc.isDocPinned(this._activeDoc) ? 'Pen on' : 'Pen off'} onClick={this.onSwitchInking}>
                 <FontAwesomeIcon className="documentdecorations-icon" size="sm" icon="pen-nib" />
             </div>
@@ -666,7 +666,7 @@ export class MobileInterface extends React.Component {
 
     // DocButton: Button that appears on the bottom of the screen to initiate image upload
     @computed get uploadImageButton() {
-        if (this._activeDoc.type === DocumentType.COL && this._activeDoc !== this._homeDoc && this._activeDoc._viewType !== CollectionViewType.Docking && this._activeDoc.title !== 'WORKSPACES') {
+        if (this._activeDoc.type === DocumentType.COL && this._activeDoc !== this._homeDoc && this._activeDoc._type_collection !== CollectionViewType.Docking && this._activeDoc.title !== 'WORKSPACES') {
             return (
                 <div className="docButton" id="imageButton" title={Doc.isDocPinned(this._activeDoc) ? 'Pen on' : 'Pen off'} onClick={this.toggleUpload}>
                     <FontAwesomeIcon className="documentdecorations-icon" size="sm" icon="upload" />
@@ -722,9 +722,9 @@ export class MobileInterface extends React.Component {
     // Logic for switching the menu into the icons
     @action
     changeToIconView = () => {
-        if ((this._homeDoc._viewType = 'stacking')) {
+        if ((this._homeDoc._type_collection = 'stacking')) {
             this._menuListView = false;
-            this._homeDoc._viewType = 'masonry';
+            this._homeDoc._type_collection = 'masonry';
             this._homeDoc.columnWidth = 300;
             this._homeDoc._columnWidth = 300;
             const menuButtons = DocListCast(this._homeDoc.data);
@@ -742,8 +742,8 @@ export class MobileInterface extends React.Component {
     // Logic for switching the menu into the stacking view
     @action
     changeToListView = () => {
-        if ((this._homeDoc._viewType = 'masonry')) {
-            this._homeDoc._viewType = 'stacking';
+        if ((this._homeDoc._type_collection = 'masonry')) {
+            this._homeDoc._type_collection = 'stacking';
             this._menuListView = true;
             const menuButtons = DocListCast(this._homeDoc.data);
             menuButtons.map(doc => {
@@ -800,7 +800,7 @@ export class MobileInterface extends React.Component {
     // Radial menu can only be used if it is a colleciton and it is not a homeDoc
     // (and cannot be used on Dashboard to avoid pin to presentation opening on right)
     @computed get displayRadialMenu() {
-        return this._activeDoc.type === 'collection' && this._activeDoc !== this._homeDoc && this._activeDoc._viewType !== CollectionViewType.Docking ? <RadialMenu /> : null;
+        return this._activeDoc.type === 'collection' && this._activeDoc !== this._homeDoc && this._activeDoc._type_collection !== CollectionViewType.Docking ? <RadialMenu /> : null;
     }
 
     onDragOver = (e: React.DragEvent) => {
-- 
cgit v1.2.3-70-g09d2


From 46cf6c823ca8ab628cd8c5bd7fdfe8945344a014 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Tue, 16 May 2023 14:50:29 -0400
Subject: fixed bugs with goldenlayout dragging and undoing.  fixed searching
 for filter values in sidebars.  Stopped creating empty list for collections
 when datafield() is accessed because it messes up undo of a collection. fixed
 tab title editing.  from marquee.  Added UndoStack UI and additional naming
 support in code.

---
 src/client/documents/Documents.ts                  | 12 ++--
 src/client/goldenLayout.js                         |  2 +-
 src/client/util/CurrentUserUtils.ts                | 44 +++++++-------
 src/client/util/DragManager.ts                     |  6 +-
 src/client/util/LinkFollower.ts                    |  2 +-
 src/client/util/SelectionManager.ts                |  2 +-
 src/client/util/UndoManager.ts                     | 53 ++++++++++++-----
 src/client/views/ContextMenuItem.tsx               |  5 +-
 src/client/views/DocumentDecorations.tsx           | 46 +++++----------
 src/client/views/EditableView.tsx                  |  2 +
 src/client/views/FilterPanel.tsx                   |  9 ++-
 src/client/views/GlobalKeyHandler.ts               | 10 ++--
 src/client/views/MainView.tsx                      |  8 +--
 src/client/views/PropertiesButtons.tsx             |  6 +-
 .../views/PropertiesDocBacklinksSelector.tsx       |  2 +-
 src/client/views/PropertiesDocContextSelector.tsx  |  2 +-
 src/client/views/PropertiesView.tsx                | 32 +++++-----
 src/client/views/SidebarAnnos.scss                 | 14 +++--
 src/client/views/SidebarAnnos.tsx                  | 15 ++++-
 src/client/views/UndoStack.tsx                     | 30 ++++++++++
 .../views/collections/CollectionDockingView.tsx    | 63 ++++++++++----------
 .../collections/CollectionNoteTakingViewColumn.tsx |  5 +-
 .../collections/CollectionStackedTimeline.tsx      |  3 +-
 .../CollectionStackingViewFieldColumn.tsx          |  4 +-
 src/client/views/collections/CollectionSubView.tsx | 13 ++--
 src/client/views/collections/TabDocView.tsx        | 21 ++++---
 src/client/views/collections/TreeView.tsx          |  4 +-
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  2 -
 .../collections/collectionFreeForm/MarqueeView.tsx | 26 +++-----
 .../collectionSchema/CollectionSchemaView.tsx      | 32 +++++-----
 .../collectionSchema/SchemaColumnHeader.tsx        |  2 -
 .../collections/collectionSchema/SchemaRowBox.tsx  | 10 ++--
 .../collectionSchema/SchemaTableCell.tsx           |  2 +-
 src/client/views/linking/LinkMenu.tsx              |  2 +-
 src/client/views/nodes/AudioBox.tsx                |  2 +-
 src/client/views/nodes/DocumentLinksButton.tsx     | 20 +++----
 src/client/views/nodes/DocumentView.tsx            | 57 ++++++++++--------
 src/client/views/nodes/LinkDocPreview.tsx          |  3 +-
 src/client/views/nodes/button/FontIconBox.tsx      | 60 ++++++++-----------
 .../views/nodes/formattedText/FormattedTextBox.tsx | 69 +++++++++++-----------
 src/client/views/nodes/trails/PresBox.tsx          |  9 ++-
 src/client/views/nodes/trails/PresElementBox.tsx   | 13 ++--
 src/client/views/pdf/AnchorMenu.tsx                | 11 ++--
 src/client/views/search/SearchBox.tsx              |  2 +
 src/fields/Doc.ts                                  | 18 ++++--
 src/fields/util.ts                                 | 38 +++++++-----
 46 files changed, 421 insertions(+), 372 deletions(-)
 create mode 100644 src/client/views/UndoStack.tsx

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

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 2151d0ec7..795d60b48 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -24,7 +24,7 @@ import { DirectoryImportBox } from '../util/Import & Export/DirectoryImportBox';
 import { FollowLinkScript } from '../util/LinkFollower';
 import { LinkManager } from '../util/LinkManager';
 import { ScriptingGlobals } from '../util/ScriptingGlobals';
-import { undoBatch, UndoManager } from '../util/UndoManager';
+import { undoable, undoBatch, UndoManager } from '../util/UndoManager';
 import { CollectionDockingView } from '../views/collections/CollectionDockingView';
 import { DimUnit } from '../views/collections/collectionMulticolumn/CollectionMulticolumnView';
 import { CollectionView } from '../views/collections/CollectionView';
@@ -249,7 +249,7 @@ export class DocumentOptions {
     contextMenuIcons?: List<string>;
     defaultDoubleClick?: 'ignore' | 'default'; // ignore double clicks, or deafult (undefined) means open document full screen
     waitForDoubleClickToClick?: 'always' | 'never' | 'default'; // whether a click function wait for double click to expire.  'default' undefined = wait only if there's a click handler, "never" = never wait, "always" = alway wait
-    dontUndo?: boolean; // whether button clicks should be undoable (this is set to true for Undo/Redo/and sidebar buttons that open the siebar panel)
+    dontUndo?: boolean; // whether button clicks should be undoable ( true for Undo/Redo/and sidebar) AND whether modifications to document are undoable (true for linearview menu buttons to prevent open/close from entering undo stack)
     layout?: string | Doc; // default layout string for a document
     contentPointerEvents?: string; // pointer events allowed for content of a document view.  eg. set to "none" in menuSidebar for sharedDocs so that you can select a document, but not interact with its contents
     childLimitHeight?: number; // whether to limit the height of collection children.  0 - means  height can be no bigger than width
@@ -1532,7 +1532,7 @@ export namespace DocUtils {
                 description: 'Quick Notes',
                 subitems: DocListCast((Doc.UserDoc()['template-notes'] as Doc).data).map((note, i) => ({
                     description: ':' + StrCast(note.title),
-                    event: undoBatch((args: { x: number; y: number }) => {
+                    event: undoable((args: { x: number; y: number }) => {
                         const textDoc = Docs.Create.TextDocument('', {
                             _width: 200,
                             x,
@@ -1546,7 +1546,7 @@ export namespace DocUtils {
                             textDoc[pivotField] = pivotValue;
                         }
                         docTextAdder(textDoc);
-                    }),
+                    }, 'create quick note'),
                     icon: StrCast(note.icon) as IconProp,
                 })) as ContextMenuProps[],
                 icon: 'sticky-note',
@@ -1557,7 +1557,7 @@ export namespace DocUtils {
             .filter(doc => doc && doc !== Doc.UserDoc().emptyTrail && doc !== Doc.UserDoc().emptyDataViz)
             .map((dragDoc, i) => ({
                 description: ':' + StrCast(dragDoc.title).replace('Untitled ', ''),
-                event: undoBatch((args: { x: number; y: number }) => {
+                event: undoable((args: { x: number; y: number }) => {
                     const newDoc = DocUtils.copyDragFactory(dragDoc);
                     if (newDoc) {
                         newDoc.author = Doc.CurrentUserEmail;
@@ -1570,7 +1570,7 @@ export namespace DocUtils {
                         }
                         docAdder?.(newDoc);
                     }
-                }),
+                }, StrCast(dragDoc.title)),
                 icon: Doc.toIcon(dragDoc),
             })) as ContextMenuProps[];
         ContextMenu.Instance.addItem({
diff --git a/src/client/goldenLayout.js b/src/client/goldenLayout.js
index 9cb20d834..843b8bb5f 100644
--- a/src/client/goldenLayout.js
+++ b/src/client/goldenLayout.js
@@ -4740,7 +4740,7 @@
                         newstack._$init();
                         newstack.addChild(this.contentItems[0]);
                     }
-                    correctRowOrCol.addChild(newstack,  insertBefore ? 0 : undefined, true);
+                    correctRowOrCol.addChild(newstack,  !insertBefore ? 0 : undefined, true);
                     newstack.config[dimension] = 50;
                     contentItem.config[dimension] = 50;
                     correctRowOrCol.callDownwards('setSize');
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index 26fe8f440..b0a2c7d60 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -363,7 +363,7 @@ export class CurrentUserUtils {
         const menuBtns = CurrentUserUtils.leftSidebarMenuBtnDescriptions(doc).map(({ title, target, icon, scripts, funcs }) => {
             const btnDoc = myLeftSidebarMenu ? DocListCast(myLeftSidebarMenu.data).find(doc => doc.title === title) : undefined;
             const reqdBtnOpts:DocumentOptions = {
-                title, icon, target, btnType: ButtonType.MenuButton, isSystem: true, dontUndo: true,  dontRegisterView: true,
+                title, icon, target, btnType: ButtonType.MenuButton, isSystem: true, dontUndo: true, dontRegisterView: true,
                 _width: 60, _height: 60,  _stayInCollection: true, _hideContextMenu: true,
                 _removeDropProperties: new List<string>(["_stayInCollection"]),
             };
@@ -598,12 +598,12 @@ export class CurrentUserUtils {
                             CurrentUserUtils.createToolButton(opts), scripts, funcs);
         
         const btnDescs = [// setup reactions to change the highlights on the undo/redo buttons -- would be better to encode this in the undo/redo buttons, but the undo/redo stacks are not wired up that way yet
-            { scripts: { onClick: "undo()"}, opts: { title: "undo", icon: "undo-alt", toolTip: "Click to undo" }},
-            { scripts: { onClick: "redo()"}, opts: { title: "redo", icon: "redo-alt", toolTip: "Click to redo" }},
-            { scripts: { },                  opts: { title: "linker", icon: "linkui", toolTip: "link started"}},
-            { scripts: { },                  opts: { title: "currently playing", icon: "currentlyplayingui", toolTip: "currently playing media"}},
+            { scripts: { onClick: "undo()"},      opts: { title: "undo", icon: "undo-alt", toolTip: "Click to undo" }},
+            { scripts: { onClick: "redo()"},      opts: { title: "redo", icon: "redo-alt", toolTip: "Click to redo" }},
+            { scripts: { },                       opts: { title: "linker", icon: "linkui", toolTip: "link started"}},
+            { scripts: { },                       opts: { title: "currently playing", icon: "currentlyplayingui", toolTip: "currently playing media"}},
         ];
-        const btns = btnDescs.map(desc => dockBtn({_width: 30, _height: 30, dontUndo: true, _stayInCollection: true, ...desc.opts}, desc.scripts));
+        const btns = btnDescs.map(desc => dockBtn({_width: 30, _height: 30, defaultDoubleClick: 'ignore', dontUndo: true, _stayInCollection: true, ...desc.opts}, desc.scripts));
         const dockBtnsReqdOpts:DocumentOptions = {
             title: "docked buttons", _height: 40, flexGap: 0, boxShadow: "standard", childDropAction: 'embed',
             childDontRegisterViews: true, linearViewIsExpanded: true, linearViewExpandable: true, ignoreClick: true
@@ -632,21 +632,21 @@ export class CurrentUserUtils {
     }
     static textTools():Button[] {
         return [
-            { title: "Font",    toolTip: "Font", width: 100,     btnType: ButtonType.DropdownList,                      toolType:"font",     ignoreClick: true,   scripts: {script:  '{ return setFontAttr(self.toolType, value, _readOnly_);}'},
+            { title: "Font",     toolTip: "Font", width: 100,     btnType: ButtonType.DropdownList,                      toolType:"font",     ignoreClick: true,   scripts: {script:  '{ return setFontAttr(self.toolType, value, _readOnly_);}'},
               btnList: new List<string>(["Roboto", "Roboto Mono", "Nunito", "Times New Roman", "Arial", "Georgia", "Comic Sans MS", "Tahoma", "Impact", "Crimson Text"]) },
-            { title: "Size",    toolTip: "Font size (%size)",    btnType: ButtonType.NumberDropdownButton, width: 75,           toolType:"fontSize", ignoreClick: true,   scripts: {script:  '{ return setFontAttr(self.toolType, value, _readOnly_);}'}, numBtnMax: 200, numBtnMin: 0 },
-            { title: "Color",   toolTip: "Font color (%color)",  btnType: ButtonType.ColorButton,  icon: "font",        toolType:"fontColor",ignoreClick: true,   scripts: {script:  '{ return setFontAttr(self.toolType, value, _readOnly_);}'}},
+            { title: "Font Size",toolTip: "Font size (%size)",    btnType: ButtonType.NumberDropdownButton, width: 75,           toolType:"fontSize", ignoreClick: true,   scripts: {script:  '{ return setFontAttr(self.toolType, value, _readOnly_);}'}, numBtnMax: 200, numBtnMin: 0 },
+            { title: "Color",    toolTip: "Font color (%color)",  btnType: ButtonType.ColorButton,  icon: "font",        toolType:"fontColor",ignoreClick: true,   scripts: {script:  '{ return setFontAttr(self.toolType, value, _readOnly_);}'}},
             { title: "Highlight",toolTip:"Font highlight",       btnType: ButtonType.ColorButton,  icon: "highlighter", toolType:"highlight",ignoreClick: true,   scripts: {script:  '{ return setFontAttr(self.toolType, value, _readOnly_);}'},funcs: {hidden: "IsNoviceMode()"} },
-            { title: "Bold",    toolTip: "Bold (Ctrl+B)",        btnType: ButtonType.ToggleButton, icon: "bold",        toolType:"bold",                          scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'} },
-            { title: "Italic",  toolTip: "Italic (Ctrl+I)",      btnType: ButtonType.ToggleButton, icon: "italic",      toolType:"italics",                       scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'} },
-            { title: "Under",   toolTip: "Underline (Ctrl+U)",   btnType: ButtonType.ToggleButton, icon: "underline",   toolType:"underline",                     scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'} },
-            { title: "Bullets", toolTip: "Bullet List",          btnType: ButtonType.ToggleButton, icon: "list",        toolType:"bullet",                        scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'} },
-            { title: "#",       toolTip: "Number List",          btnType: ButtonType.ToggleButton, icon: "list-ol",     toolType:"decimal",                       scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'} },
-            { title: "Left",    toolTip: "Left align (Cmd-[)",   btnType: ButtonType.ToggleButton, icon: "align-left",  toolType:"left",                          scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}' }},
-            { title: "Center",  toolTip: "Center align (Cmd-\\)",btnType: ButtonType.ToggleButton, icon: "align-center",toolType:"center",                        scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'} },
-            { title: "Right",   toolTip: "Right align (Cmd-])",  btnType: ButtonType.ToggleButton, icon: "align-right", toolType:"right",                         scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'} },
-            { title: "Dictate", toolTip: "Dictate",              btnType: ButtonType.ToggleButton, icon: "microphone",  toolType:"dictation",                     scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'}},
-            { title: "NoLink",  toolTip: "Auto Link",            btnType: ButtonType.ToggleButton, icon: "link",        toolType:"noAutoLink", expertMode:true,   scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'}, funcs: {hidden: 'IsNoviceMode()'}},
+            { title: "Bold",     toolTip: "Bold (Ctrl+B)",        btnType: ButtonType.ToggleButton, icon: "bold",        toolType:"bold",                          scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'} },
+            { title: "Italic",   toolTip: "Italic (Ctrl+I)",      btnType: ButtonType.ToggleButton, icon: "italic",      toolType:"italics",                       scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'} },
+            { title: "Under",    toolTip: "Underline (Ctrl+U)",   btnType: ButtonType.ToggleButton, icon: "underline",   toolType:"underline",                     scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'} },
+            { title: "Bullets",  toolTip: "Bullet List",          btnType: ButtonType.ToggleButton, icon: "list",        toolType:"bullet",                        scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'} },
+            { title: "#",        toolTip: "Number List",          btnType: ButtonType.ToggleButton, icon: "list-ol",     toolType:"decimal",                       scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'} },
+            { title: "Left",     toolTip: "Left align (Cmd-[)",   btnType: ButtonType.ToggleButton, icon: "align-left",  toolType:"left",                          scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}' }},
+            { title: "Center",   toolTip: "Center align (Cmd-\\)",btnType: ButtonType.ToggleButton, icon: "align-center",toolType:"center",                        scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'} },
+            { title: "Right",    toolTip: "Right align (Cmd-])",  btnType: ButtonType.ToggleButton, icon: "align-right", toolType:"right",                         scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'} },
+            { title: "Dictate",  toolTip: "Dictate",              btnType: ButtonType.ToggleButton, icon: "microphone",  toolType:"dictation",                     scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'}},
+            { title: "NoLink",   toolTip: "Auto Link",            btnType: ButtonType.ToggleButton, icon: "link",        toolType:"noAutoLink", expertMode:true,   scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'}, funcs: {hidden: 'IsNoviceMode()'}},
             // { title: "Strikethrough", tooltip: "Strikethrough", btnType: ButtonType.ToggleButton, icon: "strikethrough", scripts: {onClick:: 'toggleStrikethrough()'}},
             // { title: "Superscript", tooltip: "Superscript", btnType: ButtonType.ToggleButton, icon: "superscript", scripts: {onClick:: 'toggleSuperscript()'}},
             // { title: "Subscript", tooltip: "Subscript", btnType: ButtonType.ToggleButton, icon: "subscript", scripts: {onClick:: 'toggleSubscript()'}},
@@ -711,7 +711,7 @@ export class CurrentUserUtils {
         const reqdOpts:DocumentOptions = {
             ...OmitKeys(params, ["scripts", "funcs", "subMenu"]).omit,
             backgroundColor: params.backgroundColor ??"transparent", ///  a bit hacky. if an onClick is specified, then assume a toggle uses onClick to get the backgroundColor (see below).  Otherwise, assume a transparent background
-            color: Colors.WHITE, isSystem: true, dontUndo: true, 
+            color: Colors.WHITE, isSystem: true, //dontUndo: true, 
             _nativeWidth: params.width ?? 30, _width: params.width ?? 30,
             _height: 30, _nativeHeight: 30, linearBtnWidth: params.linearBtnWidth,
             toolType: params.toolType, expertMode: params.expertMode,
@@ -727,14 +727,14 @@ export class CurrentUserUtils {
 
     /// Initializes all the default buttons for the top bar context menu
     static setupContextMenuButtons(doc: Doc, field="myContextMenuBtns") {
-        const reqdCtxtOpts:DocumentOptions = { title: "context menu buttons", flexGap: 0, childDropAction: 'embed', childDontRegisterViews: true,  linearViewIsExpanded: true, ignoreClick: true, linearViewExpandable: false, _height: 35 };
+        const reqdCtxtOpts:DocumentOptions = { title: "context menu buttons", dontUndo:true, flexGap: 0, childDropAction: 'embed', childDontRegisterViews: true,  linearViewIsExpanded: true, ignoreClick: true, linearViewExpandable: false, _height: 35 };
         const ctxtMenuBtnsDoc = DocUtils.AssignDocField(doc, field, (opts, items) => this.linearButtonList(opts, items??[]), reqdCtxtOpts, undefined);
         const ctxtMenuBtns = CurrentUserUtils.contextMenuTools().map(params => {
             const menuBtnDoc = DocListCast(ctxtMenuBtnsDoc?.data).find(doc => doc.title === params.title);
             if (!params.subMenu) {
                 return this.setupContextMenuButton(params, menuBtnDoc);
             } else {
-                const reqdSubMenuOpts = { ...OmitKeys(params, ["scripts", "funcs", "subMenu"]).omit,
+                const reqdSubMenuOpts = { ...OmitKeys(params, ["scripts", "funcs", "subMenu"]).omit, dontUndo: true,
                     childDontRegisterViews: true, flexGap: 0, _height: 30, ignoreClick: params.scripts?.onClick ? false : true,
                     linearViewSubMenu: true, linearViewExpandable: true, };
                 const items = params.subMenu?.map(sub =>
diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts
index 070e0f918..e3798233e 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -263,7 +263,7 @@ export namespace DragManager {
 
     // drags a column from a schema view
     export function StartColumnDrag(ele: HTMLElement[], dragData: ColumnDragData, downX: number, downY: number, options?: DragOptions) {
-        StartDrag(ele, dragData, downX, downY, options);
+        StartDrag(ele, dragData, downX, downY, options, undefined, 'Drag Column');
     }
 
     export function SetSnapLines(horizLines: number[], vertLines: number[]) {
@@ -325,10 +325,10 @@ export namespace DragManager {
     export let docsBeingDragged: Doc[] = observable([] as Doc[]);
     export let CanEmbed = false;
     export let DocDragData: DocumentDragData | undefined;
-    export function StartDrag(eles: HTMLElement[], dragData: { [id: string]: any }, downX: number, downY: number, options?: DragOptions, finishDrag?: (dropData: DragCompleteEvent) => void) {
+    export function StartDrag(eles: HTMLElement[], dragData: { [id: string]: any }, downX: number, downY: number, options?: DragOptions, finishDrag?: (dropData: DragCompleteEvent) => void, dragUndoName?:string) {
         if (dragData.dropAction === 'none') return;
         DocDragData = dragData as DocumentDragData;
-        const batch = UndoManager.StartBatch('dragging');
+        const batch = UndoManager.StartBatch(dragUndoName ?? 'document drag');
         eles = eles.filter(e => e);
         CanEmbed = dragData.canEmbed || false;
         if (!dragDiv) {
diff --git a/src/client/util/LinkFollower.ts b/src/client/util/LinkFollower.ts
index 2812d6c88..f74409e42 100644
--- a/src/client/util/LinkFollower.ts
+++ b/src/client/util/LinkFollower.ts
@@ -28,7 +28,7 @@ export class LinkFollower {
     // if the target isn't onscreen, then it will open up the target in the lightbox, or in place
     // depending on the followLinkLocation property of the source (or the link itself as a fallback);
     public static FollowLink = (linkDoc: Opt<Doc>, sourceDoc: Doc, altKey: boolean) => {
-        const batch = UndoManager.StartBatch('follow link click');
+        const batch = UndoManager.StartBatch('Follow Link');
         runInAction(() => (LinkFollower.IsFollowing = true)); // turn off decoration bounds while following links since animations may occur, and DocDecorations is based on screenToLocal which is not always an observable value
         LinkFollower.traverseLink(
             linkDoc,
diff --git a/src/client/util/SelectionManager.ts b/src/client/util/SelectionManager.ts
index fba0a4f76..0125331ec 100644
--- a/src/client/util/SelectionManager.ts
+++ b/src/client/util/SelectionManager.ts
@@ -118,6 +118,6 @@ ScriptingGlobals.add(function SelectionManager_selectedDocType(type: string, exp
     if (type === 'tab') {
         return SelectionManager.Views().lastElement()?.props.renderDepth === 0;
     }
-    let selected = (sel => (checkContext ? DocCast(sel?.context) : sel))(SelectionManager.SelectedSchemaDoc() ?? SelectionManager.Docs().lastElement());
+    let selected = (sel => (checkContext ? DocCast(sel?.embedContainer) : sel))(SelectionManager.SelectedSchemaDoc() ?? SelectionManager.Docs().lastElement());
     return selected?.type === type || selected?.type_collection === type || !type;
 });
diff --git a/src/client/util/UndoManager.ts b/src/client/util/UndoManager.ts
index d0aec45a6..6fef9d660 100644
--- a/src/client/util/UndoManager.ts
+++ b/src/client/util/UndoManager.ts
@@ -3,7 +3,7 @@ import { Without } from '../../Utils';
 
 function getBatchName(target: any, key: string | symbol): string {
     const keyName = key.toString();
-    if (target && target.constructor && target.constructor.name) {
+    if (target?.constructor?.name) {
         return `${target.constructor.name}.${keyName}`;
     }
     return keyName;
@@ -34,6 +34,17 @@ function propertyDecorator(target: any, key: string | symbol) {
     });
 }
 
+export function undoable(fn: (...args: any[]) => any, batchName: string): (...args: any[]) => any {
+    return function () {
+        const batch = UndoManager.StartBatch(batchName);
+        try {
+            return fn.apply(undefined, arguments as any);
+        } finally {
+            batch.end();
+        }
+    };
+}
+
 export function undoBatch(target: any, key: string | symbol, descriptor?: TypedPropertyDescriptor<any>): any;
 export function undoBatch(fn: (...args: any[]) => any): (...args: any[]) => any;
 export function undoBatch(target: any, key?: string | symbol, descriptor?: TypedPropertyDescriptor<any>): any {
@@ -73,15 +84,18 @@ export namespace UndoManager {
     }
     type UndoBatch = UndoEvent[];
 
+    export let undoStackNames: string[] = observable([]);
+    export let redoStackNames: string[] = observable([]);
     export let undoStack: UndoBatch[] = observable([]);
     export let redoStack: UndoBatch[] = observable([]);
     let currentBatch: UndoBatch | undefined;
-    export let batchCounter = 0;
+    export let batchCounter = observable.box(0);
     let undoing = false;
     let tempEvents: UndoEvent[] | undefined = undefined;
 
-    export function AddEvent(event: UndoEvent): void {
-        if (currentBatch && batchCounter && !undoing) {
+    export function AddEvent(event: UndoEvent, value?: any): void {
+        if (currentBatch && batchCounter.get() && !undoing) {
+            console.log('                         '.slice(0, batchCounter.get()) + 'UndoEvent : ' + event.prop + ' = ' + value);
             currentBatch.push(event);
             tempEvents?.push(event);
         }
@@ -135,11 +149,13 @@ export namespace UndoManager {
 
         private dispose = (cancel: boolean) => {
             if (this.disposed) {
-                throw new Error('Cannot dispose an already disposed batch');
+                console.log('WARNING: undo batch already disposed');
+                return false;
+            } else {
+                this.disposed = true;
+                openBatches.splice(openBatches.indexOf(this));
+                return EndBatch(this.batchName, cancel);
             }
-            this.disposed = true;
-            openBatches.splice(openBatches.indexOf(this));
-            return EndBatch(cancel);
         };
 
         end = () => this.dispose(false);
@@ -147,22 +163,23 @@ export namespace UndoManager {
     }
 
     export function StartBatch(batchName: string): Batch {
-        // console.log("Start " + batchCounter + " " + batchName);
-        batchCounter++;
-        if (batchCounter > 0 && currentBatch === undefined) {
+        console.log('                          '.slice(0, batchCounter.get()) + 'Start ' + batchCounter + ' ' + batchName);
+        runInAction(() => batchCounter.set(batchCounter.get() + 1));
+        if (currentBatch === undefined) {
             currentBatch = [];
         }
         return new Batch(batchName);
     }
 
-    const EndBatch = action((cancel: boolean = false) => {
-        batchCounter--;
-        // console.log("End " + batchCounter);
-        if (batchCounter === 0 && currentBatch?.length) {
-            // console.log("------ended----")
+    const EndBatch = action((batchName: string, cancel: boolean = false) => {
+        runInAction(() => batchCounter.set(batchCounter.get() - 1));
+        console.log('                          '.slice(0, batchCounter.get()) + 'End ' + batchName + ' (' + currentBatch?.length + ')');
+        if (batchCounter.get() === 0 && currentBatch?.length) {
             if (!cancel) {
                 undoStack.push(currentBatch);
+                undoStackNames.push(batchName ?? '???');
             }
+            redoStackNames.length = 0;
             redoStack.length = 0;
             currentBatch = undefined;
             return true;
@@ -204,6 +221,7 @@ export namespace UndoManager {
             return;
         }
 
+        const names = undoStackNames.pop();
         const commands = undoStack.pop();
         if (!commands) {
             return;
@@ -215,6 +233,7 @@ export namespace UndoManager {
         }
         undoing = false;
 
+        redoStackNames.push(names ?? '???');
         redoStack.push(commands);
     });
 
@@ -223,6 +242,7 @@ export namespace UndoManager {
             return;
         }
 
+        const names = redoStackNames.pop();
         const commands = redoStack.pop();
         if (!commands) {
             return;
@@ -234,6 +254,7 @@ export namespace UndoManager {
         }
         undoing = false;
 
+        undoStackNames.push(names ?? '???');
         undoStack.push(commands);
     });
 }
diff --git a/src/client/views/ContextMenuItem.tsx b/src/client/views/ContextMenuItem.tsx
index e87d2046b..33f250986 100644
--- a/src/client/views/ContextMenuItem.tsx
+++ b/src/client/views/ContextMenuItem.tsx
@@ -40,10 +40,7 @@ export class ContextMenuItem extends React.Component<ContextMenuProps & { select
     handleEvent = async (e: React.MouseEvent<HTMLDivElement>) => {
         if ('event' in this.props) {
             this.props.closeMenu?.();
-            let batch: UndoManager.Batch | undefined;
-            if (this.props.undoable !== false) {
-                batch = UndoManager.StartBatch(`Context menu event: ${this.props.description}`);
-            }
+            const batch = this.props.undoable !== false ? UndoManager.StartBatch(`Click Menu item: ${this.props.description}`) : undefined;
             await this.props.event({ x: e.clientX, y: e.clientY });
             batch?.end();
         }
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 39073d763..8077b9af1 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -2,23 +2,25 @@ import { IconProp } from '@fortawesome/fontawesome-svg-core';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { Tooltip } from '@material-ui/core';
 import { IconButton } from 'browndash-components';
-import { action, computed, observable, reaction, runInAction } from 'mobx';
+import { action, computed, observable, reaction } from 'mobx';
 import { observer } from 'mobx-react';
 import { FaUndo } from 'react-icons/fa';
 import { DateField } from '../../fields/DateField';
 import { AclAdmin, AclEdit, DataSym, Doc, DocListCast, Field, HeightSym, WidthSym } from '../../fields/Doc';
-import { Document } from '../../fields/documentSchemas';
 import { InkField } from '../../fields/InkField';
+import { RichTextField } from '../../fields/RichTextField';
 import { ScriptField } from '../../fields/ScriptField';
 import { Cast, NumCast, StrCast } from '../../fields/Types';
 import { GetEffectiveAcl } from '../../fields/util';
 import { aggregateBounds, emptyFunction, numberValue, returnFalse, setupMoveUpEvents, Utils } from '../../Utils';
 import { Docs } from '../documents/Documents';
 import { DocumentType } from '../documents/DocumentTypes';
+import { DocumentManager } from '../util/DocumentManager';
 import { DragManager } from '../util/DragManager';
+import { LinkFollower } from '../util/LinkFollower';
 import { SelectionManager } from '../util/SelectionManager';
 import { SnappingManager } from '../util/SnappingManager';
-import { undoBatch, UndoManager } from '../util/UndoManager';
+import { UndoManager } from '../util/UndoManager';
 import { CollectionDockingView } from './collections/CollectionDockingView';
 import { CollectionFreeFormView } from './collections/collectionFreeForm';
 import { DocumentButtonBar } from './DocumentButtonBar';
@@ -27,15 +29,11 @@ import { Colors } from './global/globalEnums';
 import { InkingStroke } from './InkingStroke';
 import { InkStrokeProperties } from './InkStrokeProperties';
 import { LightboxView } from './LightboxView';
-import { DocumentView, OpenWhere, OpenWhereMod } from './nodes/DocumentView';
+import { DocumentView, OpenWhereMod } from './nodes/DocumentView';
 import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox';
 import { ImageBox } from './nodes/ImageBox';
 import React = require('react');
-import { RichTextField } from '../../fields/RichTextField';
-import { LinkFollower } from '../util/LinkFollower';
 import _ = require('lodash');
-import { DocumentManager } from '../util/DocumentManager';
-import { isUndefined } from 'lodash';
 
 @observer
 export class DocumentDecorations extends React.Component<{ PanelWidth: number; PanelHeight: number; boundsLeft: number; boundsTop: number }, { value: string }> {
@@ -150,7 +148,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
                             Doc.SetInPlace(d.rootDoc, titleFieldKey, titleField, true);
                         }
                     }),
-                'title blur'
+                'edit title'
             );
         }
     };
@@ -227,11 +225,6 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
             .filter(v => v && v.props.renderDepth > 0);
         if (forceDeleteOrIconify === false && this._iconifyBatch) return;
         this._deleteAfterIconify = forceDeleteOrIconify || this._iconifyBatch ? true : false;
-        if (!this._iconifyBatch) {
-            this._iconifyBatch = UndoManager.StartBatch('iconifying');
-        } else {
-            forceDeleteOrIconify = false; // can't force immediate close in the middle of iconifying -- have to wait until iconifying completes
-        }
         var iconifyingCount = views.length;
         const finished = action((force?: boolean) => {
             if ((force || --iconifyingCount === 0) && this._iconifyBatch) {
@@ -250,6 +243,12 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
                 this._iconifyBatch = undefined;
             }
         });
+        if (!this._iconifyBatch) {
+            this._iconifyBatch = UndoManager.StartBatch(forceDeleteOrIconify ? 'delete selected docs' : 'iconifying');
+        } else {
+            forceDeleteOrIconify = false; // can't force immediate close in the middle of iconifying -- have to wait until iconifying completes
+        }
+
         if (forceDeleteOrIconify) finished(forceDeleteOrIconify);
         else if (!this._deleteAfterIconify) views.forEach(dv => dv.iconify(finished));
     };
@@ -397,7 +396,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
     onRotateDown = (e: React.PointerEvent): void => {
         this._isRotating = true;
         const rcScreen = { X: this.rotCenter[0], Y: this.rotCenter[1] };
-        const rotateUndo = UndoManager.StartBatch('rotatedown');
+        const rotateUndo = UndoManager.StartBatch('drag rotation');
         const selectedInk = SelectionManager.Views().filter(i => i.ComponentView instanceof InkingStroke);
         const centerPoint = this.rotCenter.slice();
         const infos = new Map<Doc, { unrotatedDocPos: { x: number; y: number }; startRotCtr: { x: number; y: number }; accumRot: number }>();
@@ -465,7 +464,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
         const bounds = e.currentTarget.getBoundingClientRect();
         this._offX = this._resizeHdlId.toLowerCase().includes('left') ? bounds.right - e.clientX : bounds.left - e.clientX;
         this._offY = this._resizeHdlId.toLowerCase().includes('top') ? bounds.bottom - e.clientY : bounds.top - e.clientY;
-        this._resizeUndo = UndoManager.StartBatch('DocDecs resize');
+        this._resizeUndo = UndoManager.StartBatch('drag resizing');
         this._snapX = e.pageX;
         this._snapY = e.pageY;
         const ffviewSet = new Set<CollectionFreeFormView>();
@@ -770,20 +769,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
 
         const topBtn = (key: string, icon: string, pointerDown: undefined | ((e: React.PointerEvent) => void), click: undefined | ((e: any) => void), title: string) => (
             <Tooltip key={key} title={<div className="dash-tooltip">{title}</div>} placement="top">
-                <div
-                    className={`documentDecorations-${key}Button`}
-                    onContextMenu={e => e.preventDefault()}
-                    onPointerDown={
-                        pointerDown ??
-                        (e =>
-                            setupMoveUpEvents(
-                                this,
-                                e,
-                                returnFalse,
-                                emptyFunction,
-                                undoBatch(e => click!(e))
-                            ))
-                    }>
+                <div className={`documentDecorations-${key}Button`} onContextMenu={e => e.preventDefault()} onPointerDown={pointerDown ?? (e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, e => click!(e)))}>
                     <FontAwesomeIcon icon={icon as any} />
                 </div>
             </Tooltip>
diff --git a/src/client/views/EditableView.tsx b/src/client/views/EditableView.tsx
index 6b4132814..7043edcee 100644
--- a/src/client/views/EditableView.tsx
+++ b/src/client/views/EditableView.tsx
@@ -156,12 +156,14 @@ export class EditableView extends React.Component<EditableProps> {
                 break;
             case ':':
                 if (this.props.menuCallback) {
+                    e.stopPropagation();
                     this.props.menuCallback(e.currentTarget.getBoundingClientRect().x, e.currentTarget.getBoundingClientRect().y);
                     break;
                 }
 
             default:
                 if (this.props.textCallback?.(e.key)) {
+                    e.stopPropagation();
                     this._editing = false;
                     this.props.isEditingCallback?.(false);
                 }
diff --git a/src/client/views/FilterPanel.tsx b/src/client/views/FilterPanel.tsx
index 75e0e7c4c..53c1f1018 100644
--- a/src/client/views/FilterPanel.tsx
+++ b/src/client/views/FilterPanel.tsx
@@ -10,6 +10,7 @@ import { UserOptions } from '../util/GroupManager';
 import './FilterPanel.scss';
 import { FieldView } from './nodes/FieldView';
 import { SearchBox } from './search/SearchBox';
+import { undoable } from '../util/UndoManager';
 
 interface filterProps {
     rootDoc: Doc;
@@ -31,7 +32,7 @@ export class FilterPanel extends React.Component<filterProps> {
         return targetView?.ComponentView?.annotationKey ?? targetView?.ComponentView?.fieldKey ?? 'data';
     }
     @computed get targetDocChildren() {
-        return DocListCast(this.targetDoc?.[this.targetDocChildKey] || Doc.ActiveDashboard?.data);
+        return [...DocListCast(this.targetDoc?.[this.targetDocChildKey] || Doc.ActiveDashboard?.data), ...DocListCast(this.targetDoc[Doc.LayoutFieldKey(this.targetDoc) + '_sidebar'])];
     }
 
     @computed get allDocs() {
@@ -89,6 +90,7 @@ export class FilterPanel extends React.Component<filterProps> {
                     const fieldKey = Doc.LayoutFieldKey(t);
                     const annos = !Field.toString(Doc.LayoutField(t) as Field).includes('CollectionView');
                     DocListCast(t[annos ? fieldKey + '_annotations' : fieldKey]).forEach(newdoc => newarray.push(newdoc));
+                    annos && DocListCast(t[fieldKey + '_sidebar']).forEach(newdoc => newarray.push(newdoc));
                 });
                 subDocs = newarray;
             }
@@ -204,7 +206,8 @@ export class FilterPanel extends React.Component<filterProps> {
                                 .find(filter => filter.split(':')[0] === facetHeader)
                                 ?.split(':')[1] ?? '-empty-'
                         }
-                        onKeyDown={e => e.key === 'Enter' && Doc.setDocFilter(this.targetDoc, facetHeader, e.currentTarget.value, !e.currentTarget.value ? 'remove' : 'match')}
+                        onBlur={undoable(e => Doc.setDocFilter(this.targetDoc, facetHeader, e.currentTarget.value, !e.currentTarget.value ? 'remove' : 'match'), 'set text filter')}
+                        onKeyDown={e => e.key === 'Enter' && undoable(e => Doc.setDocFilter(this.targetDoc, facetHeader, e.currentTarget.value, !e.currentTarget.value ? 'remove' : 'match'), 'set text filter')(e)}
                     />
                 );
             case 'checkbox':
@@ -220,7 +223,7 @@ export class FilterPanel extends React.Component<filterProps> {
                                         ?.split(':')[2] === 'check'
                                 }
                                 type={type}
-                                onChange={e => Doc.setDocFilter(this.targetDoc, facetHeader, fval, e.target.checked ? 'check' : 'remove')}
+                                onChange={undoable(e => Doc.setDocFilter(this.targetDoc, facetHeader, fval, e.target.checked ? 'check' : 'remove'), 'set filter')}
                             />
                             {facetValue}
                         </div>
diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts
index b9b92dd2b..625bc760d 100644
--- a/src/client/views/GlobalKeyHandler.ts
+++ b/src/client/views/GlobalKeyHandler.ts
@@ -161,12 +161,10 @@ export class KeyManager {
             case 'delete':
             case 'backspace':
                 if (document.activeElement?.tagName !== 'INPUT' && document.activeElement?.tagName !== 'TEXTAREA') {
-                    UndoManager.RunInBatch(() => {
-                        if (LightboxView.LightboxDoc) {
-                            LightboxView.SetLightboxDoc(undefined);
-                            SelectionManager.DeselectAll();
-                        } else DocumentDecorations.Instance.onCloseClick(true);
-                    }, 'backspace');
+                    if (LightboxView.LightboxDoc) {
+                        LightboxView.SetLightboxDoc(undefined);
+                        SelectionManager.DeselectAll();
+                    } else DocumentDecorations.Instance.onCloseClick(true);
                     return { stopPropagation: true, preventDefault: true };
                 }
                 break;
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 853f9cace..50f451c0a 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -915,11 +915,11 @@ export class MainView extends React.Component {
         return !SelectionManager.Views().some(dv => dv.rootDoc.freeform_snapLines) ? null : (
             <div className="mainView-snapLines">
                 <svg style={{ width: '100%', height: '100%' }}>
-                    {SnappingManager.horizSnapLines().map(l => (
-                        <line x1="0" y1={l} x2="2000" y2={l} stroke="black" opacity={0.3} strokeWidth={0.5} strokeDasharray={'1 1'} />
+                    {SnappingManager.horizSnapLines().map((l, i) => (
+                        <line key={i} x1="0" y1={l} x2="2000" y2={l} stroke="black" opacity={0.3} strokeWidth={0.5} strokeDasharray={'1 1'} />
                     ))}
-                    {SnappingManager.vertSnapLines().map(l => (
-                        <line y1="0" x1={l} y2="2000" x2={l} stroke="black" opacity={0.3} strokeWidth={0.5} strokeDasharray={'1 1'} />
+                    {SnappingManager.vertSnapLines().map((l, i) => (
+                        <line key={i} y1="0" x1={l} y2="2000" x2={l} stroke="black" opacity={0.3} strokeWidth={0.5} strokeDasharray={'1 1'} />
                     ))}
                 </svg>
             </div>
diff --git a/src/client/views/PropertiesButtons.tsx b/src/client/views/PropertiesButtons.tsx
index a37447505..a5c58c9d2 100644
--- a/src/client/views/PropertiesButtons.tsx
+++ b/src/client/views/PropertiesButtons.tsx
@@ -14,7 +14,7 @@ import { DocUtils } from '../documents/Documents';
 import { CollectionViewType, DocumentType } from '../documents/DocumentTypes';
 import { LinkManager } from '../util/LinkManager';
 import { SelectionManager } from '../util/SelectionManager';
-import { undoBatch } from '../util/UndoManager';
+import { undoable, undoBatch } from '../util/UndoManager';
 import { Colors } from './global/globalEnums';
 import { InkingStroke } from './InkingStroke';
 import { DocumentView, OpenWhere } from './nodes/DocumentView';
@@ -51,11 +51,11 @@ export class PropertiesButtons extends React.Component<{}, {}> {
                     <div
                         className={`propertiesButtons-linkButton-empty toggle-${StrCast(targetDoc[property]).includes(':hover') ? 'hover' : targetDoc[property] ? 'on' : 'off'}`}
                         onPointerDown={e => e.stopPropagation()}
-                        onClick={undoBatch(() => {
+                        onClick={undoable(() => {
                             if (SelectionManager.Views().length > 1) {
                                 SelectionManager.Views().forEach(dv => (onClick ?? onPropToggle)(dv, dv.rootDoc, property));
                             } else if (targetDoc) (onClick ?? onPropToggle)(undefined, targetDoc, property);
-                        })}>
+                        }, property)}>
                         <FontAwesomeIcon className="documentdecorations-icon" size="lg" icon={icon(BoolCast(targetDoc?.[property])) as any} />
                     </div>
                     <div className="propertiesButtons-title">{label}</div>
diff --git a/src/client/views/PropertiesDocBacklinksSelector.tsx b/src/client/views/PropertiesDocBacklinksSelector.tsx
index 46e6fd188..7b21629da 100644
--- a/src/client/views/PropertiesDocBacklinksSelector.tsx
+++ b/src/client/views/PropertiesDocBacklinksSelector.tsx
@@ -25,7 +25,7 @@ export class PropertiesDocBacklinksSelector extends React.Component<PropertiesDo
 
         if (otherdoc) {
             otherdoc.hidden = false;
-            this.props.addDocTab(Doc.IsDocDataProto(otherdoc) ? Doc.MakeDelegate(otherdoc) : otherdoc, OpenWhere.toggleRight);
+            this.props.addDocTab(Doc.IsDataProto(otherdoc) ? Doc.MakeDelegate(otherdoc) : otherdoc, OpenWhere.toggleRight);
         }
     };
 
diff --git a/src/client/views/PropertiesDocContextSelector.tsx b/src/client/views/PropertiesDocContextSelector.tsx
index 12a09196e..e1279c9a7 100644
--- a/src/client/views/PropertiesDocContextSelector.tsx
+++ b/src/client/views/PropertiesDocContextSelector.tsx
@@ -48,7 +48,7 @@ export class PropertiesDocContextSelector extends React.Component<PropertiesDocC
 
     getOnClick = (col: Doc, target: Doc) => {
         if (!this.props.DocView) return;
-        col = Doc.IsDocDataProto(col) ? Doc.MakeDelegate(col) : col;
+        col = Doc.IsDataProto(col) ? Doc.MakeDelegate(col) : col;
         DocFocusOrOpen(Doc.GetProto(this.props.DocView.props.Document), undefined, col);
     };
 
diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx
index 19c138a21..297820e37 100644
--- a/src/client/views/PropertiesView.tsx
+++ b/src/client/views/PropertiesView.tsx
@@ -21,7 +21,7 @@ import { LinkManager } from '../util/LinkManager';
 import { SelectionManager } from '../util/SelectionManager';
 import { SharingManager } from '../util/SharingManager';
 import { Transform } from '../util/Transform';
-import { undoBatch, UndoManager } from '../util/UndoManager';
+import { undoable, undoBatch, UndoManager } from '../util/UndoManager';
 import { EditableView } from './EditableView';
 import { FilterPanel } from './FilterPanel';
 import { Colors } from './global/globalEnums';
@@ -538,6 +538,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
                     className="inputBox-input"
                     type="text"
                     value={value}
+                    readOnly={true}
                     onChange={e => {
                         setter(e.target.value);
                     }}
@@ -656,21 +657,21 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
         return this.inputBoxDuo(
             'hgt',
             this.shapeHgt,
-            (val: string) => {
+            undoable((val: string) => {
                 if (!isNaN(Number(val))) {
                     this.shapeHgt = val;
                 }
                 return true;
-            },
+            }, 'set height'),
             'H:',
             'wid',
             this.shapeWid,
-            (val: string) => {
+            undoable((val: string) => {
                 if (!isNaN(Number(val))) {
                     this.shapeWid = val;
                 }
                 return true;
-            },
+            }, 'set width'),
             'W:'
         );
     }
@@ -678,21 +679,21 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
         return this.inputBoxDuo(
             'Xps',
             this.shapeXps,
-            (val: string) => {
+            undoable((val: string) => {
                 if (val !== '0' && !isNaN(Number(val))) {
                     this.shapeXps = val;
                 }
                 return true;
-            },
+            }, 'set x coord'),
             'X:',
             'Yps',
             this.shapeYps,
-            (val: string) => {
+            undoable((val: string) => {
                 if (val !== '0' && !isNaN(Number(val))) {
                     this.shapeYps = val;
                 }
                 return true;
-            },
+            }, 'set y coord'),
             'Y:'
         );
     }
@@ -867,7 +868,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
     regInput = (key: string, value: any, setter: (val: string) => {}) => {
         return (
             <div className="inputBox">
-                <input className="inputBox-input" type="text" value={value} onChange={e => setter(e.target.value)} />
+                <input className="inputBox-input" type="text" readOnly={true} value={value} onChange={e => setter(e.target.value)} />
                 <div className="inputBox-button">
                     <div className="inputBox-button-up" key="up2" onPointerDown={undoBatch(action(() => this.upDownButtons('up', key)))}>
                         <FontAwesomeIcon icon="caret-up" color="white" size="sm" />
@@ -1228,8 +1229,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
         }
     });
 
-    @undoBatch
-    changeFollowBehavior = action((follow: Opt<string>) => this.sourceAnchor && (this.sourceAnchor.followLinkLocation = follow));
+    changeFollowBehavior = undoable((loc: Opt<string>) => this.sourceAnchor && (this.sourceAnchor.followLinkLocation = loc), 'change follow behavior');
 
     @undoBatch
     changeAnimationBehavior = action((behavior: string) => this.sourceAnchor && (this.sourceAnchor.followLinkAnimEffect = behavior));
@@ -1315,6 +1315,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
                 autoComplete={'off'}
                 id="link_relationship_input"
                 value={StrCast(LinkManager.currentLink?.link_relationship)}
+                readOnly={true}
                 onKeyDown={this.onRelationshipKey}
                 onBlur={this.onSelectOutRelationship}
                 onChange={e => this.handlelinkRelationshipChange(e.currentTarget.value)}
@@ -1332,6 +1333,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
                 style={{ textAlign: 'left' }}
                 id="link_description_input"
                 value={Field.toString(LinkManager.currentLink?.link_description as any as Field)}
+                readOnly={true}
                 onKeyDown={this.onDescriptionKey}
                 onBlur={this.onSelectOutDesc}
                 onChange={e => this.handleDescriptionChange(e.currentTarget.value)}
@@ -1457,7 +1459,9 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
                                             <select style={{ width: '100%', gridColumn: 2 }} onChange={e => this.changeAnimationBehavior(e.currentTarget.value)} value={StrCast(this.sourceAnchor?.followLinkAnimEffect, 'default')}>
                                                 <option value="default">Default</option>
                                                 {[PresEffect.None, PresEffect.Zoom, PresEffect.Lightspeed, PresEffect.Fade, PresEffect.Flip, PresEffect.Rotate, PresEffect.Bounce, PresEffect.Roll].map(effect => (
-                                                    <option value={effect.toString()}>{effect.toString()}</option>
+                                                    <option key={effect.toString()} value={effect.toString()}>
+                                                        {effect.toString()}
+                                                    </option>
                                                 ))}
                                             </select>
                                             <div className="effectDirection" style={{ marginLeft: '10px', display: 'grid', width: 40, height: 36, gridColumn: 3, gridTemplateRows: '12px 12px 12px' }}>
@@ -1567,7 +1571,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
                                         <div className="propertiesView-input inline" style={{ display: 'grid', gridTemplateColumns: '78px calc(100% - 108px) 50px' }}>
                                             <p>Zoom %</p>
                                             <div className="ribbon-property" style={{ display: !targZoom ? 'none' : 'inline-flex' }}>
-                                                <input className="presBox-input" style={{ width: '100%' }} type="number" value={zoom} />
+                                                <input className="presBox-input" style={{ width: '100%' }} readOnly={true} type="number" value={zoom} />
                                                 <div className="ribbon-propertyUpDown" style={{ display: 'flex', flexDirection: 'column' }}>
                                                     <div className="ribbon-propertyUpDownItem" onClick={undoBatch(() => this.setZoom(String(zoom), 0.1))}>
                                                         <FontAwesomeIcon icon={'caret-up'} />
diff --git a/src/client/views/SidebarAnnos.scss b/src/client/views/SidebarAnnos.scss
index a0506cb3a..d7de2b641 100644
--- a/src/client/views/SidebarAnnos.scss
+++ b/src/client/views/SidebarAnnos.scss
@@ -4,22 +4,26 @@
     overflow: auto;
     flex-flow: row;
     flex-wrap: wrap;
-    .sidebarAnnos-filterTag, .sidebarAnnos-filterTag-active,
-    .sidebarAnnos-filterUser, .sidebarAnnos-filterUser-active {
+    .sidebarAnnos-filterTag,
+    .sidebarAnnos-filterTag-active,
+    .sidebarAnnos-filterUser,
+    .sidebarAnnos-filterUser-active {
         font-weight: bold;
         font-size: 10px;
         padding-left: 5;
         padding-right: 5;
         box-shadow: black 1px 1px 3px;
         border-radius: 5;
-        margin: 2; 
+        margin: 2;
         height: 15;
         background-color: lightgrey;
     }
-    .sidebarAnnos-filterUser, .sidebarAnnos-filterUser-active {
+    .sidebarAnnos-filterUser,
+    .sidebarAnnos-filterUser-active {
         border-radius: 15px;
     }
+    .sidebarAnnos-filterUser-active,
     .sidebarAnnos-filterTag-active {
         background-color: white;
     }
-}
\ No newline at end of file
+}
diff --git a/src/client/views/SidebarAnnos.tsx b/src/client/views/SidebarAnnos.tsx
index e12621f35..c9e52a1db 100644
--- a/src/client/views/SidebarAnnos.tsx
+++ b/src/client/views/SidebarAnnos.tsx
@@ -49,13 +49,21 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> {
         );
         return keys;
     }
+    @computed get allHashtags() {
+        const keys = new Set<string>();
+        DocListCast(this.props.rootDoc[this.sidebarKey]).forEach(doc => StrListCast(doc.tags).forEach(tag => keys.add(tag)));
+        return Array.from(keys.keys())
+            .filter(key => key[0])
+            .filter(key => !key.startsWith('_') && (key[0] === '#' || key[0] === key[0].toUpperCase()))
+            .sort();
+    }
     @computed get allUsers() {
         const keys = new Set<string>();
         DocListCast(this.props.rootDoc[this.sidebarKey]).forEach(doc => keys.add(StrCast(doc.author)));
         return Array.from(keys.keys()).sort();
     }
     get filtersKey() {
-        return '_' + this.sidebarKey + '-docFilters';
+        return '_' + this.sidebarKey + '_docFilters';
     }
 
     anchorMenuClick = (anchor: Doc) => {
@@ -179,9 +187,9 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> {
     };
     render() {
         const renderTag = (tag: string) => {
-            const active = StrListCast(this.props.rootDoc[this.filtersKey]).includes(`${tag}:${tag}:check`);
+            const active = StrListCast(this.props.rootDoc[this.filtersKey]).includes(`tags:${tag}:check`);
             return (
-                <div key={tag} className={`sidebarAnnos-filterTag${active ? '-active' : ''}`} onClick={e => Doc.setDocFilter(this.props.rootDoc, tag, tag, 'check', true, this.sidebarKey, e.shiftKey)}>
+                <div key={tag} className={`sidebarAnnos-filterTag${active ? '-active' : ''}`} onClick={e => Doc.setDocFilter(this.props.rootDoc, 'tags', tag, 'check', true, this.sidebarKey, e.shiftKey)}>
                     {tag}
                 </div>
             );
@@ -216,6 +224,7 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> {
                 }}>
                 <div className="sidebarAnnos-tagList" style={{ height: this.filtersHeight() }} onWheel={e => e.stopPropagation()}>
                     {this.allUsers.map(renderUsers)}
+                    {this.allHashtags.map(renderTag)}
                     {Array.from(this.allMetadata.keys())
                         .sort()
                         .map(key => renderMeta(key, this.allMetadata.get(key)))}
diff --git a/src/client/views/UndoStack.tsx b/src/client/views/UndoStack.tsx
new file mode 100644
index 000000000..01e184d6b
--- /dev/null
+++ b/src/client/views/UndoStack.tsx
@@ -0,0 +1,30 @@
+import { observer } from 'mobx-react';
+import * as React from 'react';
+import { UndoManager } from '../util/UndoManager';
+import './ScriptingRepl.scss';
+
+@observer
+export class UndoStack extends React.Component {
+    render() {
+        return (
+            <div className="scriptingRepl-outerContainer">
+                <div className="scriptingRepl-commandsContainer" ref={r => r?.scroll({ behavior: 'auto', top: r?.scrollHeight + 20 })} style={{ background: UndoManager.batchCounter.get() ? 'yellow' : undefined }}>
+                    {UndoManager.undoStackNames.map((name, i) => (
+                        <div className="scriptingRepl-resultContainer" key={i}>
+                            <div className="scriptingRepl-commandString">{name.replace(/[^\.]*\./, '')}</div>
+                        </div>
+                    ))}
+                    {Array.from(UndoManager.redoStackNames)
+                        .reverse()
+                        .map((name, i) => (
+                            <div className="scriptingRepl-resultContainer" key={i}>
+                                <div className="scriptingRepl-commandString" style={{ fontWeight: 'bold', color: 'red' }}>
+                                    {name.replace(/[^\.]*\./, '')}
+                                </div>
+                            </div>
+                        ))}
+                </div>
+            </div>
+        );
+    }
+}
diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index cce21a3aa..4ae24af60 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -6,7 +6,7 @@ import { Doc, DocListCast, Opt } from '../../../fields/Doc';
 import { Id } from '../../../fields/FieldSymbols';
 import { InkTool } from '../../../fields/InkField';
 import { List } from '../../../fields/List';
-import { Cast, ImageCast, NumCast, StrCast } from '../../../fields/Types';
+import { ImageCast, NumCast, StrCast } from '../../../fields/Types';
 import { ImageField } from '../../../fields/URLField';
 import { inheritParentAcls } from '../../../fields/util';
 import { emptyFunction, incrementTitleCopy } from '../../../Utils';
@@ -20,14 +20,14 @@ import { SelectionManager } from '../../util/SelectionManager';
 import { undoBatch, UndoManager } from '../../util/UndoManager';
 import { DashboardView } from '../DashboardView';
 import { LightboxView } from '../LightboxView';
+import { OpenWhere, OpenWhereMod } from '../nodes/DocumentView';
+import { OverlayView } from '../OverlayView';
+import { ScriptingRepl } from '../ScriptingRepl';
 import './CollectionDockingView.scss';
 import { CollectionFreeFormView } from './collectionFreeForm';
 import { CollectionSubView, SubCollectionViewProps } from './CollectionSubView';
 import { TabDocView } from './TabDocView';
 import React = require('react');
-import { OpenWhere, OpenWhereMod } from '../nodes/DocumentView';
-import { OverlayView } from '../OverlayView';
-import { ScriptingRepl } from '../ScriptingRepl';
 const _global = (window /* browser */ || global) /* node */ as any;
 
 @observer
@@ -85,6 +85,7 @@ export class CollectionDockingView extends CollectionSubView() {
 
     tabItemDropped = () => DragManager.CompleteWindowDrag?.(false);
     tabDragStart = (proxy: any, finishDrag?: (aborted: boolean) => void) => {
+        this._flush = this._flush ?? UndoManager.StartBatch('tab move');
         const dashDoc = proxy?._contentItem?.tab?.DashDoc as Doc;
         dashDoc && (DragManager.DocDragData = new DragManager.DocumentDragData([proxy._contentItem.tab.DashDoc]));
         DragManager.CompleteWindowDrag = (aborted: boolean) => {
@@ -92,12 +93,12 @@ export class CollectionDockingView extends CollectionSubView() {
                 proxy._dragListener.AbortDrag();
                 if (this._flush) {
                     this._flush.cancel(); // cancel the undo change being logged
-                    this._flush = undefined;
                     this.setupGoldenLayout(); // restore golden layout to where it was before the drag (this is a no-op when using StartOtherDrag because the proxy dragged item was never in the golden layout)
                 }
                 DragManager.CompleteWindowDrag = undefined;
             }
             finishDrag?.(aborted);
+            setTimeout(this.endUndoBatch, 100);
         };
     };
     @undoBatch
@@ -180,7 +181,6 @@ export class CollectionDockingView extends CollectionSubView() {
     //
     //  Creates a split on any side of the docking view based on the passed input pullSide and then adds the Document to the requested side
     //
-    @undoBatch
     @action
     public static AddSplit(document: Doc, pullSide: OpenWhereMod, stack?: any, panelName?: string, keyValue?: boolean) {
         if (document?._type_collection === CollectionViewType.Docking) return DashboardView.openDashboard(document);
@@ -195,6 +195,8 @@ export class CollectionDockingView extends CollectionSubView() {
         if (!instance) return false;
         const docContentConfig = CollectionDockingView.makeDocumentConfig(document, panelName, undefined, keyValue);
 
+        CollectionDockingView.Instance._flush = CollectionDockingView.Instance._flush ?? UndoManager.StartBatch('Add Split');
+        setTimeout(CollectionDockingView.Instance.endUndoBatch, 100);
         if (!pullSide && stack) {
             stack.addChild(docContentConfig, undefined);
             setTimeout(() => stack.setActiveContentItem(stack.contentItems[stack.contentItems.length - 1]));
@@ -370,16 +372,30 @@ export class CollectionDockingView extends CollectionSubView() {
         !LightboxView.LightboxDoc && cur && this._goldenLayout?.updateSize(cur.getBoundingClientRect().width, cur.getBoundingClientRect().height);
     };
 
+    endUndoBatch = () => {
+        const json = JSON.stringify(this._goldenLayout.toConfig());
+        const matches = json.match(/\"documentId\":\"[a-z0-9-]+\"/g);
+        const docids = matches?.map(m => m.replace('"documentId":"', '').replace('"', ''));
+        const docs = !docids
+            ? []
+            : docids
+                  .map(id => DocServer.GetCachedRefField(id))
+                  .filter(f => f)
+                  .map(f => f as Doc);
+        const changesMade = this.props.Document.dockingConfig !== json;
+        if (changesMade) {
+            this.props.Document.dockingConfig = json;
+            this.props.Document.data = new List<Doc>(docs);
+        }
+        this._flush?.end();
+        this._flush = undefined;
+    };
+
     @action
     onPointerUp = (e: MouseEvent): void => {
         window.removeEventListener('pointerup', this.onPointerUp);
-        const flush = this._flush;
-        this._flush = undefined;
-        if (flush) {
-            DragManager.CompleteWindowDrag = undefined;
-            if (!this.stateChanged()) flush.cancel();
-            else flush.end();
-        }
+        DragManager.CompleteWindowDrag = undefined;
+        setTimeout(this.endUndoBatch, 100);
     };
 
     @action
@@ -393,10 +409,8 @@ export class CollectionDockingView extends CollectionSubView() {
             window.addEventListener('mouseup', this.onPointerUp);
             if (!htmlTarget.closest('*.lm_content') && (htmlTarget.closest('*.lm_tab') || htmlTarget.closest('*.lm_stack'))) {
                 const className = typeof htmlTarget.className === 'string' ? htmlTarget.className : '';
-                if (!className.includes('lm_close') && !className.includes('lm_maximise')) {
-                    this._flush = UndoManager.StartBatch('golden layout edit');
-                    DocServer.UPDATE_SERVER_CACHE();
-                }
+                if (className.includes('lm_maximise')) this._flush = UndoManager.StartBatch('tab maximize');
+                else if (!className.includes('lm_close')) DocServer.UPDATE_SERVER_CACHE();
             }
         }
         if (!InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE) && !InteractionUtils.IsType(e, InteractionUtils.PENTYPE) && ![InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool)) {
@@ -452,25 +466,12 @@ export class CollectionDockingView extends CollectionSubView() {
     stateChanged = () => {
         this._ignoreStateChange = JSON.stringify(this._goldenLayout.toConfig());
         const json = JSON.stringify(this._goldenLayout.toConfig());
-        const matches = json.match(/\"documentId\":\"[a-z0-9-]+\"/g);
-        const docids = matches?.map(m => m.replace('"documentId":"', '').replace('"', ''));
-        const docs = !docids
-            ? []
-            : docids
-                  .map(id => DocServer.GetCachedRefField(id))
-                  .filter(f => f)
-                  .map(f => f as Doc);
         const changesMade = this.props.Document.dockingConfig !== json;
-        if (changesMade && !this._flush) {
-            UndoManager.RunInBatch(() => {
-                this.props.Document.dockingConfig = json;
-                this.props.Document.data = new List<Doc>(docs);
-            }, 'state changed');
-        }
         return changesMade;
     };
 
     tabDestroyed = (tab: any) => {
+        this._flush = this._flush ?? UndoManager.StartBatch('tab movement');
         if (tab.DashDoc && ![DocumentType.KVP, DocumentType.PRES].includes(tab.DashDoc?.type)) {
             Doc.AddDocToList(Doc.MyHeaderBar, 'data', tab.DashDoc);
             Doc.AddDocToList(Doc.MyRecentlyClosed, 'data', tab.DashDoc, undefined, true, true);
diff --git a/src/client/views/collections/CollectionNoteTakingViewColumn.tsx b/src/client/views/collections/CollectionNoteTakingViewColumn.tsx
index 63becac1e..2f28ecd00 100644
--- a/src/client/views/collections/CollectionNoteTakingViewColumn.tsx
+++ b/src/client/views/collections/CollectionNoteTakingViewColumn.tsx
@@ -121,7 +121,8 @@ export class CollectionNoteTakingViewColumn extends React.Component<CSVFieldColu
 
     @action pointerEntered = () => SnappingManager.GetIsDragging() && (this._background = '#b4b4b4');
     @action pointerLeave = () => (this._background = 'inherit');
-    textCallback = (char: string) => this.addNewTextDoc('-typed text-', false, true);
+    @undoBatch
+    addTextNote = (char: string) => this.addNewTextDoc('-typed text-', false, true);
 
     // addNewTextDoc is called when a user starts typing in a column to create a new node
     @action
@@ -272,7 +273,7 @@ export class CollectionNoteTakingViewColumn extends React.Component<CSVFieldColu
                         {!this.props.chromeHidden ? (
                             <div className="collectionNoteTakingView-DocumentButtons" style={{ marginBottom: 10 }}>
                                 <div key={`${heading}-add-document`} className="collectionNoteTakingView-addDocumentButton">
-                                    <EditableView GetValue={returnEmptyString} SetValue={this.addNewTextDoc} textCallback={this.textCallback} placeholder={"Type ':' for commands"} contents={'+ New Node'} menuCallback={this.menuCallback} />
+                                    <EditableView GetValue={returnEmptyString} SetValue={this.addNewTextDoc} textCallback={this.addTextNote} placeholder={"Type ':' for commands"} contents={'+ New Node'} menuCallback={this.menuCallback} />
                                 </div>
                                 <div key={`${this.props.Document[Id]}-addGroup`} className="collectionNoteTakingView-addDocumentButton">
                                     <EditableView {...this.props.editableViewProps()} />
diff --git a/src/client/views/collections/CollectionStackedTimeline.tsx b/src/client/views/collections/CollectionStackedTimeline.tsx
index 6b4c8a3e9..b131d38d8 100644
--- a/src/client/views/collections/CollectionStackedTimeline.tsx
+++ b/src/client/views/collections/CollectionStackedTimeline.tsx
@@ -119,7 +119,8 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack
         // onClick play scripts
         CollectionStackedTimeline.RangeScript =
             CollectionStackedTimeline.RangeScript ||
-            ScriptField.MakeFunction(`scriptContext.clickAnchor(this, clientX)`, {
+            ScriptField.MakeFunction(`setTimeout(() => scriptContext.clickAnchor(this, clientX))`, {
+                // setTimeout is a hack to run script in its own properly named undo group (instead of being part of the generic onClick)
                 self: Doc.name,
                 scriptContext: 'any',
                 clientX: 'number',
diff --git a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
index 243550c0b..6be9cb72d 100644
--- a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
+++ b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
@@ -128,7 +128,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
 
     @action pointerEntered = () => SnappingManager.GetIsDragging() && (this._background = '#b4b4b4');
     @action pointerLeave = () => (this._background = 'inherit');
-    textCallback = (char: string) => this.addNewTextDoc('-typed text-', false, true);
+    @undoBatch typedNote = (char: string) => this.addNewTextDoc('-typed text-', false, true);
 
     @action
     addNewTextDoc = (value: string, shiftDown?: boolean, forceEmptyNote?: boolean) => {
@@ -363,7 +363,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
                                 <EditableView
                                     GetValue={returnEmptyString}
                                     SetValue={this.addNewTextDoc}
-                                    textCallback={this.textCallback}
+                                    textCallback={this.typedNote}
                                     placeholder={"Type ':' for commands"}
                                     contents={<FontAwesomeIcon icon={'plus'} />}
                                     menuCallback={this.menuCallback}
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index bcda13c8b..cfe78afa1 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -5,7 +5,6 @@ import { AclPrivate, Doc, DocListCast, Field, Opt, StrListCast } from '../../../
 import { Id } from '../../../fields/FieldSymbols';
 import { List } from '../../../fields/List';
 import { listSpec } from '../../../fields/Schema';
-import { ScriptField } from '../../../fields/ScriptField';
 import { Cast, ScriptCast, StrCast } from '../../../fields/Types';
 import { WebField } from '../../../fields/URLField';
 import { GestureUtils } from '../../../pen-gestures/GestureUtils';
@@ -65,11 +64,7 @@ export function CollectionSubView<X>(moreProps?: X) {
         // to its children which may be templates.
         // If 'annotationField' is specified, then all children exist on that field of the extension document, otherwise, they exist directly on the data document under 'fieldKey'
         @computed get dataField() {
-            if (this.layoutDoc[this.props.fieldKey]) return this.layoutDoc[this.props.fieldKey];
-            // sets the dataDoc's data field to an empty list if the data field is undefined - prevents issues with addonly
-            // setTimeout changes it outside of the @computed section
-            !this.dataDoc[this.props.fieldKey] && setTimeout(() => !this.dataDoc[this.props.fieldKey] && (this.dataDoc[this.props.fieldKey] = new List<Doc>()));
-            return this.dataDoc[this.props.fieldKey];
+            return this.layoutDoc[this.props.fieldKey];
         }
 
         get childLayoutPairs(): { layout: Doc; data: Doc }[] {
@@ -130,8 +125,9 @@ export function CollectionSubView<X>(moreProps?: X) {
                     const fieldKey = Doc.LayoutFieldKey(d);
                     const annos = !Field.toString(Doc.LayoutField(d) as Field).includes(CollectionView.name);
                     const data = d[annos ? fieldKey + '_annotations' : fieldKey];
-                    if (data !== undefined) {
-                        let subDocs = DocListCast(data);
+                    const side = annos && d[fieldKey + '_sidebar'];
+                    if (data !== undefined || side !== undefined) {
+                        let subDocs = [...DocListCast(data), ...DocListCast(side)];
                         if (subDocs.length > 0) {
                             let newarray: Doc[] = [];
                             notFiltered = notFiltered || (!searchDocs.length && DocUtils.FilterDocs(subDocs, childDocFilters, docRangeFilters, d).length);
@@ -142,6 +138,7 @@ export function CollectionSubView<X>(moreProps?: X) {
                                     const annos = !Field.toString(Doc.LayoutField(t) as Field).includes(CollectionView.name);
                                     notFiltered = notFiltered || ((!searchDocs.length || searchDocs.includes(t)) && ((!childDocFilters.length && !docRangeFilters.length) || DocUtils.FilterDocs([t], childDocFilters, docRangeFilters, d).length));
                                     DocListCast(t[annos ? fieldKey + '_annotations' : fieldKey]).forEach(newdoc => newarray.push(newdoc));
+                                    annos && DocListCast(t[fieldKey + '_sidebar']).forEach(newdoc => newarray.push(newdoc));
                                 });
                                 subDocs = newarray;
                             }
diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx
index 7e88959a4..33fa434e1 100644
--- a/src/client/views/collections/TabDocView.tsx
+++ b/src/client/views/collections/TabDocView.tsx
@@ -13,14 +13,13 @@ import { listSpec } from '../../../fields/Schema';
 import { BoolCast, Cast, DocCast, NumCast, StrCast } from '../../../fields/Types';
 import { emptyFunction, lightOrDark, returnEmptyDoclist, returnFalse, returnTrue, setupMoveUpEvents, simulateMouseClick, Utils } from '../../../Utils';
 import { DocServer } from '../../DocServer';
-import { DocUtils } from '../../documents/Documents';
 import { CollectionViewType, DocumentType } from '../../documents/DocumentTypes';
 import { DocumentManager } from '../../util/DocumentManager';
 import { DragManager, dropActionType } from '../../util/DragManager';
 import { SelectionManager } from '../../util/SelectionManager';
 import { SnappingManager } from '../../util/SnappingManager';
 import { Transform } from '../../util/Transform';
-import { undoBatch, UndoManager } from '../../util/UndoManager';
+import { undoable, UndoManager } from '../../util/UndoManager';
 import { DashboardView } from '../DashboardView';
 import { Colors, Shadows } from '../global/globalEnums';
 import { LightboxView } from '../LightboxView';
@@ -116,15 +115,13 @@ export class TabDocView extends React.Component<TabDocViewProps> {
 
             titleEle.size = StrCast(doc.title).length + 3;
             titleEle.value = doc.title;
-            titleEle.onkeydown = (e: KeyboardEvent) => {
-                e.stopPropagation();
-            };
-            titleEle.onchange = undoBatch(
-                action((e: any) => {
+            titleEle.onkeydown = (e: KeyboardEvent) => e.stopPropagation();
+            titleEle.onchange = (e: any) => {
+                undoable(() => {
                     titleEle.size = e.currentTarget.value.length + 3;
                     Doc.GetProto(doc).title = e.currentTarget.value;
-                })
-            );
+                }, 'edit tab title')();
+            };
 
             if (tab.element[0].children[1].children.length === 1) {
                 iconWrap.className = 'lm_iconWrap lm_moreInfo';
@@ -198,7 +195,9 @@ export class TabDocView extends React.Component<TabDocViewProps> {
                 action(selected => {
                     if (selected) this._activated = true;
                     const toggle = tab.element[0].children[2].children[0] as HTMLInputElement;
-                    selected && tab.contentItem !== tab.header.parent.getActiveContentItem() && UndoManager.RunInBatch(() => tab.header.parent.setActiveContentItem(tab.contentItem), 'tab switch');
+                    if (selected && tab.contentItem !== tab.header.parent.getActiveContentItem()) {
+                        undoable(() => tab.header.parent.setActiveContentItem(tab.contentItem), 'tab switch')();
+                    }
                     toggle.style.fontWeight = selected ? 'bold' : '';
                     // toggle.style.textTransform = selected ? "uppercase" : "";
                 }),
@@ -234,7 +233,7 @@ export class TabDocView extends React.Component<TabDocViewProps> {
     public static PinDoc(docs: Doc | Doc[], pinProps: PinProps) {
         const docList = docs instanceof Doc ? [docs] : docs;
 
-        const batch = UndoManager.StartBatch('pinning doc');
+        const batch = UndoManager.StartBatch('Pin doc to pres trail');
         const curPres = Doc.ActivePresentation ?? Doc.MakeCopy(Doc.UserDoc().emptyTrail as Doc, true);
 
         if (!Doc.ActivePresentation) {
diff --git a/src/client/views/collections/TreeView.tsx b/src/client/views/collections/TreeView.tsx
index 037148bb9..f56eaee07 100644
--- a/src/client/views/collections/TreeView.tsx
+++ b/src/client/views/collections/TreeView.tsx
@@ -102,7 +102,7 @@ export class TreeView extends React.Component<TreeViewProps> {
     private _treedropDisposer?: DragManager.DragDropDisposer;
 
     get treeViewOpenIsTransient() {
-        return this.props.treeView.doc.treeViewOpenIsTransient || Doc.IsDocDataProto(this.doc);
+        return this.props.treeView.doc.treeViewOpenIsTransient || Doc.IsDataProto(this.doc);
     }
     set treeViewOpen(c: boolean) {
         if (this.treeViewOpenIsTransient) this._transientOpenState = c;
@@ -221,7 +221,7 @@ export class TreeView extends React.Component<TreeViewProps> {
         } else {
             // choose an appropriate embedding or make one. --- choose the first embedding that (1) user owns,  (2) has no context field ... otherwise make a new embedding
             const bestEmbedding =
-                docView.props.Document.author === Doc.CurrentUserEmail && !Doc.IsDocDataProto(docView.props.Document)
+                docView.props.Document.author === Doc.CurrentUserEmail && !Doc.IsDataProto(docView.props.Document)
                     ? docView.props.Document
                     : DocListCast(this.props.document.proto_embeddings).find(doc => !doc.embedContainer && doc.author === Doc.CurrentUserEmail);
             const nextBestEmbedding = DocListCast(this.props.document.proto_embeddings).find(doc => doc.author === Doc.CurrentUserEmail);
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 5ac444147..95046661e 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -483,7 +483,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         return false;
     }
 
-    @undoBatch
     @action
     updateClusters(_freeform_useClusters: boolean) {
         this.props.Document._freeform_useClusters = _freeform_useClusters;
@@ -534,7 +533,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         }
     }
 
-    @undoBatch
     @action
     updateCluster(doc: Doc) {
         const childLayouts = this.childLayoutPairs.map(pair => pair.layout);
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index 47d7801e6..641088675 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -102,7 +102,6 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
         this._lassoPts = [];
     };
 
-    @undoBatch
     @action
     onKeyPress = (e: KeyboardEvent) => {
         //make textbox and add it to this collection
@@ -111,7 +110,6 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
         const [x, y] = this.Transform.transformPoint(this._downX, this._downY);
         if (e.key === '?') {
             cm.setDefaultItem('?', (str: string) => this.props.addDocTab(Docs.Create.WebDocument(`https://bing.com/search?q=${str}`, { _width: 400, x, y, _height: 512, _nativeWidth: 850, title: 'bing', data_useCors: true }), OpenWhere.addRight));
-
             cm.displayMenu(this._downX, this._downY, undefined, true);
             e.stopPropagation();
         } else if (e.key === 'u' && this.props.ungroup) {
@@ -173,7 +171,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
             e.stopPropagation();
         } else if (!e.ctrlKey && !e.metaKey && SelectionManager.Views().length < 2) {
             FormattedTextBox.SelectOnLoadChar = Doc.UserDoc().defaultTextLayout && !this.props.childLayoutString ? e.key : '';
-            FormattedTextBox.LiveTextUndo = UndoManager.StartBatch('live text batch');
+            FormattedTextBox.LiveTextUndo = UndoManager.StartBatch('type new note');
             this.props.addLiveTextDocument(DocUtils.GetNewTextDoc('-typed text-', x, y, 200, 100, this.props.xPadding === 0));
             e.stopPropagation();
         }
@@ -396,6 +394,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
         return newCollection;
     });
 
+    @undoBatch
     @action
     pileup = (e: KeyboardEvent | React.PointerEvent | undefined) => {
         const selected = this.marqueeSelect(false);
@@ -508,8 +507,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
     };
 
     @undoBatch
-    @action
-    summary = (e: KeyboardEvent | React.PointerEvent | undefined) => {
+    summary = action((e: KeyboardEvent | React.PointerEvent | undefined) => {
         const selected = this.marqueeSelect(false).map(d => {
             this.props.removeDocument?.(d);
             d.x = NumCast(d.x) - this.Bounds.left;
@@ -534,19 +532,10 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
         this.props.addDocument?.(portal);
         this.props.addLiveTextDocument(summary);
         MarqueeOptionsMenu.Instance.fadeOut(true);
-    };
+    });
 
     @action
-    background = (e: KeyboardEvent | React.PointerEvent | undefined) => {
-        const newCollection = this.getCollection([], undefined, undefined);
-        this.props.addDocument?.(newCollection);
-        MarqueeOptionsMenu.Instance.fadeOut(true);
-        this.hideMarquee();
-        setTimeout(() => this.props.selectDocuments([newCollection]));
-    };
-
-    @undoBatch
-    marqueeCommand = action((e: KeyboardEvent) => {
+    marqueeCommand = (e: KeyboardEvent) => {
         if (this._commandExecuted || (e as any).propagationIsStopped) {
             return;
         }
@@ -557,7 +546,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
             this.delete(e, e.key === 'h');
             e.stopPropagation();
         }
-        if ('cbtsSpg'.indexOf(e.key) !== -1) {
+        if ('ctsSpg'.indexOf(e.key) !== -1) {
             this._commandExecuted = true;
             e.stopPropagation();
             e.preventDefault();
@@ -565,7 +554,6 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
             if (e.key === 'g') this.collection(e, true);
             if (e.key === 'c' || e.key === 't') this.collection(e);
             if (e.key === 's' || e.key === 'S') this.summary(e);
-            if (e.key === 'b') this.background(e);
             if (e.key === 'p') this.pileup(e);
             this.cleanupInteractions(false);
         }
@@ -575,7 +563,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
             e.preventDefault();
             this._lassoFreehand = !this._lassoFreehand;
         }
-    });
+    };
 
     touchesLine(r1: { left: number; top: number; width: number; height: number }) {
         for (const lassoPt of this._lassoPts) {
diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx
index bcc2ca129..2a68f9b58 100644
--- a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx
+++ b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx
@@ -13,7 +13,7 @@ import { Docs, DocumentOptions, DocUtils, FInfo } from '../../../documents/Docum
 import { DocumentManager } from '../../../util/DocumentManager';
 import { DragManager } from '../../../util/DragManager';
 import { SelectionManager } from '../../../util/SelectionManager';
-import { undoBatch } from '../../../util/UndoManager';
+import { undoable, undoBatch } from '../../../util/UndoManager';
 import { ContextMenu } from '../../ContextMenu';
 import { EditableView } from '../../EditableView';
 import { Colors } from '../../global/globalEnums';
@@ -25,6 +25,8 @@ import { CollectionSubView } from '../CollectionSubView';
 import './CollectionSchemaView.scss';
 import { SchemaColumnHeader } from './SchemaColumnHeader';
 import { SchemaRowBox } from './SchemaRowBox';
+import { faHeartPulse } from '@fortawesome/free-solid-svg-icons';
+import { faGalacticSenate } from '@fortawesome/free-brands-svg-icons';
 
 export enum ColumnType {
     Number,
@@ -224,7 +226,7 @@ export class CollectionSchemaView extends CollectionSubView() {
 
     @undoBatch
     @action
-    setSort = (field: string | undefined, desc: boolean = false) => {
+    setColumnSort = (field: string | undefined, desc: boolean = false) => {
         this.layoutDoc.sortField = field;
         this.layoutDoc.sortDesc = desc;
     };
@@ -484,24 +486,11 @@ export class CollectionSchemaView extends CollectionSubView() {
         return false;
     };
 
-    @action
-    addNewTextDoc = (value: string, shiftDown?: boolean, forceEmptyNote?: boolean) => {
-        if (!value && !forceEmptyNote) return false;
-        const newDoc = Docs.Create.TextDocument(value, { title: value, _layout_autoHeight: true });
-        FormattedTextBox.SelectOnLoad = newDoc[Id];
-        FormattedTextBox.SelectOnLoadChar = forceEmptyNote ? '' : ' ';
-        return this.addRow(newDoc) || false;
-    };
-
     menuCallback = (x: number, y: number) => {
         ContextMenu.Instance.clearItems();
 
-        DocUtils.addDocumentCreatorMenuItems(doc => this.addRow(doc), this.addRow, x, y, true);
+        DocUtils.addDocumentCreatorMenuItems(this.addRow, this.addRow, x, y, true);
 
-        ContextMenu.Instance.setDefaultItem('::', (name: string): void => {
-            Doc.GetProto(this.props.Document)[name] = '';
-            this.addRow(Docs.Create.TextDocument('', { title: name, _layout_autoHeight: true }));
-        });
         ContextMenu.Instance.displayMenu(x, y, undefined, true);
     };
 
@@ -866,7 +855,7 @@ export class CollectionSchemaView extends CollectionSubView() {
                                 columnWidths={this.displayColumnWidths}
                                 sortField={this.sortField}
                                 sortDesc={this.sortDesc}
-                                setSort={this.setSort}
+                                setSort={this.setColumnSort}
                                 rowHeight={this.rowHeightFunc}
                                 removeColumn={this.removeColumn}
                                 resizeColumn={this.startResize}
@@ -879,7 +868,14 @@ export class CollectionSchemaView extends CollectionSubView() {
                     {this._columnMenuIndex !== undefined && this.renderColumnMenu}
                     {this._filterColumnIndex !== undefined && this.renderFilterMenu}
                     <CollectionSchemaViewDocs schema={this} childDocs={this.sortedDocsFunc} rowHeight={this.rowHeightFunc} setRef={(ref: HTMLDivElement | null) => (this._tableContentRef = ref)} />
-                    <EditableView GetValue={returnEmptyString} SetValue={this.addNewTextDoc} placeholder={"Type ':' for commands"} contents={'+ New Node'} menuCallback={this.menuCallback} height={CollectionSchemaView._newNodeInputHeight} />
+                    <EditableView
+                        GetValue={returnEmptyString}
+                        SetValue={undoable(value => (value ? this.addRow(Docs.Create.TextDocument(value, { title: value, _layout_autoHeight: true })) : false), 'add text doc')}
+                        placeholder={"Type ':' for commands"}
+                        contents={'+ New Node'}
+                        menuCallback={this.menuCallback}
+                        height={CollectionSchemaView._newNodeInputHeight}
+                    />
                 </div>
                 {this.previewWidth > 0 && <div className="schema-preview-divider" style={{ width: CollectionSchemaView._previewDividerWidth }} onPointerDown={this.onDividerDown}></div>}
                 {this.previewWidth > 0 && (
diff --git a/src/client/views/collections/collectionSchema/SchemaColumnHeader.tsx b/src/client/views/collections/collectionSchema/SchemaColumnHeader.tsx
index 7da3c042c..46c2f622b 100644
--- a/src/client/views/collections/collectionSchema/SchemaColumnHeader.tsx
+++ b/src/client/views/collections/collectionSchema/SchemaColumnHeader.tsx
@@ -5,8 +5,6 @@ import { observer } from 'mobx-react';
 import { emptyFunction, setupMoveUpEvents } from '../../../../Utils';
 import { Colors } from '../../global/globalEnums';
 import './CollectionSchemaView.scss';
-import { SnappingManager } from '../../../util/SnappingManager';
-import { DragManager } from '../../../util/DragManager';
 
 export interface SchemaColumnHeaderProps {
     columnKeys: string[];
diff --git a/src/client/views/collections/collectionSchema/SchemaRowBox.tsx b/src/client/views/collections/collectionSchema/SchemaRowBox.tsx
index 45bfe4f77..978b65053 100644
--- a/src/client/views/collections/collectionSchema/SchemaRowBox.tsx
+++ b/src/client/views/collections/collectionSchema/SchemaRowBox.tsx
@@ -7,7 +7,7 @@ import { Doc } from '../../../../fields/Doc';
 import { BoolCast } from '../../../../fields/Types';
 import { DragManager } from '../../../util/DragManager';
 import { SnappingManager } from '../../../util/SnappingManager';
-import { undoBatch } from '../../../util/UndoManager';
+import { undoable, undoBatch } from '../../../util/UndoManager';
 import { ViewBoxBaseComponent } from '../../DocComponent';
 import { Colors } from '../../global/globalEnums';
 import { OpenWhere } from '../../nodes/DocumentView';
@@ -111,18 +111,18 @@ export class SchemaRowBox extends ViewBoxBaseComponent<FieldViewProps>() {
                     }}>
                     <div
                         className="schema-row-button"
-                        onPointerDown={undoBatch(e => {
+                        onPointerDown={undoable(e => {
                             e.stopPropagation();
                             this.props.removeDocument?.(this.rootDoc);
-                        })}>
+                        }, 'Delete Row')}>
                         <FontAwesomeIcon icon="times" />
                     </div>
                     <div
                         className="schema-row-button"
-                        onPointerDown={e => {
+                        onPointerDown={undoable(e => {
                             e.stopPropagation();
                             this.props.addDocTab(this.rootDoc, OpenWhere.addRight);
-                        }}>
+                        }, 'Open Doc on Right')}>
                         <FontAwesomeIcon icon="external-link-alt" />
                     </div>
                 </div>
diff --git a/src/client/views/collections/collectionSchema/SchemaTableCell.tsx b/src/client/views/collections/collectionSchema/SchemaTableCell.tsx
index 42bf32475..6d5ef9df6 100644
--- a/src/client/views/collections/collectionSchema/SchemaTableCell.tsx
+++ b/src/client/views/collections/collectionSchema/SchemaTableCell.tsx
@@ -122,7 +122,7 @@ export class SchemaTableCell extends React.Component<SchemaTableCellProps> {
                         const ret = KeyValueBox.SetField(this.props.Document, this.props.fieldKey.replace(/^_/, ''), value);
                         this.props.finishEdit?.();
                         return ret;
-                    })}
+                    }, 'edit schema cell')}
                 />
             </div>
         );
diff --git a/src/client/views/linking/LinkMenu.tsx b/src/client/views/linking/LinkMenu.tsx
index 3f6369898..65d13a6c3 100644
--- a/src/client/views/linking/LinkMenu.tsx
+++ b/src/client/views/linking/LinkMenu.tsx
@@ -49,7 +49,7 @@ export class LinkMenu extends React.Component<Props> {
             <LinkMenuGroup key={group[0]} itemHandler={this.props.itemHandler} docView={this.props.docView} sourceDoc={this.props.docView.props.Document} group={group[1]} groupType={group[0]} clearLinkEditor={this.clear} />
         ));
 
-        return linkItems.length ? linkItems : this.props.style ? [<></>] : [<p key="">No links have been created yet. Drag the linking button onto another document to create a link.</p>];
+        return linkItems.length ? linkItems : this.props.style ? [] : [<p key="none">No links have been created yet. Drag the linking button onto another document to create a link.</p>];
     };
 
     render() {
diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx
index 8e83cf121..410e0bbdc 100644
--- a/src/client/views/nodes/AudioBox.tsx
+++ b/src/client/views/nodes/AudioBox.tsx
@@ -447,7 +447,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
 
     timelineScreenToLocal = () => this.props.ScreenToLocalTransform().translate(0, -AudioBox.topControlsHeight);
 
-    setPlayheadTime = (time: number) => (this._ele!.currentTime = this.layoutDoc._layout_currentTimecode = time);
+    setPlayheadTime = (time: number) => (this._ele!.currentTime /*= this.layoutDoc._layout_currentTimecode*/ = time);
 
     playing = () => this.mediaState === media_state.Playing;
 
diff --git a/src/client/views/nodes/DocumentLinksButton.tsx b/src/client/views/nodes/DocumentLinksButton.tsx
index d5ca30957..bd1952ecb 100644
--- a/src/client/views/nodes/DocumentLinksButton.tsx
+++ b/src/client/views/nodes/DocumentLinksButton.tsx
@@ -9,7 +9,7 @@ import { DocUtils } from '../../documents/Documents';
 import { DragManager } from '../../util/DragManager';
 import { Hypothesis } from '../../util/HypothesisUtils';
 import { LinkManager } from '../../util/LinkManager';
-import { undoBatch, UndoManager } from '../../util/UndoManager';
+import { undoable, undoBatch, UndoManager } from '../../util/UndoManager';
 import './DocumentLinksButton.scss';
 import { DocumentView } from './DocumentView';
 import { LinkDescriptionPopup } from './LinkDescriptionPopup';
@@ -78,7 +78,6 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp
         );
     };
 
-    @undoBatch
     onLinkButtonDown = (e: React.PointerEvent): void => {
         setupMoveUpEvents(
             this,
@@ -123,17 +122,14 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp
             e,
             returnFalse,
             emptyFunction,
-            undoBatch(
-                action(e => {
-                    DocumentLinksButton.finishLinkClick(e.clientX, e.clientY, DocumentLinksButton.StartLink, this.props.View.props.Document, true, this.props.View);
-                })
-            )
+            action(e => DocumentLinksButton.finishLinkClick(e.clientX, e.clientY, DocumentLinksButton.StartLink, this.props.View.props.Document, true, this.props.View))
         );
     };
 
-    public static finishLinkClick = undoBatch(
-        action((screenX: number, screenY: number, startLink: Doc, endLink: Doc, startIsAnnotation: boolean, endLinkView?: DocumentView, pinProps?: PinProps) => {
-            if (startLink === endLink) {
+    @undoBatch
+    public static finishLinkClick(screenX: number, screenY: number, startLink: Doc | undefined, endLink: Doc, startIsAnnotation: boolean, endLinkView?: DocumentView, pinProps?: PinProps) {
+        runInAction(() => {
+            if (startLink === endLink || !startLink) {
                 DocumentLinksButton.StartLink = undefined;
                 DocumentLinksButton.StartLinkView = undefined;
                 DocumentLinksButton.AnnotationId = undefined;
@@ -185,8 +181,8 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp
                     );
                 }
             }
-        })
-    );
+        });
+    }
 
     @action clearLinks() {
         DocumentLinksButton.StartLink = undefined;
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 359b72352..52eee84ac 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -7,6 +7,7 @@ import { AclPrivate, AnimationSym, DataSym, Doc, DocListCast, Field, Opt, StrLis
 import { Id } from '../../../fields/FieldSymbols';
 import { InkTool } from '../../../fields/InkField';
 import { List } from '../../../fields/List';
+import { RefField } from '../../../fields/RefField';
 import { listSpec } from '../../../fields/Schema';
 import { ScriptField } from '../../../fields/ScriptField';
 import { BoolCast, Cast, DocCast, ImageCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
@@ -35,8 +36,11 @@ import { ContextMenuProps } from '../ContextMenuItem';
 import { DocComponent } from '../DocComponent';
 import { EditableView } from '../EditableView';
 import { GestureOverlay } from '../GestureOverlay';
+import { InkingStroke } from '../InkingStroke';
 import { LightboxView } from '../LightboxView';
+import { OverlayView } from '../OverlayView';
 import { StyleProp } from '../StyleProvider';
+import { UndoStack } from '../UndoStack';
 import { CollectionFreeFormDocumentView } from './CollectionFreeFormDocumentView';
 import { DocumentContentsView, ObserverJsxParser } from './DocumentContentsView';
 import { DocumentLinksButton } from './DocumentLinksButton';
@@ -47,8 +51,6 @@ import { LinkAnchorBox } from './LinkAnchorBox';
 import { PresEffect, PresEffectDirection } from './trails';
 import { PinProps, PresBox } from './trails/PresBox';
 import React = require('react');
-import { InkingStroke } from '../InkingStroke';
-import { RefField } from '../../../fields/RefField';
 const { Howl } = require('howler');
 
 interface Window {
@@ -456,27 +458,26 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
                         // instead of in the global lightbox
                         const oldFunc = DocumentViewInternal.addDocTabFunc;
                         DocumentViewInternal.addDocTabFunc = this.props.addDocTab;
-                        const res =
-                            this.onClickHandler?.script.run(
-                                {
-                                    this: this.layoutDoc,
-                                    self: this.rootDoc,
-                                    _readOnly_: false,
-                                    scriptContext: this.props.scriptContext,
-                                    documentView: this.props.DocumentView(),
-                                    clientX,
-                                    clientY,
-                                    shiftKey,
-                                    altKey,
-                                    metaKey,
-                                },
-                                console.log
-                            ).result?.select === true
-                                ? this.props.select(false)
-                                : '';
+                        this.onClickHandler?.script.run(
+                            {
+                                this: this.layoutDoc,
+                                self: this.rootDoc,
+                                _readOnly_: false,
+                                scriptContext: this.props.scriptContext,
+                                documentView: this.props.DocumentView(),
+                                clientX,
+                                clientY,
+                                shiftKey,
+                                altKey,
+                                metaKey,
+                            },
+                            console.log
+                        ).result?.select === true
+                            ? this.props.select(false)
+                            : '';
                         DocumentViewInternal.addDocTabFunc = oldFunc;
                     };
-                    clickFunc = () => (this.props.Document.dontUndo ? func() : UndoManager.RunInBatch(func, 'on click'));
+                    clickFunc = () => (this.props.Document.dontUndo ? func() : UndoManager.RunInBatch(func, 'click ' + this.rootDoc.title));
                 } else {
                     // onDragStart implies a button doc that we don't want to select when clicking. RootDocument & isTemplateForField implies we're clicking on part of a template instance and we want to select the whole template, not the part
                     if ((this.layoutDoc.onDragStart || this.props.Document.rootDocument) && !(e.ctrlKey || e.button > 0)) {
@@ -490,7 +491,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
                 if ((clickFunc && waitFordblclick !== 'never') || waitFordblclick === 'always') {
                     this._doubleClickTimeout && clearTimeout(this._doubleClickTimeout);
                     this._doubleClickTimeout = setTimeout(this._singleClickFunc, 300);
-                } else {
+                } else if (!DocumentView.LongPress) {
                     this._singleClickFunc();
                     this._singleClickFunc = undefined;
                 }
@@ -502,7 +503,15 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
 
     @action
     onPointerDown = (e: React.PointerEvent): void => {
-        this._longPressSelector = setTimeout(() => DocumentView.LongPress && this.props.select(false), 1000);
+        this._longPressSelector = setTimeout(() => {
+            if (DocumentView.LongPress) {
+                if (this.rootDoc.dontUndo) {
+                    OverlayView.Instance.addWindow(<UndoStack />, { x: 300, y: 100, width: 200, height: 200, title: 'Undo Stack' });
+                } else {
+                    this.props.select(false);
+                }
+            }
+        }, 1000);
         if (!GestureOverlay.DownDocView) GestureOverlay.DownDocView = this.props.DocumentView();
 
         this._downX = e.clientX;
@@ -557,7 +566,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
         if (this.onPointerUpHandler?.script) {
             this.onPointerUpHandler.script.run({ self: this.rootDoc, this: this.layoutDoc }, console.log);
         } else if (e.button === 0 && Utils.isClick(e.clientX, e.clientY, this._downX, this._downY, this._downTime)) {
-            this._doubleTap = Date.now() - this._lastTap < Utils.CLICK_TIME;
+            this._doubleTap = this.rootDoc.defaultDoubleClick !== 'ignore' && Date.now() - this._lastTap < Utils.CLICK_TIME;
             if (!this.props.isSelected(true)) this._lastTap = Date.now(); // don't want to process the start of a double tap if the doucment is selected
         }
         if (DocumentView.LongPress) e.preventDefault();
diff --git a/src/client/views/nodes/LinkDocPreview.tsx b/src/client/views/nodes/LinkDocPreview.tsx
index c6172ee01..450176a49 100644
--- a/src/client/views/nodes/LinkDocPreview.tsx
+++ b/src/client/views/nodes/LinkDocPreview.tsx
@@ -123,7 +123,6 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> {
                                 this._markerTargetDoc = linkTarget;
                                 this._targetDoc = /*linkTarget?.type === DocumentType.MARKER &&*/ linkTarget?.annotationOn ? Cast(linkTarget.annotationOn, Doc, null) ?? linkTarget : linkTarget;
                             }
-                            this._toolTipText = 'link to ' + this._targetDoc?.title;
                             if (LinkDocPreview.LinkInfo?.noPreview || this._linkSrc?.followLinkToggle || this._markerTargetDoc?.type === DocumentType.PRES) this.followLink();
                         }
                     })
@@ -296,7 +295,7 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> {
                 className="linkDocPreview"
                 ref={this._linkDocRef}
                 onPointerDown={this.followLinkPointerDown}
-                style={{ display: !this._toolTipText ? 'none' : undefined, left: this.props.location[0], top: this.props.location[1], width: this.width() + borders, height: this.height() + borders + (this.props.showHeader ? 37 : 0) }}>
+                style={{ left: this.props.location[0], top: this.props.location[1], width: this.width() + borders, height: this.height() + borders + (this.props.showHeader ? 37 : 0) }}>
                 {this.docPreview}
             </div>
         );
diff --git a/src/client/views/nodes/button/FontIconBox.tsx b/src/client/views/nodes/button/FontIconBox.tsx
index 57aa852ac..b07cf7e00 100644
--- a/src/client/views/nodes/button/FontIconBox.tsx
+++ b/src/client/views/nodes/button/FontIconBox.tsx
@@ -16,7 +16,7 @@ import { CollectionViewType, DocumentType } from '../../../documents/DocumentTyp
 import { LinkManager } from '../../../util/LinkManager';
 import { ScriptingGlobals } from '../../../util/ScriptingGlobals';
 import { SelectionManager } from '../../../util/SelectionManager';
-import { undoBatch, UndoManager } from '../../../util/UndoManager';
+import { undoable, undoBatch, UndoManager } from '../../../util/UndoManager';
 import { CollectionFreeFormView } from '../../collections/collectionFreeForm';
 import { ContextMenu } from '../../ContextMenu';
 import { DocComponent } from '../../DocComponent';
@@ -129,29 +129,27 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
      * Number button
      */
     @computed get numberSliderButton() {
-        const numScript = ScriptCast(this.rootDoc.script);
-        const setValue = (value: number) => UndoManager.RunInBatch(() => numScript?.script.run({ self: this.rootDoc, value, _readOnly_: false }), 'set num value');
-
+        const numScript = (value?: number) => ScriptCast(this.rootDoc.script).script.run({ self: this.rootDoc, value, _readOnly_: value === undefined });
         // Script for checking the outcome of the toggle
-        const checkResult = Number(numScript?.script.run({ self: this.rootDoc, value: 0, _readOnly_: true }).result ?? 0).toPrecision(NumCast(this.dataDoc.numPrecision, 3));
-
+        const checkResult = Number(numScript().result ?? 0).toPrecision(NumCast(this.dataDoc.numPrecision, 3));
         const label = !FontIconBox.GetShowLabels() ? null : <div className="fontIconBox-label">{this.label}</div>;
 
         const dropdown = (
             <div className="menuButton-dropdownBox" onPointerDown={e => e.stopPropagation()}>
                 <input
+                    className="menu-slider"
                     type="range"
                     step="1"
                     min={NumCast(this.rootDoc.numBtnMin, 0)}
                     max={NumCast(this.rootDoc.numBtnMax, 100)}
+                    //readOnly={true}
                     value={checkResult}
-                    className="menu-slider"
-                    onPointerDown={() => (this._batch = UndoManager.StartBatch('presDuration'))}
+                    onPointerDown={() => (this._batch = UndoManager.StartBatch('num slider changing'))}
                     onPointerUp={() => this._batch?.end()}
-                    onChange={e => {
+                    onChange={undoable(e => {
                         e.stopPropagation();
-                        setValue(Number(e.target.value));
-                    }}
+                        numScript(Number(e.target.value));
+                    }, 'set num value')}
                 />
             </div>
         );
@@ -174,20 +172,13 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
      * Number button
      */
     @computed get numberDropdownButton() {
-        const numScript = ScriptCast(this.rootDoc.script);
-        const setValue = (value: number) => UndoManager.RunInBatch(() => numScript?.script.run({ self: this.rootDoc, value, _readOnly_: false }), 'set num value');
-
-        // Script for checking the outcome of the toggle
-        const checkResult = Number(numScript?.script.run({ self: this.rootDoc, value: 0, _readOnly_: true }).result ?? 0).toPrecision(NumCast(this.dataDoc.numPrecision, 3));
+        const numScript = (value?: number) => ScriptCast(this.rootDoc.script)?.script.run({ self: this.rootDoc, value, _readOnly_: value === undefined });
 
-        const label = !FontIconBox.GetShowLabels() ? null : <div className="fontIconBox-label">{this.label}</div>;
+        const checkResult = Number(numScript().result ?? 0).toPrecision(NumCast(this.dataDoc.numPrecision, 3));
 
         const items: number[] = [];
-        for (let i = 0; i < 100; i++) {
-            if (i % 2 === 0) {
-                items.push(i);
-            }
-        }
+        for (let i = 0; i < 100; i += 2) items.push(i);
+
         const list = items.map(value => {
             return (
                 <div
@@ -196,15 +187,15 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
                     style={{
                         backgroundColor: value.toString() === checkResult ? Colors.LIGHT_BLUE : undefined,
                     }}
-                    onClick={() => setValue(value)}>
+                    onClick={undoable(value => numScript(value), `${this.rootDoc.title} button set from list`)}>
                     {value}
                 </div>
             );
         });
         return (
             <div className="menuButton numBtn list">
-                <div className="button" onClick={action(e => setValue(Number(checkResult) - 1))}>
-                    <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={'minus'} />
+                <div className="button" onClick={undoable(e => numScript(Number(checkResult) - 1), `${this.rootDoc.title} decrement value`)}>
+                    <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon="minus" />
                 </div>
                 <div
                     className={`button ${'number'}`}
@@ -217,9 +208,9 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
                         this.noTooltip = this.rootDoc.dropDownOpen;
                         Doc.UnBrushAllDocs();
                     })}>
-                    <input style={{ width: 30 }} className="button-input" type="number" value={checkResult} onChange={undoBatch(action(e => setValue(Number(e.target.value))))} />
+                    <input style={{ width: 30 }} className="button-input" type="number" value={checkResult} readOnly={true} onChange={undoable(e => numScript(Number(e.target.value)), `${this.rootDoc.title} button set value`)} />
                 </div>
-                <div className={`button`} onClick={action(e => setValue(Number(checkResult) + 1))}>
+                <div className={`button`} onClick={undoable(e => numScript(Number(checkResult) + 1), `${this.rootDoc.title} increment value`)}>
                     <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={'plus'} />
                 </div>
 
@@ -322,12 +313,12 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
             .map(value => (
                 <div
                     className="list-item"
-                    key={`${value}`}
+                    key={value}
                     style={{
                         fontFamily: script.script.originalScript.startsWith('{ return setFont') ? value : undefined,
                         backgroundColor: value === text ? Colors.LIGHT_BLUE : undefined,
                     }}
-                    onClick={undoBatch(() => script.script.run({ self: this.rootDoc, value }))}>
+                    onClick={undoable(() => script.script.run({ self: this.rootDoc, value }), value)}>
                     {value[0].toUpperCase() + value.slice(1)}
                 </div>
             ));
@@ -640,29 +631,26 @@ ScriptingGlobals.add(function toggleOverlay(checkResult?: boolean) {
 ScriptingGlobals.add(function showFreeform(attr: 'grid' | 'snaplines' | 'clusters' | 'arrange' | 'viewAll', checkResult?: boolean) {
     const selected = SelectionManager.Docs().lastElement();
     // prettier-ignore
-    const map: Map<'grid' | 'snaplines' | 'clusters' | 'arrange'| 'viewAll', { undo: boolean, checkResult: (doc:Doc) => any; setDoc: (doc:Doc) => void;}> = new Map([
+    const map: Map<'grid' | 'snaplines' | 'clusters' | 'arrange'| 'viewAll', { waitForRender?: boolean, checkResult: (doc:Doc) => any; setDoc: (doc:Doc) => void;}> = new Map([
         ['grid', {
-            undo: false,
             checkResult: (doc:Doc) => doc._freeform_backgroundGrid,
             setDoc: (doc:Doc) => doc._freeform_backgroundGrid = !doc._freeform_backgroundGrid,
         }],
         ['snaplines', {
-            undo: false,
             checkResult: (doc:Doc) => doc._freeform_snapLines,
             setDoc: (doc:Doc) => doc._freeform_snapLines = !doc._freeform_snapLines,
         }],
         ['viewAll', {
-            undo: false,
             checkResult: (doc:Doc) => doc._freeform_fitContentsToBox,
             setDoc: (doc:Doc) => doc._freeform_fitContentsToBox = !doc._freeform_fitContentsToBox,
         }],
         ['clusters', {
-            undo: false,
+            waitForRender: true, // flags that undo batch should terminate after a re-render giving the script the chance to fire
             checkResult: (doc:Doc) => doc._freeform_useClusters,
             setDoc: (doc:Doc) => doc._freeform_useClusters = !doc._freeform_useClusters,
         }],
         ['arrange', {
-            undo: true,
+            waitForRender: true, // flags that undo batch should terminate after a re-render giving the script the chance to fire
             checkResult: (doc:Doc) => doc._autoArrange,
             setDoc: (doc:Doc) => doc._autoArrange = !doc._autoArrange,
         }],
@@ -671,7 +659,7 @@ ScriptingGlobals.add(function showFreeform(attr: 'grid' | 'snaplines' | 'cluster
     if (checkResult) {
         return map.get(attr)?.checkResult(selected) ? Colors.MEDIUM_BLUE : 'transparent';
     }
-    const batch = map.get(attr)?.undo ? UndoManager.StartBatch('set feature') : { end: () => {} };
+    const batch = map.get(attr)?.waitForRender ? UndoManager.StartBatch('set freeform attribute') : { end: () => {} };
     SelectionManager.Docs().map(dv => map.get(attr)?.setDoc(dv));
     setTimeout(() => batch.end(), 100);
 });
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 9c06aa7d8..e85835002 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -301,11 +301,11 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
             this._editorView.updateState(state);
 
             const dataDoc = Doc.IsDelegateField(DocCast(this.layoutDoc.proto), this.fieldKey) ? DocCast(this.layoutDoc.proto) : this.dataDoc;
-            const curText = state.doc.textBetween(0, state.doc.content.size, ' \n');
-            const curTemp = this.layoutDoc.resolvedDataDoc ? Cast(this.layoutDoc[this.fieldKey], RichTextField) : undefined; // the actual text in the text box
-            const curProto = Cast(Cast(dataDoc.proto, Doc, null)?.[this.fieldKey], RichTextField, null); // the default text inherited from a prototype
-            const curLayout = this.rootDoc !== this.layoutDoc ? Cast(this.layoutDoc[this.fieldKey], RichTextField, null) : undefined; // the default text stored in a layout template
-            const json = JSON.stringify(state.toJSON());
+            const newText = state.doc.textBetween(0, state.doc.content.size, ' \n');
+            const newJson = JSON.stringify(state.toJSON());
+            const prevData = Cast(this.layoutDoc[this.fieldKey], RichTextField, null); // the actual text in the text box
+            const prevLayoutData = this.rootDoc !== this.layoutDoc ? Cast(this.layoutDoc[this.fieldKey], RichTextField, null) : undefined; // the default text stored in a layout template
+            const protoData = Cast(Cast(dataDoc.proto, Doc, null)?.[this.fieldKey], RichTextField, null); // the default text inherited from a prototype
             const effectiveAcl = GetEffectiveAcl(dataDoc);
 
             const removeSelection = (json: string | undefined) => (json?.indexOf('"storedMarks"') === -1 ? json?.replace(/"selection":.*/, '') : json?.replace(/"selection":"\"storedMarks\""/, '"storedMarks"'));
@@ -320,29 +320,25 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
                 dataDoc.tags = accumTags.length ? new List<string>(Array.from(new Set<string>(accumTags))) : undefined;
 
                 let unchanged = true;
-                if (this._applyingChange !== this.fieldKey && removeSelection(json) !== removeSelection(curProto?.Data)) {
+                if (this._applyingChange !== this.fieldKey && removeSelection(newJson) !== removeSelection(prevData?.Data)) {
                     this._applyingChange = this.fieldKey;
-                    const textChange = curText !== Cast(dataDoc[this.fieldKey], RichTextField)?.Text;
+                    const textChange = newText !== prevData?.Text;
                     textChange && (dataDoc[this.fieldKey + '_modificationDate'] = new DateField(new Date(Date.now())));
-                    if ((!curTemp && !curProto) || curText || json.includes('dash')) {
+                    if ((!prevData && !protoData) || newText || (!newText && !protoData)) {
                         // if no template, or there's text that didn't come from the layout template, write it to the document. (if this is driven by a template, then this overwrites the template text which is intended)
-                        if (removeSelection(json) !== removeSelection(curLayout?.Data)) {
+                        if (removeSelection(newJson) !== removeSelection(prevLayoutData?.Data)) {
                             const numstring = NumCast(dataDoc[this.fieldKey], null);
-                            if (numstring !== undefined) {
-                                dataDoc[this.fieldKey] = Number(curText);
-                            } else {
-                                dataDoc[this.fieldKey] = new RichTextField(json, curText);
-                            }
+                            dataDoc[this.fieldKey] = numstring !== undefined ? Number(newText) : new RichTextField(newJson, newText);
                             dataDoc[this.fieldKey + '_noTemplate'] = true; //(curTemp?.Text || "") !== curText; // mark the data field as being split from the template if it has been edited
-                            textChange && ScriptCast(this.layoutDoc.onTextChanged, null)?.script.run({ this: this.layoutDoc, self: this.rootDoc, text: curText });
+                            textChange && ScriptCast(this.layoutDoc.onTextChanged, null)?.script.run({ this: this.layoutDoc, self: this.rootDoc, text: newText });
                             unchanged = false;
                         }
                     } else {
                         // if we've deleted all the text in a note driven by a template, then restore the template data
                         dataDoc[this.fieldKey] = undefined;
-                        this._editorView.updateState(EditorState.fromJSON(this.config, JSON.parse((curProto || curTemp).Data)));
+                        this._editorView.updateState(EditorState.fromJSON(this.config, JSON.parse((protoData || prevData).Data)));
                         dataDoc[this.fieldKey + '_noTemplate'] = undefined; // mark the data field as not being split from any template it might have
-                        ScriptCast(this.layoutDoc.onTextChanged, null)?.script.run({ this: this.layoutDoc, self: this.rootDoc, text: curText });
+                        ScriptCast(this.layoutDoc.onTextChanged, null)?.script.run({ this: this.layoutDoc, self: this.rootDoc, text: newText });
                         unchanged = false;
                     }
                     this._applyingChange = '';
@@ -667,7 +663,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
         this.layoutDoc._width = !preview && this.SidebarShown ? NumCast(this.layoutDoc._width) * 2 : Math.max(20, NumCast(this.layoutDoc._width) * prevWidth);
     };
     sidebarDown = (e: React.PointerEvent) => {
-        const batch = UndoManager.StartBatch('sidebar');
+        const batch = UndoManager.StartBatch('toggle sidebar');
         setupMoveUpEvents(
             this,
             e,
@@ -694,13 +690,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
         return false;
     };
 
-    @undoBatch
     deleteAnnotation = (anchor: Doc) => {
+        const batch = UndoManager.StartBatch('delete link');
         LinkManager.Instance.deleteLink(LinkManager.Links(anchor)[0]);
         // const docAnnotations = DocListCast(this.props.dataDoc[this.fieldKey]);
         // this.props.dataDoc[this.fieldKey] = new List<Doc>(docAnnotations.filter(a => a !== this.annoTextRegion));
         // AnchorMenu.Instance.fadeOut(true);
         this.props.select(false);
+        setTimeout(batch.end); // wait for reaction to remove link from document
     };
 
     @undoBatch
@@ -733,17 +730,22 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
                 .split(' ')
                 .filter(h => h);
             const anchorDoc = Array.from(hrefs).lastElement().replace(Doc.localServerPath(), '').split('?')[0];
+            const deleteMarkups = undoBatch(() => {
+                const sel = editor.state.selection;
+                editor.dispatch(editor.state.tr.removeMark(sel.from, sel.to, editor.state.schema.marks.linkAnchor));
+            });
             e.persist();
             anchorDoc &&
                 DocServer.GetRefField(anchorDoc).then(
                     action(anchor => {
+                        anchor && SelectionManager.SelectSchemaViewDoc(anchor as Doc);
                         AnchorMenu.Instance.Status = 'annotation';
-                        AnchorMenu.Instance.Delete = () => this.deleteAnnotation(anchor as Doc);
+                        AnchorMenu.Instance.Delete = !anchor && editor.state.selection.empty ? returnFalse : !anchor ? deleteMarkups : () => this.deleteAnnotation(anchor as Doc);
                         AnchorMenu.Instance.Pinned = false;
-                        AnchorMenu.Instance.PinToPres = () => this.pinToPres(anchor as Doc);
-                        AnchorMenu.Instance.MakeTargetToggle = () => this.makeTargetToggle(anchor as Doc);
-                        AnchorMenu.Instance.ShowTargetTrail = () => this.showTargetTrail(anchor as Doc);
-                        AnchorMenu.Instance.IsTargetToggler = () => this.isTargetToggler(anchor as Doc);
+                        AnchorMenu.Instance.PinToPres = !anchor ? returnFalse : () => this.pinToPres(anchor as Doc);
+                        AnchorMenu.Instance.MakeTargetToggle = !anchor ? returnFalse : () => this.makeTargetToggle(anchor as Doc);
+                        AnchorMenu.Instance.ShowTargetTrail = !anchor ? returnFalse : () => this.showTargetTrail(anchor as Doc);
+                        AnchorMenu.Instance.IsTargetToggler = !anchor ? returnFalse : () => this.isTargetToggler(anchor as Doc);
                         AnchorMenu.Instance.jumpTo(e.clientX, e.clientY, true);
                     })
                 );
@@ -1002,7 +1004,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
                     anchorDoc ??
                     Docs.Create.TextConfigDocument({
                         //
-                        title: '#' + this._editorView?.state.doc.textBetween(sel.from, sel.to),
+                        title: 'text(' + this._editorView?.state.doc.textBetween(sel.from, sel.to) + ')',
                         annotationOn: this.dataDoc,
                     });
                 const href = targetHref ?? Doc.localServerPath(anchor);
@@ -1480,11 +1482,11 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
         }
 
         const selectOnLoad = this.rootDoc[Id] === FormattedTextBox.SelectOnLoad && (!LightboxView.LightboxDoc || LightboxView.IsLightboxDocView(this.props.docViewPath()));
-        if (selectOnLoad && !this.props.dontRegisterView && !this.props.dontSelectOnLoad && this.isActiveTab(this.ProseRef)) {
+        if (this._editorView && selectOnLoad && !this.props.dontRegisterView && !this.props.dontSelectOnLoad && this.isActiveTab(this.ProseRef)) {
             const selLoadChar = FormattedTextBox.SelectOnLoadChar;
             FormattedTextBox.SelectOnLoad = '';
             this.props.select(false);
-            if (selLoadChar && this._editorView) {
+            if (selLoadChar) {
                 const $from = this._editorView.state.selection.anchor ? this._editorView.state.doc.resolve(this._editorView.state.selection.anchor - 1) : undefined;
                 const mark = schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.floor(Date.now() / 1000) });
                 const curMarks = this._editorView.state.storedMarks ?? $from?.marksAcross(this._editorView.state.selection.$head) ?? [];
@@ -1494,10 +1496,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
                     .insertText(FormattedTextBox.SelectOnLoadChar, this._editorView.state.doc.content.size - 1, this._editorView.state.doc.content.size)
                     .setStoredMarks(storedMarks);
                 this._editorView.dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(tr.doc.content.size))));
-            } else if (this._editorView && curText && !FormattedTextBox.DontSelectInitialText) {
+            } else if (curText && !FormattedTextBox.DontSelectInitialText) {
                 selectAll(this._editorView.state, this._editorView?.dispatch);
-                this.startUndoTypingBatch();
-            } else if (this._editorView) {
+            } else {
                 this._editorView.dispatch(this._editorView.state.tr.addStoredMark(schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.floor(Date.now() / 1000) })));
             }
         }
@@ -1506,7 +1507,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
         if (this._editorView) {
             const tr = this._editorView.state.tr;
             const { from, to } = tr.selection;
-            // for some reason, the selection is sometimes lost in the sidebar view when prosemirror syncs the seledtion with the dom, so reset the selectoin after the document has ben fully instantiated.
+            // for some reason, the selection is sometimes lost in the sidebar view when prosemirror syncs the seledtion with the dom, so reset the selection after the document has ben fully instantiated.
             if (FormattedTextBox.DontSelectInitialText) setTimeout(() => this._editorView?.dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(from), tr.doc.resolve(to)))), 250);
             this._editorView.state.storedMarks = [
                 ...(this._editorView.state.storedMarks ?? []),
@@ -1630,7 +1631,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
     onFocused = (e: React.FocusEvent): void => {
         //applyDevTools.applyDevTools(this._editorView);
         this.ProseRef?.children[0] === e.nativeEvent.target && this._editorView && RichTextMenu.Instance?.updateMenu(this._editorView, undefined, this.props);
-        this.startUndoTypingBatch();
+        e.stopPropagation();
     };
 
     onClick = (e: React.MouseEvent): void => {
@@ -1705,13 +1706,11 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
         }
     }
     startUndoTypingBatch() {
-        !this._undoTyping && (this._undoTyping = UndoManager.StartBatch('undoTyping'));
+        !this._undoTyping && (this._undoTyping = UndoManager.StartBatch('text edits on ' + this.rootDoc.title));
     }
     public endUndoTypingBatch() {
-        const wasUndoing = this._undoTyping;
         this._undoTyping?.end();
         this._undoTyping = undefined;
-        return wasUndoing;
     }
 
     @action
diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx
index 3d1d11141..3eadc3047 100644
--- a/src/client/views/nodes/trails/PresBox.tsx
+++ b/src/client/views/nodes/trails/PresBox.tsx
@@ -1428,6 +1428,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                 min={min}
                 max={max}
                 value={value}
+                readOnly={true}
                 style={{ marginLeft: hmargin, marginRight: hmargin, width: `calc(100% - ${2 * (hmargin ?? 0)}px)` }}
                 className={`toolbar-slider ${active ? '' : 'none'}`}
                 onPointerDown={e => {
@@ -1501,7 +1502,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                             <div className="ribbon-doubleButton">
                                 <div className="presBox-subheading">Slide Duration</div>
                                 <div className="ribbon-property">
-                                    <input className="presBox-input" type="number" value={duration} onKeyDown={e => e.stopPropagation()} onChange={e => this.updateDurationTime(e.target.value)} /> s
+                                    <input className="presBox-input" type="number" readOnly={true} value={duration} onKeyDown={e => e.stopPropagation()} onChange={e => this.updateDurationTime(e.target.value)} /> s
                                 </div>
                                 <div className="ribbon-propertyUpDown">
                                     <div className="ribbon-propertyUpDownItem" onClick={() => this.updateDurationTime(String(duration), 1000)}>
@@ -1654,7 +1655,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                         <div className="ribbon-doubleButton" style={{ display: activeItem.presMovement === PresMovement.Zoom ? 'inline-flex' : 'none' }}>
                             <div className="presBox-subheading">Zoom (% screen filled)</div>
                             <div className="ribbon-property">
-                                <input className="presBox-input" type="number" value={zoom} onChange={e => this.updateZoom(e.target.value)} />%
+                                <input className="presBox-input" type="number" readOnly={true} value={zoom} onChange={e => this.updateZoom(e.target.value)} />%
                             </div>
                             <div className="ribbon-propertyUpDown">
                                 <div className="ribbon-propertyUpDownItem" onClick={() => this.updateZoom(String(zoom), 0.1)}>
@@ -1669,7 +1670,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                         <div className="ribbon-doubleButton" style={{ display: 'inline-flex' }}>
                             <div className="presBox-subheading">Transition Time</div>
                             <div className="ribbon-property">
-                                <input className="presBox-input" type="number" value={transitionSpeed} onKeyDown={e => e.stopPropagation()} onChange={action(e => this.updateTransitionTime(e.target.value))} /> s
+                                <input className="presBox-input" type="number" readOnly={true} value={transitionSpeed} onKeyDown={e => e.stopPropagation()} onChange={action(e => this.updateTransitionTime(e.target.value))} /> s
                             </div>
                             <div className="ribbon-propertyUpDown">
                                 <div className="ribbon-propertyUpDownItem" onClick={() => this.updateTransitionTime(String(transitionSpeed), 1000)}>
@@ -1756,6 +1757,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                                             className="presBox-input"
                                             style={{ textAlign: 'center', width: '100%', height: 15, fontSize: 10 }}
                                             type="number"
+                                            readOnly={true}
                                             value={NumCast(activeItem.presStartTime).toFixed(2)}
                                             onKeyDown={e => e.stopPropagation()}
                                             onChange={action((e: React.ChangeEvent<HTMLInputElement>) => {
@@ -1782,6 +1784,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                                             onKeyDown={e => e.stopPropagation()}
                                             style={{ textAlign: 'center', width: '100%', height: 15, fontSize: 10 }}
                                             type="number"
+                                            readOnly={true}
                                             value={NumCast(activeItem.presEndTime).toFixed(2)}
                                             onChange={action((e: React.ChangeEvent<HTMLInputElement>) => {
                                                 activeItem.presEndTime = Number(e.target.value);
diff --git a/src/client/views/nodes/trails/PresElementBox.tsx b/src/client/views/nodes/trails/PresElementBox.tsx
index 4eb6aee25..2279ffe84 100644
--- a/src/client/views/nodes/trails/PresElementBox.tsx
+++ b/src/client/views/nodes/trails/PresElementBox.tsx
@@ -1,6 +1,6 @@
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { Tooltip } from '@material-ui/core';
-import { action, computed, IReactionDisposer, observable, reaction } from 'mobx';
+import { action, computed, IReactionDisposer, observable, reaction, runInAction } from 'mobx';
 import { observer } from 'mobx-react';
 import { Doc, DocListCast, HeightSym, Opt, WidthSym } from '../../../../fields/Doc';
 import { Id } from '../../../../fields/FieldSymbols';
@@ -13,7 +13,7 @@ import { DocumentManager } from '../../../util/DocumentManager';
 import { DragManager } from '../../../util/DragManager';
 import { SettingsManager } from '../../../util/SettingsManager';
 import { Transform } from '../../../util/Transform';
-import { undoBatch } from '../../../util/UndoManager';
+import { undoable, undoBatch } from '../../../util/UndoManager';
 import { ViewBoxBaseComponent } from '../../DocComponent';
 import { EditableView } from '../../EditableView';
 import { Colors } from '../../global/globalEnums';
@@ -263,16 +263,15 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
         }
     };
 
-    @undoBatch
-    removeItem = action((e: React.MouseEvent) => {
+    removePresentationItem = undoable((e: React.MouseEvent) => {
         e.stopPropagation();
         if (this.presBox && this.indexInPres < (this.presBoxView?.itemIndex || 0)) {
-            this.presBox.itemIndex = (this.presBoxView?.itemIndex || 0) - 1;
+            runInAction(() => (this.presBox!.itemIndex = (this.presBoxView?.itemIndex || 0) - 1));
         }
         this.props.removeDocument?.(this.rootDoc);
         this.presBoxView?.removeFromSelectedArray(this.rootDoc);
         this.removeAllRecordingInOverlay();
-    });
+    }, 'Remove doc from pres trail');
 
     // set the value/title of the individual pres element
     @undoBatch
@@ -476,7 +475,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
         );
         items.push(
             <Tooltip key="trash" title={<div className="dash-tooltip">Remove from presentation</div>}>
-                <div className={'slideButton'} onClick={this.removeItem}>
+                <div className={'slideButton'} onClick={this.removePresentationItem}>
                     <FontAwesomeIcon icon={'trash'} onPointerDown={e => e.stopPropagation()} />
                 </div>
             </Tooltip>
diff --git a/src/client/views/pdf/AnchorMenu.tsx b/src/client/views/pdf/AnchorMenu.tsx
index d6dddf71a..5480600b0 100644
--- a/src/client/views/pdf/AnchorMenu.tsx
+++ b/src/client/views/pdf/AnchorMenu.tsx
@@ -395,22 +395,25 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
             ) : (
                 <>
                     <Tooltip key="trash" title={<div className="dash-tooltip">Remove Link Anchor</div>}>
-                        <button className="antimodeMenu-button" onPointerDown={this.Delete}>
+                        <button className="antimodeMenu-button" style={{ display: this.Delete === returnFalse ? 'none' : undefined }} onPointerDown={this.Delete}>
                             <FontAwesomeIcon icon="trash-alt" size="lg" />
                         </button>
                     </Tooltip>
                     <Tooltip key="Pin" title={<div className="dash-tooltip">Pin to Presentation</div>}>
-                        <button className="antimodeMenu-button" onPointerDown={this.PinToPres}>
+                        <button className="antimodeMenu-button" style={{ display: this.PinToPres === returnFalse ? 'none' : undefined }} onPointerDown={this.PinToPres}>
                             <FontAwesomeIcon icon="map-pin" size="lg" />
                         </button>
                     </Tooltip>
                     <Tooltip key="trail" title={<div className="dash-tooltip">Show Linked Trail</div>}>
-                        <button className="antimodeMenu-button" onPointerDown={this.ShowTargetTrail}>
+                        <button className="antimodeMenu-button" style={{ display: this.ShowTargetTrail === returnFalse ? 'none' : undefined }} onPointerDown={this.ShowTargetTrail}>
                             <FontAwesomeIcon icon="taxi" size="lg" />
                         </button>
                     </Tooltip>
                     <Tooltip key="toggle" title={<div className="dash-tooltip">make target visibility toggle on click</div>}>
-                        <button className="antimodeMenu-button" style={{ color: this.IsTargetToggler() ? 'black' : 'white', backgroundColor: this.IsTargetToggler() ? 'white' : 'black' }} onPointerDown={this.MakeTargetToggle}>
+                        <button
+                            className="antimodeMenu-button"
+                            style={{ display: this.IsTargetToggler === returnFalse ? 'none' : undefined, color: this.IsTargetToggler() ? 'black' : 'white', backgroundColor: this.IsTargetToggler() ? 'white' : 'black' }}
+                            onPointerDown={this.MakeTargetToggle}>
                             <FontAwesomeIcon icon="thumbtack" size="lg" />
                         </button>
                     </Tooltip>
diff --git a/src/client/views/search/SearchBox.tsx b/src/client/views/search/SearchBox.tsx
index 1c1b41f73..3479cd20f 100644
--- a/src/client/views/search/SearchBox.tsx
+++ b/src/client/views/search/SearchBox.tsx
@@ -141,6 +141,8 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() {
                 const annos = !Field.toString(Doc.LayoutField(d) as Field).includes('CollectionView');
                 const data = d[annos ? fieldKey + '_annotations' : fieldKey];
                 data && newarray.push(...DocListCast(data));
+                const sidebar = d[fieldKey + '_sidebar'];
+                sidebar && newarray.push(...DocListCast(sidebar));
                 func(depth, d);
             });
             docs = newarray;
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index 5312da009..d8447deb6 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -236,7 +236,7 @@ export class Doc extends RefField {
         if (
             doc &&
             Doc.MyFileOrphans instanceof Doc &&
-            Doc.IsDocDataProto(doc) &&
+            Doc.IsDataProto(doc) &&
             !Doc.IsSystem(doc) &&
             ![DocumentType.CONFIG, DocumentType.KVP, DocumentType.LINK, DocumentType.LINKANCHOR].includes(doc.type as any) &&
             !doc.isFolder &&
@@ -510,7 +510,7 @@ export namespace Doc {
     export function GetT<T extends Field>(doc: Doc, key: string, ctor: ToConstructor<T>, ignoreProto: boolean = false): FieldResult<T> {
         return Cast(Get(doc, key, ignoreProto), ctor) as FieldResult<T>;
     }
-    export function IsDocDataProto(doc: Doc) {
+    export function IsDataProto(doc: Doc) {
         return GetT(doc, 'isDataDoc', 'boolean', true);
     }
     export function IsBaseProto(doc: Doc) {
@@ -981,7 +981,7 @@ export namespace Doc {
     export function MakeCopy(doc: Doc, copyProto: boolean = false, copyProtoId?: string, retitle = false): Doc {
         const copy = new Doc(copyProtoId, true);
         updateCachedAcls(copy);
-        const exclude = [...Cast(doc.cloneFieldFilter, listSpec('string'), []), 'dragFactory_count', 'cloneFieldFilter'];
+        const exclude = [...StrListCast(doc.cloneFieldFilter), 'dragFactory_count', 'cloneFieldFilter'];
         Object.keys(doc).forEach(key => {
             if (exclude.includes(key)) return;
             const cfield = ComputedField.WithoutComputed(() => FieldValue(doc[key]));
@@ -1433,7 +1433,7 @@ export namespace Doc {
     export function setDocFilter(container: Opt<Doc>, key: string, value: any, modifiers: 'remove' | 'match' | 'check' | 'x' | 'exists' | 'unset', toggle?: boolean, fieldPrefix?: string, append: boolean = true) {
         if (!container) return;
         const filterField = '_' + (fieldPrefix ? fieldPrefix + '_' : '') + 'docFilters';
-        const docFilters = Cast(container[filterField], listSpec('string'), []);
+        const docFilters = StrListCast(container[filterField]);
         runInAction(() => {
             for (let i = 0; i < docFilters.length; i++) {
                 const fields = docFilters[i].split(':'); // split key:value:modifier
@@ -1745,6 +1745,16 @@ ScriptingGlobals.add(function undo() {
     SelectionManager.DeselectAll();
     return UndoManager.Undo();
 });
+
+export function ShowUndoStack() {
+    SelectionManager.DeselectAll();
+    var buffer = '';
+    UndoManager.undoStack.forEach((batch, i) => {
+        buffer += 'Batch => ' + UndoManager.undoStackNames[i] + '\n';
+        ///batch.forEach(undo => (buffer += '   ' + undo.prop + '\n'));
+    });
+    alert(buffer);
+}
 ScriptingGlobals.add(function redo() {
     SelectionManager.DeselectAll();
     return UndoManager.Redo();
diff --git a/src/fields/util.ts b/src/fields/util.ts
index f4fd3200c..a2b445d6c 100644
--- a/src/fields/util.ts
+++ b/src/fields/util.ts
@@ -65,6 +65,7 @@ const _setterImpl = action(function (target: any, prop: string | symbol | number
     if (value instanceof RefField) {
         value = new ProxyField(value);
     }
+
     if (value instanceof ObjectField) {
         if (value[Parent] && value[Parent] !== receiver && !(value instanceof PrefetchProxy)) {
             throw new Error("Can't put the same object in multiple documents at the same time");
@@ -102,20 +103,24 @@ const _setterImpl = action(function (target: any, prop: string | symbol | number
             DocServer.registerDocWithCachedUpdate(receiver, prop as string, curValue);
         }
         !receiver[Initializing] &&
+            !receiver.dontUndo &&
             (!receiver[UpdatingFromServer] || receiver[ForceServerWrite]) &&
-            UndoManager.AddEvent({
-                redo: () => (receiver[prop] = value),
-                undo: () => {
-                    const wasUpdate = receiver[UpdatingFromServer];
-                    const wasForce = receiver[ForceServerWrite];
-                    receiver[ForceServerWrite] = true; // needed since writes aren't propagated to server if UpdatingFromServerIsSet
-                    receiver[UpdatingFromServer] = true; // needed if the event caused ACL's to change such that the doc is otherwise no longer editable.
-                    receiver[prop] = curValue;
-                    receiver[ForceServerWrite] = wasForce;
-                    receiver[UpdatingFromServer] = wasUpdate;
+            UndoManager.AddEvent(
+                {
+                    redo: () => (receiver[prop] = value),
+                    undo: () => {
+                        const wasUpdate = receiver[UpdatingFromServer];
+                        const wasForce = receiver[ForceServerWrite];
+                        receiver[ForceServerWrite] = true; // needed since writes aren't propagated to server if UpdatingFromServerIsSet
+                        receiver[UpdatingFromServer] = true; // needed if the event caused ACL's to change such that the doc is otherwise no longer editable.
+                        receiver[prop] = curValue;
+                        receiver[ForceServerWrite] = wasForce;
+                        receiver[UpdatingFromServer] = wasUpdate;
+                    },
+                    prop: prop?.toString(),
                 },
-                prop: prop?.toString(),
-            });
+                value
+            );
         return true;
     }
     return false;
@@ -390,6 +395,7 @@ export function updateFunction(target: any, prop: any, value: any, receiver: any
                     diff?.op === '$addToSet'
                         ? {
                               redo: () => {
+                                  console.log('redo $add: ' + prop, diff.items); // bcz: uncomment to log undo
                                   receiver[prop].push(...diff.items.map((item: any) => item.value ?? item));
                                   lastValue = ObjectField.MakeCopy(receiver[prop]);
                               },
@@ -406,11 +412,12 @@ export function updateFunction(target: any, prop: any, value: any, receiver: any
                                   });
                                   lastValue = ObjectField.MakeCopy(receiver[prop]);
                               }),
-                              prop: '',
+                              prop: 'add ' + diff.items.length + ' items to list',
                           }
                         : diff?.op === '$remFromSet'
                         ? {
                               redo: action(() => {
+                                  console.log('redo $rem: ' + prop, diff.items); // bcz: uncomment to log undo
                                   diff.items.forEach((item: any) => {
                                       const ind = item instanceof SchemaHeaderField ? receiver[prop].findIndex((ele: any) => ele instanceof SchemaHeaderField && ele.heading === item.heading) : receiver[prop].indexOf(item.value ?? item);
                                       ind !== -1 && receiver[prop].splice(ind, 1);
@@ -430,10 +437,11 @@ export function updateFunction(target: any, prop: any, value: any, receiver: any
                                   });
                                   lastValue = ObjectField.MakeCopy(receiver[prop]);
                               },
-                              prop: '',
+                              prop: 'remove ' + diff.items.length + ' items from list',
                           }
                         : {
                               redo: () => {
+                                  console.log('redo list: ' + prop, receiver[prop]); // bcz: uncomment to log undo
                                   receiver[prop] = ObjectField.MakeCopy(newValue as List<any>);
                                   lastValue = ObjectField.MakeCopy(receiver[prop]);
                               },
@@ -442,7 +450,7 @@ export function updateFunction(target: any, prop: any, value: any, receiver: any
                                   receiver[prop] = ObjectField.MakeCopy(prevValue as List<any>);
                                   lastValue = ObjectField.MakeCopy(receiver[prop]);
                               },
-                              prop: '',
+                              prop: 'assign list',
                           }
                 );
         }
-- 
cgit v1.2.3-70-g09d2


From d3ecf7bdacd925fcd293a300c53206b12bee8ce9 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Tue, 16 May 2023 20:41:37 -0400
Subject: cleaned up UndoStack a bit to be available with the undo/redo buttons
 (using Long Press).

---
 src/client/documents/Documents.ts                  |  2 +-
 src/client/util/CurrentUserUtils.ts                | 44 +++++++++++-----------
 src/client/views/UndoStack.scss                    | 29 ++++++++++++++
 src/client/views/UndoStack.tsx                     | 35 ++++++++++++-----
 .../views/collections/CollectionDockingView.tsx    |  9 ++++-
 .../collectionLinear/CollectionLinearView.tsx      | 11 +++++-
 src/client/views/nodes/DocumentView.tsx            |  2 +-
 7 files changed, 96 insertions(+), 36 deletions(-)
 create mode 100644 src/client/views/UndoStack.scss

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

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 795d60b48..5aa69e499 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -355,7 +355,7 @@ export class DocumentOptions {
     cloneFieldFilter?: List<string>; // fields not to copy when the document is clonedclipboard?: Doc;
     filterBoolean?: string;
     data_useCors?: boolean;
-    icon?: string;
+    icon?: string; // icon used by fonticonbox to render button
     target?: Doc; // available for use in scripts as the primary target document
     sourcePanel?: Doc; // panel to display in 'targetContainer' as the result of a button onClick script
     targetContainer?: Doc; // document whose proto will be set to 'panel' as the result of a onClick click script
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index b0a2c7d60..85ab66301 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -289,23 +289,24 @@ export class CurrentUserUtils {
         emptyThings.forEach(thing => DocUtils.AssignDocField(doc, "empty"+thing.key, (opts) => thing.creator(opts), {...standardOps(thing.key), ...thing.opts}, undefined, undefined, thing.funcs));
         
         return [
-            { toolTip: "Tap or drag to create a note",            title: "Note", icon: "sticky-note",   dragFactory: doc.emptyNote as Doc,      clickFactory: DocCast(doc.emptyNote)},
-            { toolTip: "Tap or drag to create a flashcard",       title: "Flashcard", icon: "id-card",  dragFactory: doc.emptyFlashcard as Doc, clickFactory: DocCast(doc.emptyFlashcard)},
-            { toolTip: "Tap or drag to create an equation",       title: "Math", icon: "calculator",    dragFactory: doc.emptyEquation as Doc,  clickFactory: DocCast(doc.emptyEquation)},
-            { toolTip: "Tap or drag to create a note board",      title: "Notes", icon: "folder",       dragFactory: doc.emptyNoteboard as Doc, clickFactory: DocCast(doc.emptyNoteboard)},
-            { toolTip: "Tap or drag to create a collection",      title: "Col", icon: "folder",         dragFactory: doc.emptyCollection as Doc,clickFactory: DocCast(doc.emptyTab)},
-            { toolTip: "Tap or drag to create a webpage",         title: "Web", icon: "globe-asia",     dragFactory: doc.emptyWebpage as Doc,   clickFactory: DocCast(doc.emptyWebpage)},
-            { toolTip: "Tap or drag to create a comparison box",  title: "Compare", icon: "columns",    dragFactory: doc.emptyComparison as Doc,clickFactory: DocCast(doc.emptyComparison)},
-            { toolTip: "Tap or drag to create an audio recorder", title: "Audio", icon: "microphone",   dragFactory: doc.emptyAudio as Doc,     clickFactory: DocCast(doc.emptyAudio),      openFactoryLocation: OpenWhere.overlay},
-            { toolTip: "Tap or drag to create a map",             title: "Map", icon: "map-marker-alt", dragFactory: doc.emptyMap as Doc,       clickFactory: DocCast(doc.emptyMap)},
-            { toolTip: "Tap or drag to create a screen grabber",  title: "Grab", icon: "photo-video",   dragFactory: doc.emptyScreengrab as Doc,clickFactory: DocCast(doc.emptyScreengrab), openFactoryLocation: OpenWhere.overlay},
-            { toolTip: "Tap or drag to create a WebCam recorder", title: "WebCam", icon: "photo-video", dragFactory: doc.emptyWebCam as Doc,    clickFactory: DocCast(doc.emptyWebCam),     openFactoryLocation: OpenWhere.overlay},
-            { toolTip: "Tap or drag to create a button",          title: "Button", icon: "bolt",        dragFactory: doc.emptyButton as Doc,    clickFactory: DocCast(doc.emptyButton)},
-            { toolTip: "Tap or drag to create a scripting box",   title: "Script", icon: "terminal",    dragFactory: doc.emptyScript as Doc,    clickFactory: DocCast(doc.emptyScript)},
-            { toolTip: "Tap or drag to create a data viz node",   title: "DataViz", icon: "file",       dragFactory: doc.emptyDataViz as Doc,   clickFactory: DocCast(doc.emptyDataViz)},
-            { toolTip: "Tap or drag to create a bullet slide",    title: "PPT Slide", icon: "file",     dragFactory: doc.emptySlide as Doc,     clickFactory: DocCast(doc.emptySlide),      openFactoryLocation: OpenWhere.overlay},
-            { toolTip: "Tap or drag to create a data note", title: "DataNote", icon: "window-maximize", dragFactory: doc.emptyHeader as Doc,    clickFactory: DocCast(doc.emptyHeader),     openFactoryAsDelegate: true },
-            { toolTip: "Toggle a Calculator REPL",                title: "repl", icon: "calculator",                                            clickFactory: 'repl' as any,                openFactoryLocation: OpenWhere.overlay},
+            { toolTip: "Tap or drag to create a note",            title: "Note", icon: "sticky-note",   dragFactory: doc.emptyNote as Doc,       clickFactory: DocCast(doc.emptyNote)},
+            { toolTip: "Tap or drag to create a flashcard",       title: "Flashcard", icon: "id-card",  dragFactory: doc.emptyFlashcard as Doc,  clickFactory: DocCast(doc.emptyFlashcard)},
+            { toolTip: "Tap or drag to create an equation",       title: "Math", icon: "calculator",    dragFactory: doc.emptyEquation as Doc,   clickFactory: DocCast(doc.emptyEquation)},
+            { toolTip: "Tap or drag to create a note board",      title: "Notes", icon: "folder",       dragFactory: doc.emptyNoteboard as Doc,  clickFactory: DocCast(doc.emptyNoteboard)},
+            { toolTip: "Tap or drag to create a collection",      title: "Col", icon: "folder",         dragFactory: doc.emptyCollection as Doc, clickFactory: DocCast(doc.emptyTab)},
+            { toolTip: "Tap or drag to create a webpage",         title: "Web", icon: "globe-asia",     dragFactory: doc.emptyWebpage as Doc,    clickFactory: DocCast(doc.emptyWebpage)},
+            { toolTip: "Tap or drag to create a comparison box",  title: "Compare", icon: "columns",    dragFactory: doc.emptyComparison as Doc, clickFactory: DocCast(doc.emptyComparison)},
+            { toolTip: "Tap or drag to create an audio recorder", title: "Audio", icon: "microphone",   dragFactory: doc.emptyAudio as Doc,      clickFactory: DocCast(doc.emptyAudio),      openFactoryLocation: OpenWhere.overlay},
+            { toolTip: "Tap or drag to create a map",             title: "Map", icon: "map-marker-alt", dragFactory: doc.emptyMap as Doc,        clickFactory: DocCast(doc.emptyMap)},
+            { toolTip: "Tap or drag to create a screen grabber",  title: "Grab", icon: "photo-video",   dragFactory: doc.emptyScreengrab as Doc, clickFactory: DocCast(doc.emptyScreengrab), openFactoryLocation: OpenWhere.overlay},
+            { toolTip: "Tap or drag to create a WebCam recorder", title: "WebCam", icon: "photo-video", dragFactory: doc.emptyWebCam as Doc,     clickFactory: DocCast(doc.emptyWebCam),     openFactoryLocation: OpenWhere.overlay},
+            { toolTip: "Tap or drag to create a button",          title: "Button", icon: "bolt",        dragFactory: doc.emptyButton as Doc,     clickFactory: DocCast(doc.emptyButton)},
+            { toolTip: "Tap or drag to create a scripting box",   title: "Script", icon: "terminal",    dragFactory: doc.emptyScript as Doc,     clickFactory: DocCast(doc.emptyScript)},
+            { toolTip: "Tap or drag to create a data viz node",   title: "DataViz", icon: "file",       dragFactory: doc.emptyDataViz as Doc,    clickFactory: DocCast(doc.emptyDataViz)},
+            { toolTip: "Tap or drag to create a bullet slide",    title: "PPT Slide", icon: "file",     dragFactory: doc.emptySlide as Doc,      clickFactory: DocCast(doc.emptySlide),      openFactoryLocation: OpenWhere.overlay},
+            { toolTip: "Tap or drag to create a data note",       title: "DataNote", icon: "window-maximize",dragFactory: doc.emptyHeader as Doc,clickFactory: DocCast(doc.emptyHeader),     openFactoryAsDelegate: true },
+            { toolTip: "Toggle a Calculator REPL",                title: "replviewer",  icon: "calculator",                                      clickFactory: '<ScriptingRepl />' as any,   openFactoryLocation: OpenWhere.overlay}, // hack: clickFactory is not a Doc but will get interpreted as a custom UI by the openDoc() onClick script
+            { toolTip: "Toggle an UndoStack",                     title: "undostacker", icon: "calculator",                                      clickFactory: "<UndoStack>" as any,         openFactoryLocation: OpenWhere.overlay},
           ].map(tuple => (
               { openFactoryLocation: OpenWhere.addRight, 
                 scripts: { onClick:     'openDoc(copyDragFactory(this.clickFactory,this.openFactoryAsDelegate), this.openFactoryLocation)', 
@@ -598,10 +599,11 @@ export class CurrentUserUtils {
                             CurrentUserUtils.createToolButton(opts), scripts, funcs);
         
         const btnDescs = [// setup reactions to change the highlights on the undo/redo buttons -- would be better to encode this in the undo/redo buttons, but the undo/redo stacks are not wired up that way yet
-            { scripts: { onClick: "undo()"},      opts: { title: "undo", icon: "undo-alt", toolTip: "Click to undo" }},
-            { scripts: { onClick: "redo()"},      opts: { title: "redo", icon: "redo-alt", toolTip: "Click to redo" }},
-            { scripts: { },                       opts: { title: "linker", icon: "linkui", toolTip: "link started"}},
-            { scripts: { },                       opts: { title: "currently playing", icon: "currentlyplayingui", toolTip: "currently playing media"}},
+            { scripts: { onClick: "undo()"},      opts: { title: "undo",              icon: "undo-alt",               toolTip: "Click to undo" }},
+            { scripts: { onClick: "redo()"},      opts: { title: "redo",              icon: "redo-alt",               toolTip: "Click to redo" }},
+            { scripts: { },                       opts: { title: "undoStack",         layout: "<UndoStack>",          toolTip: "Undo/Redo Stack"}}, // note: layout fields are hacks -- they don't actually run through the JSX parser (yet)
+            { scripts: { },                       opts: { title: "linker",            layout: "<LinkingUI>",          toolTip: "link started"}},
+            { scripts: { },                       opts: { title: "currently playing", layout: "<CurrentlyPlayingUI>", toolTip: "currently playing media"}},
         ];
         const btns = btnDescs.map(desc => dockBtn({_width: 30, _height: 30, defaultDoubleClick: 'ignore', dontUndo: true, _stayInCollection: true, ...desc.opts}, desc.scripts));
         const dockBtnsReqdOpts:DocumentOptions = {
diff --git a/src/client/views/UndoStack.scss b/src/client/views/UndoStack.scss
new file mode 100644
index 000000000..ab21e6d7e
--- /dev/null
+++ b/src/client/views/UndoStack.scss
@@ -0,0 +1,29 @@
+.undoStack-outerContainer {
+    height: 100%;
+    display: flex;
+    flex-direction: column;
+    position: relative;
+    pointer-events: all;
+    padding-left: 4px;
+}
+
+.undoStack-resultContainer {
+    border-radius: 5px;
+}
+
+.undoStack-commandInput {
+    width: 100%;
+}
+
+.undoStack-commandResult,
+.undoStack-commandString {
+    overflow-wrap: break-word;
+}
+
+.undoStack-commandsContainer {
+    background-color: whitesmoke;
+    flex: 1 1 auto;
+    overflow-y: scroll;
+    height: 30px;
+    border-radius: 5px;
+}
diff --git a/src/client/views/UndoStack.tsx b/src/client/views/UndoStack.tsx
index 01e184d6b..f5af09e5b 100644
--- a/src/client/views/UndoStack.tsx
+++ b/src/client/views/UndoStack.tsx
@@ -1,24 +1,41 @@
+import { action, observable } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
 import { UndoManager } from '../util/UndoManager';
-import './ScriptingRepl.scss';
+import './UndoStack.scss';
 
+interface UndoStackProps {
+    width?: number;
+    height?: number;
+    inline?: boolean;
+}
 @observer
-export class UndoStack extends React.Component {
+export class UndoStack extends React.Component<UndoStackProps> {
+    @observable static HideInline: boolean;
+    @observable static Expand: boolean;
     render() {
-        return (
-            <div className="scriptingRepl-outerContainer">
-                <div className="scriptingRepl-commandsContainer" ref={r => r?.scroll({ behavior: 'auto', top: r?.scrollHeight + 20 })} style={{ background: UndoManager.batchCounter.get() ? 'yellow' : undefined }}>
+        return this.props.inline && UndoStack.HideInline ? null : (
+            <div
+                className="undoStack-outerContainer"
+                style={{ width: this.props.width, height: this.props.height ? (UndoStack.Expand ? 4 : 1) * this.props.height : undefined, top: UndoStack.Expand && this.props.height ? -this.props.height * 3 : undefined }}
+                onClick={action(e => (UndoStack.Expand = !UndoStack.Expand))}
+                onDoubleClick={action(e => (UndoStack.Expand = UndoStack.HideInline = false))}>
+                <div className="undoStack-commandsContainer" ref={r => r?.scroll({ behavior: 'auto', top: r?.scrollHeight + 20 })} style={{ background: UndoManager.batchCounter.get() ? 'yellow' : undefined }}>
+                    <div className="undoStack-resultContainer" key={0}>
+                        <div className="undoStack-commandString" style={{ fontWeight: 'bold', textAlign: 'center' }}>
+                            Undo/Redo Stack
+                        </div>
+                    </div>
                     {UndoManager.undoStackNames.map((name, i) => (
-                        <div className="scriptingRepl-resultContainer" key={i}>
-                            <div className="scriptingRepl-commandString">{name.replace(/[^\.]*\./, '')}</div>
+                        <div className="undoStack-resultContainer" key={i}>
+                            <div className="undoStack-commandString">{name.replace(/[^\.]*\./, '')}</div>
                         </div>
                     ))}
                     {Array.from(UndoManager.redoStackNames)
                         .reverse()
                         .map((name, i) => (
-                            <div className="scriptingRepl-resultContainer" key={i}>
-                                <div className="scriptingRepl-commandString" style={{ fontWeight: 'bold', color: 'red' }}>
+                            <div className="undoStack-resultContainer" key={i}>
+                                <div className="undoStack-commandString" style={{ fontWeight: 'bold', color: 'red' }}>
                                     {name.replace(/[^\.]*\./, '')}
                                 </div>
                             </div>
diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index 4ae24af60..76dadc76d 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -23,6 +23,7 @@ import { LightboxView } from '../LightboxView';
 import { OpenWhere, OpenWhereMod } from '../nodes/DocumentView';
 import { OverlayView } from '../OverlayView';
 import { ScriptingRepl } from '../ScriptingRepl';
+import { UndoStack } from '../UndoStack';
 import './CollectionDockingView.scss';
 import { CollectionFreeFormView } from './collectionFreeForm';
 import { CollectionSubView, SubCollectionViewProps } from './CollectionSubView';
@@ -587,8 +588,12 @@ ScriptingGlobals.add(
             case OpenWhere.addRight:
                 return CollectionDockingView.AddSplit(doc, OpenWhereMod.right);
             case OpenWhere.overlay:
-                if (doc === 'repl') OverlayView.Instance.addWindow(<ScriptingRepl />, { x: 300, y: 100, width: 200, height: 200, title: 'Scripting REPL' });
-                else Doc.AddDocToList(Doc.MyOverlayDocs, undefined, doc);
+                // prettier-ignore
+                switch (doc) {
+                    case '<ScriptingRepl />': return OverlayView.Instance.addWindow(<ScriptingRepl />, { x: 300, y: 100, width: 200, height: 200, title: 'Scripting REPL' });
+                    case "<UndoStack>": return OverlayView.Instance.addWindow(<UndoStack />, { x: 300, y: 100, width: 200, height: 200, title: 'Scripting REPL' });
+                }
+                Doc.AddDocToList(Doc.MyOverlayDocs, undefined, doc);
         }
     },
     'opens up document in location specified',
diff --git a/src/client/views/collections/collectionLinear/CollectionLinearView.tsx b/src/client/views/collections/collectionLinear/CollectionLinearView.tsx
index 9d82e3198..e0c294da4 100644
--- a/src/client/views/collections/collectionLinear/CollectionLinearView.tsx
+++ b/src/client/views/collections/collectionLinear/CollectionLinearView.tsx
@@ -15,6 +15,7 @@ import { DocumentLinksButton } from '../../nodes/DocumentLinksButton';
 import { DocumentView } from '../../nodes/DocumentView';
 import { LinkDescriptionPopup } from '../../nodes/LinkDescriptionPopup';
 import { StyleProp } from '../../StyleProvider';
+import { UndoStack } from '../../UndoStack';
 import { CollectionStackedTimeline } from '../CollectionStackedTimeline';
 import { CollectionSubView } from '../CollectionSubView';
 import './CollectionLinearView.scss';
@@ -161,9 +162,15 @@ export class CollectionLinearView extends CollectionSubView() {
             </span>
         );
     };
+
     getDisplayDoc = (doc: Doc, preview: boolean = false) => {
-        if (doc.icon === 'linkui') return this.getLinkUI();
-        if (doc.icon === 'currentlyplayingui') return this.getCurrentlyPlayingUI();
+        // hack to avoid overhead of making UndoStack,etc into DocumentView style Boxes. If the UndoStack is ever intended to become part of the persisten state of the dashboard, then this would have to change.
+        // prettier-ignore
+        switch (doc.layout) {
+            case '<LinkingUI>':          return this.getLinkUI();
+            case '<CurrentlyPlayingUI>': return this.getCurrentlyPlayingUI();
+            case '<UndoStack>':          return <UndoStack width={200} height={40} inline={true} />;
+        }
 
         const nested = doc._type_collection === CollectionViewType.Linear;
         const hidden = doc.hidden === true;
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 52eee84ac..10ef1e6af 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -506,7 +506,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
         this._longPressSelector = setTimeout(() => {
             if (DocumentView.LongPress) {
                 if (this.rootDoc.dontUndo) {
-                    OverlayView.Instance.addWindow(<UndoStack />, { x: 300, y: 100, width: 200, height: 200, title: 'Undo Stack' });
+                    runInAction(() => (UndoStack.HideInline = !UndoStack.HideInline));
                 } else {
                     this.props.select(false);
                 }
-- 
cgit v1.2.3-70-g09d2


From da5184d2113935df25633a6015e177a51e88df2f Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Wed, 17 May 2023 16:18:20 -0400
Subject: fixed scrolling/panning on fitWidth images.  fixed drag/zoom
 conflicts for overlayDocs that are pannable.

---
 src/client/documents/Documents.ts                  |  6 +-
 src/client/util/DocumentManager.ts                 |  2 +-
 src/client/views/DocumentDecorations.tsx           |  6 +-
 src/client/views/MarqueeAnnotator.tsx              |  6 +-
 src/client/views/OverlayView.tsx                   |  5 +-
 src/client/views/PropertiesView.tsx                |  8 +--
 .../views/collections/CollectionDockingView.tsx    |  2 +-
 src/client/views/collections/CollectionMenu.tsx    |  1 -
 src/client/views/collections/TabDocView.tsx        |  5 +-
 src/client/views/collections/TreeView.tsx          |  2 +-
 .../collectionFreeForm/CollectionFreeFormView.tsx  | 71 +++++++++++-----------
 .../collectionSchema/CollectionSchemaView.tsx      | 21 ++++---
 src/client/views/nodes/AudioBox.tsx                |  4 +-
 src/client/views/nodes/DocumentView.tsx            |  2 +-
 src/client/views/nodes/KeyValuePair.scss           |  4 --
 src/client/views/nodes/KeyValuePair.tsx            |  2 +-
 src/client/views/nodes/VideoBox.tsx                | 12 ++--
 src/client/views/nodes/WebBox.tsx                  |  1 +
 .../views/nodes/formattedText/FormattedTextBox.tsx | 29 ++++-----
 src/client/views/nodes/trails/PresBox.scss         |  1 +
 src/client/views/nodes/trails/PresBox.tsx          | 24 ++++----
 src/client/views/nodes/trails/PresElementBox.tsx   | 18 +++---
 src/fields/Doc.ts                                  |  9 +++
 23 files changed, 124 insertions(+), 117 deletions(-)

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

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 5aa69e499..35747f746 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -450,7 +450,7 @@ export namespace Docs {
                 DocumentType.IMG,
                 {
                     layout: { view: ImageBox, dataField: defaultDataKey },
-                    options: {},
+                    options: {freeform: '',},
                 },
             ],
             [
@@ -464,7 +464,7 @@ export namespace Docs {
                 DocumentType.COL,
                 {
                     layout: { view: CollectionView, dataField: defaultDataKey },
-                    options: { _layout_fitWidth: true, _freeform_panX: 0, _freeform_panY: 0, _freeform_scale: 1 },
+                    options: { _layout_fitWidth: true, freeform: '', _freeform_panX: 0, _freeform_panY: 0, _freeform_scale: 1 },
                 },
             ],
             [
@@ -1035,7 +1035,7 @@ export namespace Docs {
         // }
 
         export function FreeformDocument(documents: Array<Doc>, options: DocumentOptions, id?: string) {
-            const inst = InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { _xPadding: 20, _yPadding: 20, freeform: '', ...options, _type_collection: CollectionViewType.Freeform }, id);
+            const inst = InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { _xPadding: 20, _yPadding: 20,  ...options, _type_collection: CollectionViewType.Freeform }, id);
             documents.forEach(d => (d.embedContainer = inst));
             return inst;
         }
diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts
index ea5770b6a..642ea26da 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -318,7 +318,7 @@ export function DocFocusOrOpen(doc: Doc, options: DocFocusOptions = { willZoomCe
         if (dv && (!containingDoc || dv.props.docViewPath().lastElement()?.Document === containingDoc)) {
             DocumentManager.Instance.showDocumentView(dv, options).then(() => dv && Doc.linkFollowHighlight(dv.rootDoc));
         } else {
-            const container = DocCast(containingDoc ?? doc.embedContainer);
+            const container = DocCast(containingDoc ?? doc.embedContainer ?? doc);
             const showDoc = !Doc.IsSystem(container) ? container : doc;
             options.toggleTarget = undefined;
             DocumentManager.Instance.showDocument(showDoc, options, () => DocumentManager.Instance.showDocument(doc, { ...options, openLocation: undefined })).then(() => {
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 8077b9af1..beddc0e8a 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -10,7 +10,7 @@ import { AclAdmin, AclEdit, DataSym, Doc, DocListCast, Field, HeightSym, WidthSy
 import { InkField } from '../../fields/InkField';
 import { RichTextField } from '../../fields/RichTextField';
 import { ScriptField } from '../../fields/ScriptField';
-import { Cast, NumCast, StrCast } from '../../fields/Types';
+import { Cast, DocCast, NumCast, StrCast } from '../../fields/Types';
 import { GetEffectiveAcl } from '../../fields/util';
 import { aggregateBounds, emptyFunction, numberValue, returnFalse, setupMoveUpEvents, Utils } from '../../Utils';
 import { Docs } from '../documents/Documents';
@@ -189,7 +189,9 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
     @action
     onBackgroundMove = (dragTitle: boolean, e: PointerEvent): boolean => {
         const dragDocView = SelectionManager.Views()[0];
-        if (DocListCast(Doc.MyOverlayDocs.data).includes(dragDocView.rootDoc)) return false;
+        const containers = new Set<Doc|undefined>();
+        SelectionManager.Views().forEach(v => containers.add(DocCast(v.rootDoc.embedContainer)));
+        if (containers.size > 1) return false;
         const { left, top } = dragDocView.getBounds() || { left: 0, top: 0 };
         const dragData = new DragManager.DocumentDragData(
             SelectionManager.Views().map(dv => dv.props.Document),
diff --git a/src/client/views/MarqueeAnnotator.tsx b/src/client/views/MarqueeAnnotator.tsx
index 1d6c94c13..c9555ab91 100644
--- a/src/client/views/MarqueeAnnotator.tsx
+++ b/src/client/views/MarqueeAnnotator.tsx
@@ -153,8 +153,8 @@ export class MarqueeAnnotator extends React.Component<MarqueeAnnotatorProps> {
                 annotationOn: this.props.rootDoc,
                 title: 'Annotation on ' + this.props.rootDoc.title,
             });
-            marqueeAnno.x = NumCast(this.props.docView.props.Document.freeform_panXMin) + (parseInt(anno.style.left || '0') - containerOffset[0]) / scale / NumCast(this.props.docView.props.Document._freeform_scale, 1);
-            marqueeAnno.y = NumCast(this.props.docView.props.Document.freeform_panYMin) + (parseInt(anno.style.top || '0') - containerOffset[1]) / scale / NumCast(this.props.docView.props.Document._freeform_scale, 1) + NumCast(this.props.scrollTop);
+            marqueeAnno.x = NumCast(this.props.docView.props.Document.freeform_panX_min) + (parseInt(anno.style.left || '0') - containerOffset[0]) / scale / NumCast(this.props.docView.props.Document._freeform_scale, 1);
+            marqueeAnno.y = NumCast(this.props.docView.props.Document.freeform_panY_min) + (parseInt(anno.style.top || '0') - containerOffset[1]) / scale / NumCast(this.props.docView.props.Document._freeform_scale, 1) + NumCast(this.props.scrollTop);
             marqueeAnno._height = parseInt(anno.style.height || '0') / scale / NumCast(this.props.docView.props.Document._freeform_scale, 1);
             marqueeAnno._width = parseInt(anno.style.width || '0') / scale / NumCast(this.props.docView.props.Document._freeform_scale, 1);
             anno.remove();
@@ -243,7 +243,7 @@ export class MarqueeAnnotator extends React.Component<MarqueeAnnotatorProps> {
         this._top = Math.min(this._startY, this._startY + this._height);
         this._width = Math.abs(this._width);
         this._height = Math.abs(this._height);
-        e.stopPropagation();
+        //e.stopPropagation(); // overlay documents are all 'active', yet they can be dragged. if we stop propagation, then they can be marqueed but not dragged.  if we don't stop, then they will be marqueed and dragged, but the marquee will be zero width since the doc will move along with the cursor.
     };
 
     onSelectEnd = (e: PointerEvent) => {
diff --git a/src/client/views/OverlayView.tsx b/src/client/views/OverlayView.tsx
index 9baedae6d..00f9aa879 100644
--- a/src/client/views/OverlayView.tsx
+++ b/src/client/views/OverlayView.tsx
@@ -159,7 +159,7 @@ export class OverlayView extends React.Component {
     }
 
     removeOverlayDoc = (doc: Doc | Doc[]) => {
-        (doc instanceof Doc ? [doc] : doc).forEach(doc => Doc.RemoveDocFromList(Doc.MyOverlayDocs, undefined, doc));
+        (doc instanceof Doc ? [doc] : doc).forEach(Doc.RemFromMyOverlay);
         return true;
     };
 
@@ -177,6 +177,7 @@ export class OverlayView extends React.Component {
                     offsety = 0;
                 const dref = React.createRef<HTMLDivElement>();
                 const onPointerMove = action((e: PointerEvent, down: number[]) => {
+                 if (e.cancelBubble) return false;  // if the overlay doc processed the move event (e.g., to pan its contents), then the event should be marked as canceled since propagation can't be stopped
                     if (e.buttons === 1) {
                         d.overlayX = e.clientX + offsetx;
                         d.overlayY = e.clientY + offsety;
@@ -196,7 +197,7 @@ export class OverlayView extends React.Component {
                 });
 
                 const onPointerDown = (e: React.PointerEvent) => {
-                    setupMoveUpEvents(this, e, onPointerMove, emptyFunction, emptyFunction);
+                    setupMoveUpEvents(this, e, onPointerMove, emptyFunction, emptyFunction, false);
                     offsetx = NumCast(d.overlayX) - e.clientX;
                     offsety = NumCast(d.overlayY) - e.clientY;
                 };
diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx
index 75514a090..0c89f1a27 100644
--- a/src/client/views/PropertiesView.tsx
+++ b/src/client/views/PropertiesView.tsx
@@ -1623,8 +1623,8 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
                     </div>
                 );
             }
-            if (this.isPres) {
-                const selectedItem: boolean = PresBox.Instance?.selectedArray.size > 0;
+            if (this.isPres && PresBox.Instance) {
+                const selectedItem: boolean = PresBox.Instance.selectedArray.size > 0;
                 const type = [DocumentType.AUDIO, DocumentType.VID].includes(DocCast(PresBox.Instance.activeItem?.annotationOn)?.type as any as DocumentType)
                     ? (DocCast(PresBox.Instance.activeItem?.annotationOn)?.type as any as DocumentType)
                     : PresBox.targetRenderedDoc(PresBox.Instance.activeItem)?.type;
@@ -1636,8 +1636,8 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
                         <div className="propertiesView-name" style={{ borderBottom: 0 }}>
                             {this.editableTitle}
                             <div className="propertiesView-presSelected">
-                                <div className="propertiesView-selectedCount">{PresBox.Instance?.selectedArray.size} selected</div>
-                                <div className="propertiesView-selectedList">{PresBox.Instance?.listOfSelected}</div>
+                                <div className="propertiesView-selectedCount">{PresBox.Instance.selectedArray.size} selected</div>
+                                <div className="propertiesView-selectedList">{PresBox.Instance.listOfSelected}</div>
                             </div>
                         </div>
                         {!selectedItem ? null : (
diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index 76dadc76d..e9cc2c894 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -593,7 +593,7 @@ ScriptingGlobals.add(
                     case '<ScriptingRepl />': return OverlayView.Instance.addWindow(<ScriptingRepl />, { x: 300, y: 100, width: 200, height: 200, title: 'Scripting REPL' });
                     case "<UndoStack>": return OverlayView.Instance.addWindow(<UndoStack />, { x: 300, y: 100, width: 200, height: 200, title: 'Scripting REPL' });
                 }
-                Doc.AddDocToList(Doc.MyOverlayDocs, undefined, doc);
+                Doc.AddToMyOverlay(doc);
         }
     },
     'opens up document in location specified',
diff --git a/src/client/views/collections/CollectionMenu.tsx b/src/client/views/collections/CollectionMenu.tsx
index 570d9c8fb..d2338d742 100644
--- a/src/client/views/collections/CollectionMenu.tsx
+++ b/src/client/views/collections/CollectionMenu.tsx
@@ -549,7 +549,6 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewMenu
     @action
     startRecording = () => {
         const doc = Docs.Create.ScreenshotDocument({ title: 'screen recording', _layout_fitWidth: true, _width: 400, _height: 200, mediaState: 'pendingRecording' });
-        //Doc.AddDocToList(Doc.MyOverlayDocs, undefined, doc);
         CollectionDockingView.AddSplit(doc, OpenWhereMod.right);
     };
 
diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx
index 33fa434e1..b20d05433 100644
--- a/src/client/views/collections/TabDocView.tsx
+++ b/src/client/views/collections/TabDocView.tsx
@@ -292,13 +292,12 @@ export class TabDocView extends React.Component<TabDocViewProps> {
             PresBox.Instance?.clearSelectedArray();
             pinDoc && PresBox.Instance?.addToSelectedArray(pinDoc); //Update selected array
         });
-        if (
+        if ( // open the presentation trail if it's not already opened
             !Array.from(CollectionDockingView.Instance?.tabMap ?? [])
                 .map(d => d.DashDoc)
                 .includes(curPres)
         ) {
-            const docs = Cast(Doc.MyOverlayDocs.data, listSpec(Doc), []);
-            if (docs.includes(curPres)) docs.splice(docs.indexOf(curPres), 1);
+            if (Doc.IsInMyOverlay(curPres)) Doc.RemFromMyOverlay(curPres);
             CollectionDockingView.AddSplit(curPres, OpenWhereMod.right);
             setTimeout(() => DocumentManager.Instance.showDocument(docList.lastElement(), { willPan: true }), 100); // keeps the pinned doc in view since the sidebar shifts things
         }
diff --git a/src/client/views/collections/TreeView.tsx b/src/client/views/collections/TreeView.tsx
index f56eaee07..abbb0df4c 100644
--- a/src/client/views/collections/TreeView.tsx
+++ b/src/client/views/collections/TreeView.tsx
@@ -761,7 +761,7 @@ export class TreeView extends React.Component<TreeViewProps> {
         const makeFolder = { script: ScriptField.MakeFunction(`scriptContext.makeFolder()`, { scriptContext: 'any' })!, icon: 'folder-plus', label: 'New Folder' };
         const deleteItem = { script: ScriptField.MakeFunction(`scriptContext.deleteItem()`, { scriptContext: 'any' })!, icon: 'folder-plus', label: 'Delete' };
         const folderOp = this.childDocs?.length ? [makeFolder] : [];
-        const openEmbedding = { script: ScriptField.MakeFunction(`openDoc(getEmbedding(self), ${OpenWhere.addRight})`)!, icon: 'copy', label: 'Open Embedding' };
+        const openEmbedding = { script: ScriptField.MakeFunction(`openDoc(getEmbedding(self), ${OpenWhere.addRight})`)!, icon: 'copy', label: 'Open New Embedding' };
         const focusDoc = { script: ScriptField.MakeFunction(`DocFocusOrOpen(self)`)!, icon: 'eye', label: 'Focus or Open' };
         return [
             ...(this.props.contextMenuItems ?? []).filter(mi => (!mi.filter ? true : mi.filter.script.run({ doc: this.doc })?.result)),
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 9bf071cc0..3b5dd45d9 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -635,7 +635,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                     case InkTool.None:
                         if (!(this.props.layoutEngine?.() || StrCast(this.layoutDoc._layoutEngine))) {
                             this._hitCluster = this.pickCluster(this.getTransform().transformPoint(e.clientX, e.clientY));
-                            setupMoveUpEvents(this, e, this.onPointerMove, emptyFunction, emptyFunction, this._hitCluster !== -1 ? true : false);
+                            setupMoveUpEvents(this, e, this.onPointerMove, emptyFunction, emptyFunction, this._hitCluster !== -1 ? true : false, false);
                         }
                         break;
                 }
@@ -868,9 +868,14 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
             Doc.ActiveTool = InkTool.None;
         } else {
             if (this.tryDragCluster(e, this._hitCluster)) {
+                e.stopPropagation(); // we're moving a cluster, so stop propagation and return true to end panning and let the document drag take over
                 return true;
             }
-            this.pan(e);
+            // pan the view if this is a regular collection, or it's an overlay and the overlay is zoomed (otherwise, there's nothing to pan)
+            if (!this.props.isAnnotationOverlay || (1 - NumCast(this.rootDoc._freeform_scale_min, 1)/ this.getLocalTransform().inverse().Scale)) {
+                this.pan(e);                
+                e.stopPropagation();  // if we are actually panning, stop propagation -- this will preven things like the overlayView from dragging the document while we're panning
+            }
         }
         return false;
     };
@@ -1023,12 +1028,12 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         if (deltaScale * invTransform.Scale > 20) {
             deltaScale = 20 / invTransform.Scale;
         }
-        if (deltaScale < 1 && invTransform.Scale <= NumCast(this.rootDoc._freeform_scaleMin, 1) && this.isAnnotationOverlay) {
+        if (deltaScale < 1 && invTransform.Scale <= NumCast(this.rootDoc._freeform_scale_min, 1) && this.isAnnotationOverlay) {
             this.setPan(0, 0);
             return;
         }
-        if (deltaScale * invTransform.Scale < NumCast(this.rootDoc._freeform_scaleMin, 1) && this.isAnnotationOverlay) {
-            deltaScale = NumCast(this.rootDoc._freeform_scaleMin, 1) / invTransform.Scale;
+        if (deltaScale * invTransform.Scale < NumCast(this.rootDoc._freeform_scale_min, 1) && this.isAnnotationOverlay) {
+            deltaScale = NumCast(this.rootDoc._freeform_scale_min, 1) / invTransform.Scale;
         }
 
         const localTransform = invTransform.scaleAbout(deltaScale, x, y);
@@ -1040,36 +1045,25 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     };
 
     @action
-    onPointerWheel = (e: React.WheelEvent): void => {
+    onPointerWheel = (e:React.WheelEvent): void => {
         if (this.Document._isGroup || !this.isContentActive()) return; // group style collections neither pan nor zoom
         PresBox.Instance?.pauseAutoPres();
-        if (this.layoutDoc._Transform || DocListCast(Doc.MyOverlayDocs?.data).includes(this.props.Document) || this.props.Document.treeViewOutlineMode === TreeViewType.outline) return;
+        if (this.layoutDoc._Transform || this.props.Document.treeViewOutlineMode === TreeViewType.outline) return;
         e.stopPropagation();
+        const docHeight = NumCast(this.rootDoc[Doc.LayoutFieldKey(this.rootDoc)+"_nativeHeight"], this.nativeHeight);
+        const scrollable = NumCast(this.layoutDoc[this.scaleFieldKey],1) ===1 && docHeight > this.props.PanelHeight() / this.nativeDimScaling ;
         switch (!e.ctrlKey ? Doc.UserDoc().freeformScrollMode : freeformScrollMode.Pan) {
             case freeformScrollMode.Pan:
                 // if ctrl is selected then zoom
-                if (e.ctrlKey) {
-                    if (this.props.isContentActive(true)) {
-                        if (this.props.isAnnotationOverlayScrollable) {
-                            // bcz: zooming on a webbox doesn't get the correct coordinates here for unknown reasons.
-                            // so better to do nothing than having things jump around.
-                        } else this.zoom(e.screenX, e.screenY, e.deltaY);
-                    }
-                } // otherwise pan
-                else if (this.props.isContentActive(true)) {
-                    const dx = -e.deltaX;
-                    const dy = -e.deltaY;
-                    if (e.shiftKey) {
-                        !this.props.isAnnotationOverlayScrollable && this.scrollPan({ deltaX: dy, deltaY: 0 });
-                    } else {
-                        !this.props.isAnnotationOverlayScrollable && this.scrollPan({ deltaX: dx, deltaY: dy });
-                    }
+                if (!e.ctrlKey && this.props.isContentActive(true)) {
+                    this.scrollPan({ deltaX: -e.deltaX, deltaY: e.shiftKey ? 0 :-Math.max(-1, Math.min(1,e.deltaY)) });
+                    break;
                 }
-                break;
             default:
             case freeformScrollMode.Zoom:
-                if (this.props.isContentActive(true)) {
-                    !this.props.isAnnotationOverlayScrollable && this.zoom(e.clientX, e.clientY, e.deltaY); // if (!this.props.isAnnotationOverlay) // bcz: do we want to zoom in on images/videos/etc?
+                if ((e.ctrlKey || !scrollable) && this.props.isContentActive(true)) {
+                    this.zoom(e.clientX, e.clientY,Math.max(-1, Math.min(1,e.deltaY))); // if (!this.props.isAnnotationOverlay) // bcz: do we want to zoom in on images/videos/etc?
+                    e.preventDefault();
                 }
                 break;
         }
@@ -1112,19 +1106,20 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                 else if (ranges.yrange.max <= panY - panelHgtMin / 2) panY = ranges.yrange.min - (this.props.originTopLeft ? panelHgtMax / 2 : panelHgtMin / 2);
             }
         }
-        if (!this.layoutDoc._lockedTransform || LightboxView.LightboxDoc || DocListCast(Doc.MyOverlayDocs?.data).includes(this.Document)) {
+        if (!this.layoutDoc._lockedTransform || LightboxView.LightboxDoc) {
             this.setPanZoomTransition(panTime);
-            const scale = this.getLocalTransform().inverse().Scale;
-            const minScale = NumCast(this.rootDoc._freeform_scaleMin, 1);
-            const minPanX = NumCast(this.rootDoc._freeform_panXMin, 0);
-            const minPanY = NumCast(this.rootDoc._freeform_panYMin, 0);
-            const newPanX = Math.min(minPanX + (1 - minScale / scale) * NumCast(this.rootDoc._freeform_panXMax, this.nativeWidth), Math.max(minPanX, panX));
+            const minScale = NumCast(this.rootDoc._freeform_scale_min, 1);
+            const scale = (1 - minScale/ this.getLocalTransform().inverse().Scale);
+            const minPanX = NumCast(this.rootDoc._freeform_panX_min, 0);
+            const minPanY = NumCast(this.rootDoc._freeform_panY_min, 0);
+            const maxPanX = NumCast(this.rootDoc._freeform_panX_max, this.nativeWidth);
+            const newPanX = Math.min(minPanX + scale *maxPanX, Math.max(minPanX, panX));
             const fitYscroll = (((this.nativeHeight / this.nativeWidth) * this.props.PanelWidth() - this.props.PanelHeight()) * this.props.ScreenToLocalTransform().Scale) / minScale;
             const nativeHeight = (this.props.PanelHeight() / this.props.PanelWidth() / (this.nativeHeight / this.nativeWidth)) * this.nativeHeight;
             const maxScrollTop = this.nativeHeight / this.props.ScreenToLocalTransform().Scale - this.props.PanelHeight();
             const maxPanY =
                 minPanY + // minPanY + scrolling introduced by view scaling + scrolling introduced by layout_fitWidth
-                (1 - minScale / scale) * NumCast(this.rootDoc._panYMax, nativeHeight) +
+                scale* NumCast(this.rootDoc._panY_max, nativeHeight) +
                 (!this.props.getScrollHeight?.() ? fitYscroll : 0); // when not zoomed, scrolling is handled via a scrollbar, not panning
             let newPanY = Math.max(minPanY, Math.min(maxPanY, panY));
             if (false && NumCast(this.rootDoc.layout_scrollTop) && NumCast(this.rootDoc._freeform_scale, minScale) !== minScale) {
@@ -1971,6 +1966,12 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     lightboxPanelWidth = () => Math.max(0, this.props.PanelWidth() - 30);
     lightboxPanelHeight = () => Math.max(0, this.props.PanelHeight() - 30);
     lightboxScreenToLocal = () => this.props.ScreenToLocalTransform().translate(-15, -15);
+    onPassiveWheel = (e:WheelEvent) => {
+        const docHeight = NumCast(this.rootDoc[Doc.LayoutFieldKey(this.rootDoc)+"_nativeHeight"], this.nativeHeight);
+        const scrollable = NumCast(this.layoutDoc[this.scaleFieldKey],1) ===1 && docHeight > this.props.PanelHeight() / this.nativeDimScaling ;
+        this.props.isSelected() && !scrollable && e.preventDefault()
+    }
+    _oldWheel:any;
     render() {
         TraceMobx();
         return (
@@ -1978,8 +1979,10 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                 className="collectionfreeformview-container"
                 ref={r => {
                     this.createDashEventsTarget(r);
+                    this._oldWheel?.removeEventListener('wheel', this.onPassiveWheel);
+                    this._oldWheel= r;
                     // prevent wheel events from passivly propagating up through containers
-                    !this.props.isAnnotationOverlay && r?.addEventListener('wheel', (e: WheelEvent) => this.props.isSelected() && e.preventDefault(), { passive: false });
+                     r?.addEventListener('wheel', this.onPassiveWheel, { passive: false });
                 }}
                 onWheel={this.onPointerWheel}
                 onClick={this.onClick}
diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx
index 2a68f9b58..1cc48bebe 100644
--- a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx
+++ b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx
@@ -686,6 +686,12 @@ export class CollectionSchemaView extends CollectionSubView() {
         );
     }
 
+    onPassiveWheel = (e:WheelEvent) => { 
+        // if scrollTop is 0, then don't let wheel trigger scroll on any container (which it would since onScroll won't be triggered on this)
+        if (!this._oldWheel.scrollTop && e.deltaY <= 0) e.preventDefault();
+        e.stopPropagation();
+    }
+    _oldWheel:any;
     @computed get keysDropdown() {
         return (
             <div className="schema-key-search">
@@ -699,16 +705,11 @@ export class CollectionSchemaView extends CollectionSubView() {
                 </div>
                 <div
                     className="schema-key-list"
-                    ref={r =>
-                        r?.addEventListener(
-                            'wheel', // if scrollTop is 0, then don't let wheel trigger scroll on any container (which it would since onScroll won't be triggered on this)
-                            (e: WheelEvent) => {
-                                if (!r.scrollTop && e.deltaY <= 0) e.preventDefault();
-                                e.stopPropagation();
-                            },
-                            { passive: false }
-                        )
-                    }>
+                    ref={r => {
+                        this._oldWheel?.removeEventListener('wheel', this.onPassiveWheel);
+                        this._oldWheel= r;
+                        r?.addEventListener('wheel', this.onPassiveWheel, { passive: false } );
+                    }}>
                     {this._menuKeys.map(key => (
                         <div
                             className="schema-search-result"
diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx
index 410e0bbdc..0cb849923 100644
--- a/src/client/views/nodes/AudioBox.tsx
+++ b/src/client/views/nodes/AudioBox.tsx
@@ -362,10 +362,10 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
                 Doc.GetProto(newDoc).recordingSource = this.dataDoc;
                 Doc.GetProto(newDoc).recordingStart = ComputedField.MakeFunction(`self.recordingSource["${this.fieldKey}-recordingStart"]`);
                 Doc.GetProto(newDoc).mediaState = ComputedField.MakeFunction('self.recordingSource.mediaState');
-                if (DocListCast(Doc.MyOverlayDocs?.data).includes(this.rootDoc)) {
+                if (Doc.IsInMyOverlay(this.rootDoc)) {
                     newDoc.overlayX = this.rootDoc.x;
                     newDoc.overlayY = NumCast(this.rootDoc.y) + NumCast(this.rootDoc._height);
-                    Doc.AddDocToList(Doc.MyOverlayDocs, undefined, newDoc);
+                    Doc.AddToMyOverlay(newDoc);
                 } else {
                     this.props.addDocument?.(newDoc);
                 }
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 10ef1e6af..84f82a4b9 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -527,7 +527,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
                 !this.Document.ignoreClick &&
                 e.button === 0 &&
                 this.pointerEvents !== 'none' &&
-                !DocListCast(Doc.MyOverlayDocs?.data).includes(this.layoutDoc)
+                !Doc.IsInMyOverlay(this.layoutDoc)
             ) {
                 e.stopPropagation();
                 // don't preventDefault anymore.  Goldenlayout, PDF text selection and RTF text selection all need it to go though
diff --git a/src/client/views/nodes/KeyValuePair.scss b/src/client/views/nodes/KeyValuePair.scss
index 2b4867321..57d36932e 100644
--- a/src/client/views/nodes/KeyValuePair.scss
+++ b/src/client/views/nodes/KeyValuePair.scss
@@ -19,7 +19,6 @@
             position: relative;
             margin: 0;
         }
-        .keyValuePair-keyFieldMod,
         .keyValuePair-keyField {
             width: 100%;
             margin-left: 20px;
@@ -28,9 +27,6 @@
             overflow: auto;
             display: inline;
         }
-        .keyValuePair-keyFieldMod {
-            margin-left: 35px;
-        }
     }
 }
 .keyValuePair-td-value {
diff --git a/src/client/views/nodes/KeyValuePair.tsx b/src/client/views/nodes/KeyValuePair.tsx
index 509bb667e..91df928c4 100644
--- a/src/client/views/nodes/KeyValuePair.tsx
+++ b/src/client/views/nodes/KeyValuePair.tsx
@@ -109,7 +109,7 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> {
                             X
                         </button>
                         <input className="keyValuePair-td-key-check" type="checkbox" style={hover} onChange={this.handleCheck} ref={this.checkbox} />
-                        <div className={`keyValuePair-keyField${props.fieldKey.includes('_') ? 'Mod' : ''}`} style={{ color: keyStyle }}>
+                        <div className='keyValuePair-keyField' style={{ marginLeft: 35 * (props.fieldKey.match(/_/g)?.length ||0), color: keyStyle }}>
                             {'('.repeat(parenCount)}
                             {props.fieldKey}
                             {')'.repeat(parenCount)}
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index 3cfc3d696..9cf929679 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -870,7 +870,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
                 }),
                 returnFalse,
                 () => MarqueeAnnotator.clearAnnotations(this._savedAnnotations),
-                false
+                false, false
             );
         }
     };
@@ -1026,13 +1026,13 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
         croppingProto.videoCrop = true;
         croppingProto.layout_currentTimecode = this.layoutDoc._layout_currentTimecode;
         croppingProto.freeform_scale = viewScale;
-        croppingProto.freeform_scaleMin = viewScale;
+        croppingProto.freeform_scale_min = viewScale;
         croppingProto.freeform_ = anchx / viewScale;
         croppingProto.freeform_panY = anchy / viewScale;
-        croppingProto.freeform_panXMin = anchx / viewScale;
-        croppingProto.freeform_panXMax = anchw / viewScale;
-        croppingProto.freeform_panYMin = anchy / viewScale;
-        croppingProto.freeform_panYMax = anchh / viewScale;
+        croppingProto.freeform_panX_min = anchx / viewScale;
+        croppingProto.freeform_panX_max = anchw / viewScale;
+        croppingProto.freeform_panY_min = anchy / viewScale;
+        croppingProto.freeform_panY_max = anchh / viewScale;
         if (addCrop) {
             DocUtils.MakeLink(region, cropping, { link_relationship: 'cropped image' });
         }
diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx
index 437079ca4..48808dbad 100644
--- a/src/client/views/nodes/WebBox.tsx
+++ b/src/client/views/nodes/WebBox.tsx
@@ -745,6 +745,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
                     ref={action((r: HTMLIFrameElement | null) => (this._iframe = r))}
                     src={url}
                     onLoad={this.iframeLoaded}
+                    scrolling="no" // ugh.. on windows, I get an inner scroll bar for the iframe's body even though the scrollHeight should be set to the full height of the document.  
                     // the 'allow-top-navigation' and 'allow-top-navigation-by-user-activation' attributes are left out to prevent iframes from redirecting the top-level Dash page
                     // sandbox={"allow-forms allow-modals allow-orientation-lock allow-pointer-lock allow-popups allow-popups-to-escape-sandbox allow-presentation allow-same-origin allow-scripts"} />;
                     sandbox={`${this.layoutDoc.allowScripts ? 'allow-scripts' : ''} allow-forms allow-modals allow-orientation-lock allow-pointer-lock allow-popups allow-popups-to-escape-sandbox allow-presentation allow-same-origin`}
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index e85835002..fb0c0d2ab 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -917,10 +917,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
                     data_nativeWidth: result.nativeWidth,
                     data_nativeHeight: result.nativeHeight,
                 });
-                if (DocListCast(Doc.MyOverlayDocs?.data).includes(this.rootDoc)) {
+                if (Doc.IsInMyOverlay(this.rootDoc)) {
                     newDoc.overlayX = this.rootDoc.x;
                     newDoc.overlayY = NumCast(this.rootDoc.y) + NumCast(this.rootDoc._height);
-                    Doc.AddDocToList(Doc.MyOverlayDocs, undefined, newDoc);
+                    Doc.AddToMyOverlay(newDoc);
                 } else {
                     this.props.addDocument?.(newDoc);
                 }
@@ -2000,6 +2000,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
         return this.props.fieldKey + (usePath && (!usePath.includes(':hover') || this._isHovering) ? `_${usePath.replace(':hover', '')}` : '');
     }
     @observable _isHovering = false;
+    onPassiveWheel = (e:WheelEvent) => { 
+        // if scrollTop is 0, then don't let wheel trigger scroll on any container (which it would since onScroll won't be triggered on this)
+        if (this.props.isContentActive() && !this.props.allowScroll) {
+            if (!NumCast(this.layoutDoc._layout_scrollTop) && e.deltaY <= 0) e.preventDefault();
+            e.stopPropagation();
+        }
+    }
+    _oldWheel:any;
     render() {
         TraceMobx();
         const active = this.props.isContentActive() || this.props.isSelected();
@@ -2019,18 +2027,11 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
                 className="formattedTextBox"
                 onPointerEnter={action(() => (this._isHovering = true))}
                 onPointerLeave={action(() => (this._isHovering = false))}
-                ref={r =>
-                    r?.addEventListener(
-                        'wheel', // if scrollTop is 0, then don't let wheel trigger scroll on any container (which it would since onScroll won't be triggered on this)
-                        (e: WheelEvent) => {
-                            if (this.props.isContentActive() && !this.props.allowScroll) {
-                                if (!NumCast(this.layoutDoc._layout_scrollTop) && e.deltaY <= 0) e.preventDefault();
-                                e.stopPropagation();
-                            }
-                        },
-                        { passive: false }
-                    )
-                }
+                ref={r => {
+                    this._oldWheel?.removeEventListener('wheel', this.onPassiveWheel);
+                    this._oldWheel= r;
+                    r?.addEventListener('wheel', this.onPassiveWheel,  { passive: false } );
+                }}
                 style={{
                     ...(this.props.dontScale
                         ? {}
diff --git a/src/client/views/nodes/trails/PresBox.scss b/src/client/views/nodes/trails/PresBox.scss
index eb91c82f3..bf56b4d9e 100644
--- a/src/client/views/nodes/trails/PresBox.scss
+++ b/src/client/views/nodes/trails/PresBox.scss
@@ -951,6 +951,7 @@
         margin-right: unset;
         height: 100%;
         position: relative;
+        user-select: none;
     }
 
     select {
diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx
index 14d463a6b..913018b69 100644
--- a/src/client/views/nodes/trails/PresBox.tsx
+++ b/src/client/views/nodes/trails/PresBox.tsx
@@ -196,7 +196,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         this.turnOffEdit(true);
         this._disposers.selection = reaction(
             () => SelectionManager.Views(),
-            views => views.some(view => view.props.Document === this.rootDoc) && this.updateCurrentPresentation()
+            views => (!PresBox.Instance || views.some(view => view.props.Document === this.rootDoc)) && this.updateCurrentPresentation(),
+            {fireImmediately:true}
         );
         this._disposers.editing = reaction(
             () => this.layoutDoc.presStatus === PresStatus.Edit,
@@ -832,7 +833,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                     }
                 });
             LightboxView.SetLightboxDoc(undefined);
-            Doc.RemoveDocFromList(Doc.MyOverlayDocs, undefined, this.rootDoc);
+            Doc.RemFromMyOverlay(this.rootDoc);
             return PresStatus.Edit;
         };
     };
@@ -928,8 +929,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         return PresBox.OpenPresMinimized(this.rootDoc, [pt[0] + (this.props.PanelWidth() - 250), pt[1] + 10]);
     };
     exitMinimize = () => {
-        if (DocListCast(Doc.MyOverlayDocs?.data).includes(this.layoutDoc)) {
-            Doc.RemoveDocFromList(Doc.MyOverlayDocs, undefined, this.rootDoc);
+        if (Doc.IsInMyOverlay(this.layoutDoc)) {
+            Doc.RemFromMyOverlay(this.rootDoc);
             CollectionDockingView.AddSplit(this.rootDoc, OpenWhereMod.right);
         }
         return PresStatus.Edit;
@@ -941,7 +942,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         doc.overlayY = pt[1];
         doc._height = 30;
         doc._width = PresBox.minimizedWidth;
-        Doc.AddDocToList(Doc.MyOverlayDocs, undefined, doc);
+        Doc.AddToMyOverlay(doc);
         PresBox.Instance?.initializePresState(PresBox.Instance.itemIndex);
         return (doc.presStatus = PresStatus.Manual);
     }
@@ -1158,7 +1159,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                 }
                 break;
             case 'Escape':
-                if (DocListCast(Doc.MyOverlayDocs?.data).includes(this.layoutDoc)) {
+                if (Doc.IsInMyOverlay(this.layoutDoc)) {
                     this.exitClicked();
                 } else if (this.layoutDoc.presStatus === PresStatus.Edit) {
                     this.clearSelectedArray();
@@ -2130,10 +2131,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         const propTitle = SettingsManager.propertiesWidth > 0 ? 'Close Presentation Panel' : 'Open Presentation Panel';
         const mode = StrCast(this.rootDoc._type_collection) as CollectionViewType;
         const isMini: boolean = this.toolbarWidth <= 100;
-        const inOverlay = DocListCast(Doc.MyOverlayDocs?.data).includes(this.layoutDoc);
         const activeColor = Colors.LIGHT_BLUE;
         const inactiveColor = Colors.WHITE;
-        return mode === CollectionViewType.Carousel3D || inOverlay ? null : (
+        return mode === CollectionViewType.Carousel3D || Doc.IsInMyOverlay(this.rootDoc) ? null : (
             <div id="toolbarContainer" className={'presBox-toolbar'}>
                 {/* <Tooltip title={<><div className="dash-tooltip">{"Add new slide"}</div></>}><div className={`toolbar-button ${this.newDocumentTools ? "active" : ""}`} onClick={action(() => this.newDocumentTools = !this.newDocumentTools)}>
                     <FontAwesomeIcon icon={"plus"} />
@@ -2174,9 +2174,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
     @computed get topPanel() {
         const mode = StrCast(this.rootDoc._type_collection) as CollectionViewType;
         const isMini: boolean = this.toolbarWidth <= 100;
-        const inOverlay = DocListCast(Doc.MyOverlayDocs?.data).includes(this.layoutDoc);
         return (
-            <div className={`presBox-buttons${inOverlay ? ' inOverlay' : ''}`} style={{ background: Doc.ActivePresentation === this.rootDoc ? Colors.LIGHT_BLUE : undefined, display: !this.rootDoc._chromeHidden ? 'none' : undefined }}>
+            <div className={`presBox-buttons${Doc.IsInMyOverlay(this.rootDoc) ? ' inOverlay' : ''}`} style={{ background: Doc.ActivePresentation === this.rootDoc ? Colors.LIGHT_BLUE : undefined, display: !this.rootDoc._chromeHidden ? 'none' : undefined }}>
                 {isMini ? null : (
                     <select className="presBox-viewPicker" style={{ display: this.layoutDoc.presStatus === 'edit' ? 'block' : 'none' }} onPointerDown={e => e.stopPropagation()} onChange={this.viewChanged} value={mode}>
                         <option onPointerDown={StopEvent} value={CollectionViewType.Stacking}>
@@ -2226,7 +2225,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
     @computed get playButtons() {
         const presEnd = !this.layoutDoc.presLoop && this.itemIndex === this.childDocs.length - 1 && (this.activeItem.presIndexed === undefined || NumCast(this.activeItem.presIndexed) === (this.progressivizedItems(this.activeItem)?.length ?? 0));
         const presStart: boolean = !this.layoutDoc.presLoop && this.itemIndex === 0;
-        const inOverlay = DocListCast(Doc.MyOverlayDocs?.data).includes(this.layoutDoc);
+        const inOverlay = Doc.IsInMyOverlay(this.rootDoc);
         // Case 1: There are still other frames and should go through all frames before going to next slide
         return (
             <div className="presPanelOverlay" style={{ display: this.layoutDoc.presStatus !== 'edit' ? 'inline-flex' : 'none' }}>
@@ -2400,7 +2399,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         const mode = StrCast(this.rootDoc._type_collection) as CollectionViewType;
         const presEnd = !this.layoutDoc.presLoop && this.itemIndex === this.childDocs.length - 1 && (this.activeItem.presIndexed === undefined || NumCast(this.activeItem.presIndexed) === (this.progressivizedItems(this.activeItem)?.length ?? 0));
         const presStart = !this.layoutDoc.presLoop && this.itemIndex === 0;
-        const inOverlay = DocListCast(Doc.MyOverlayDocs?.data).includes(this.layoutDoc);
         return this.props.addDocTab === returnFalse ? ( // bcz: hack!! - addDocTab === returnFalse only when this is being rendered by the OverlayView which means the doc is a mini player
             <div className="miniPres" onClick={e => e.stopPropagation()} onPointerEnter={action(e => (this._forceKeyEvents = true))}>
                 <div
@@ -2443,7 +2441,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                 </div>
             </div>
         ) : (
-            <div className="presBox-cont" style={{ minWidth: inOverlay ? PresBox.minimizedWidth : undefined }}>
+            <div className="presBox-cont" style={{ minWidth: Doc.IsInMyOverlay(this.rootDoc) ? PresBox.minimizedWidth : undefined }}>
                 {this.topPanel}
                 {this.toolbar}
                 {this.newDocumentToolbarDropdown}
diff --git a/src/client/views/nodes/trails/PresElementBox.tsx b/src/client/views/nodes/trails/PresElementBox.tsx
index 2279ffe84..1f40b82a3 100644
--- a/src/client/views/nodes/trails/PresElementBox.tsx
+++ b/src/client/views/nodes/trails/PresElementBox.tsx
@@ -322,11 +322,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
     };
 
     removeAllRecordingInOverlay = () => {
-        DocListCast(Doc.MyOverlayDocs.data).forEach(doc => {
-            if (doc.slides === this.rootDoc) {
-                Doc.RemoveDocFromList(Doc.MyOverlayDocs, undefined, doc);
-            }
-        });
+        DocListCast(Doc.MyOverlayDocs.data).filter(doc => doc.slides === this.rootDoc).forEach(Doc.RemFromMyOverlay);
     };
 
     static removeEveryExistingRecordingInOverlay = () => {
@@ -334,9 +330,9 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
         DocListCast(Doc.MyOverlayDocs.data).forEach(doc => {
             if (doc.slides !== null) {
                 // if it's a recording video, don't remove from overlay (user can lose data)
-                if (!PresElementBox.videoIsRecorded(DocCast(doc.slides))) return;
-
-                Doc.RemoveDocFromList(Doc.MyOverlayDocs, undefined, doc);
+                if (PresElementBox.videoIsRecorded(DocCast(doc.slides))) {
+                    Doc.RemFromMyOverlay(doc);
+                }
             }
         });
     };
@@ -356,7 +352,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
         if (!iconClick) PresElementBox.removeEveryExistingRecordingInOverlay();
 
         if (activeItem.recording) {
-            Doc.AddDocToList(Doc.MyOverlayDocs, undefined, DocCast(activeItem.recording));
+            Doc.AddToMyOverlay(DocCast(activeItem.recording));
         }
     };
 
@@ -368,7 +364,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
             // if we already have an existing recording
             this.showRecording(activeItem, true);
             //   // if we already have an existing recording
-            //   Doc.AddDocToList(Doc.MyOverlayDocs, undefined, Cast(activeItem.recording, Doc, null));
+            //   Doc.AddToMyOverlay(Cast(activeItem.recording, Doc, null));
         } else {
             // Remove every recording that already exists in overlay view
             // this is a design decision to clear to focus in on the recoding mode
@@ -392,7 +388,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
             // make recording box appear in the bottom right corner of the screen
             recording.overlayX = window.innerWidth - recording[WidthSym]() - 20;
             recording.overlayY = window.innerHeight - recording[HeightSym]() - 20;
-            Doc.AddDocToList(Doc.MyOverlayDocs, undefined, recording);
+            Doc.AddToMyOverlay(recording);
         }
     };
 
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index d8447deb6..128cfffb2 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -211,6 +211,15 @@ export class Doc extends RefField {
     public static get MyTrails() {
         return DocCast(Doc.ActiveDashboard?.myTrails);
     }
+    public static IsInMyOverlay(doc:Doc) {
+        return DocListCast(Doc.MyOverlayDocs?.data).includes(doc);
+    }
+    public static AddToMyOverlay(doc:Doc) {
+        Doc.AddDocToList(Doc.MyOverlayDocs, undefined, doc);
+    }
+    public static RemFromMyOverlay(doc:Doc) {
+        Doc.RemoveDocFromList(Doc.MyOverlayDocs, undefined, doc)
+    }
     public static get MyOverlayDocs() {
         return DocCast(Doc.UserDoc().myOverlayDocs);
     }
-- 
cgit v1.2.3-70-g09d2

