From b33ff1abdeab12d977c86f18c567ba3e84a49297 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Thu, 22 Feb 2024 16:34:32 -0500
Subject: replaced freeform document view wrapper with more direct caching of
 props.

---
 .../views/collections/collectionFreeForm/CollectionFreeFormView.tsx   | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

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

diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index a69030019..3670382c5 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -37,7 +37,7 @@ import { GestureOverlay } from '../../GestureOverlay';
 import { CtrlKey } from '../../GlobalKeyHandler';
 import { ActiveInkWidth, InkingStroke, SetActiveInkColor, SetActiveInkWidth } from '../../InkingStroke';
 import { LightboxView } from '../../LightboxView';
-import { CollectionFreeFormDocumentView, CollectionFreeFormDocumentViewWrapper } from '../../nodes/CollectionFreeFormDocumentView';
+import { CollectionFreeFormDocumentView } from '../../nodes/CollectionFreeFormDocumentView';
 import { SchemaCSVPopUp } from '../../nodes/DataVizBox/SchemaCSVPopUp';
 import { DocumentView, OpenWhere } from '../../nodes/DocumentView';
 import { FieldViewProps, FocusViewOptions } from '../../nodes/FieldView';
@@ -1181,7 +1181,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         const childLayout = entry.pair.layout;
         const childData = entry.pair.data;
         return (
-            <CollectionFreeFormDocumentViewWrapper
+            <CollectionFreeFormDocumentView
                 {...OmitKeys(entry, ['replica', 'pair']).omit}
                 key={childLayout[Id] + (entry.replica || '')}
                 Document={childLayout}
-- 
cgit v1.2.3-70-g09d2


From 27306bd0ae259241182d90cc62225609dfc8da74 Mon Sep 17 00:00:00 2001
From: geireann <geireann.lindfield@gmail.com>
Date: Sun, 25 Feb 2024 12:51:26 -0500
Subject: fixed conversion of doc to schema cells to allow number 0 to be
 converted to a string.

---
 .../views/collections/collectionFreeForm/CollectionFreeFormView.tsx   | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

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

diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 3670382c5..50a9feff8 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -1962,8 +1962,8 @@ ScriptingGlobals.add(function datavizFromSchema(doc: Doc) {
         for (let i = 0; i < children.length; i++) {
             let eachRow = [];
             for (let j = 0; j < keys.length; j++) {
-                var cell = children[i][keys[j]];
-                if (cell && (cell as string)) cell = cell.toString().replace(/\,/g, '');
+                var cell = children[i][keys[j]]?.toString();
+                if (cell) cell = cell.toString().replace(/\,/g, '');
                 eachRow.push(cell);
             }
             csvRows.push(eachRow);
-- 
cgit v1.2.3-70-g09d2


From d99f64efe9e69f2159f1ad8f851b24533a996ba5 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Mon, 26 Feb 2024 12:57:48 -0500
Subject: fixed some string types to be enumerations for dropAction.  fixed bug
 in golden layout dragging where a stack's tabs could disappear.

---
 src/client/documents/Documents.ts                  | 40 ++++++++++------
 src/client/goldenLayout.js                         |  2 +-
 src/client/util/CurrentUserUtils.ts                | 44 +++++++++---------
 src/client/util/DragManager.ts                     | 54 +++++++++++++++-------
 src/client/views/MainView.tsx                      |  6 +--
 src/client/views/collections/CollectionMenu.tsx    |  4 +-
 .../views/collections/CollectionPileView.tsx       |  3 +-
 .../views/collections/CollectionTreeView.tsx       | 16 +++++--
 src/client/views/collections/TreeView.tsx          |  4 +-
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  2 +-
 .../collections/collectionFreeForm/MarqueeView.tsx |  9 +++-
 .../collectionSchema/CollectionSchemaView.tsx      |  4 +-
 .../collectionSchema/SchemaTableCell.tsx           |  7 +--
 src/client/views/nodes/DocumentView.tsx            |  2 +-
 src/client/views/nodes/trails/PresBox.tsx          |  5 +-
 src/client/views/topbar/TopBar.tsx                 |  3 +-
 src/fields/documentSchemas.ts                      |  6 +--
 17 files changed, 129 insertions(+), 82 deletions(-)

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

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 2d2f5fe4a..8a13395c3 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -68,10 +68,20 @@ class EmptyBox {
         return '';
     }
 }
+
+export enum FInfoFieldType {
+    string,
+    boolean,
+    number,
+    Doc,
+    enumeration,
+    date,
+    list,
+}
 export class FInfo {
     description: string = '';
     readOnly: boolean = false;
-    fieldType?: string = '';
+    fieldType?: FInfoFieldType;
     values?: Field[];
 
     filterable?: boolean = true;
@@ -84,7 +94,7 @@ export class FInfo {
     searchable = () => true;
 }
 class BoolInfo extends FInfo {
-    fieldType? = 'boolean';
+    fieldType? = FInfoFieldType.boolean;
     values?: boolean[] = [true, false];
     constructor(d: string, filterable?: boolean) {
         super(d);
@@ -93,7 +103,7 @@ class BoolInfo extends FInfo {
     override searchable = () => false;
 }
 class NumInfo extends FInfo {
-    fieldType? = 'number';
+    fieldType? = FInfoFieldType.number;
     values?: number[] = [];
     constructor(d: string, filterable?: boolean, readOnly?: boolean, values?: number[]) {
         super(d, readOnly);
@@ -103,7 +113,7 @@ class NumInfo extends FInfo {
     override searchable = () => false;
 }
 class StrInfo extends FInfo {
-    fieldType? = 'string';
+    fieldType? = FInfoFieldType.string;
     values?: string[] = [];
     constructor(d: string, filterable?: boolean, readOnly?: boolean, values?: string[]) {
         super(d, readOnly);
@@ -112,7 +122,7 @@ class StrInfo extends FInfo {
     }
 }
 class DocInfo extends FInfo {
-    fieldType? = 'Doc';
+    fieldType? = FInfoFieldType.Doc;
     values?: Doc[] = [];
     constructor(d: string, filterable?: boolean, values?: Doc[]) {
         super(d, true);
@@ -122,45 +132,45 @@ class DocInfo extends FInfo {
     override searchable = () => false;
 }
 class DimInfo extends FInfo {
-    fieldType? = 'enumeration';
+    fieldType? = FInfoFieldType.enumeration;
     values? = [DimUnit.Pixel, DimUnit.Ratio];
     readOnly = false;
     filterable = false;
     override searchable = () => false;
 }
 class PEInfo extends FInfo {
-    fieldType? = 'enumeration';
+    fieldType? = FInfoFieldType.enumeration;
     values? = ['all', 'none'];
     readOnly = false;
     filterable = false;
     override searchable = () => false;
 }
 class DAInfo extends FInfo {
-    fieldType? = 'enumeration';
-    values? = ['embed', 'copy', 'move', 'same', 'proto', 'none'];
+    fieldType? = FInfoFieldType.enumeration;
+    values? = ['embed', 'copy', 'move', 'same', 'add', 'inSame', 'proto'];
     readOnly = false;
     filterable = false;
     override searchable = () => false;
 }
 class CTypeInfo extends FInfo {
-    fieldType? = 'enumeration';
+    fieldType? = FInfoFieldType.enumeration;
     values? = Array.from(Object.keys(CollectionViewType));
     readOnly = false;
     filterable = false;
     override searchable = () => false;
 }
 class DTypeInfo extends FInfo {
-    fieldType? = 'enumeration';
+    fieldType? = FInfoFieldType.enumeration;
     values? = Array.from(Object.keys(DocumentType));
     override searchable = () => false;
 }
 class DateInfo extends FInfo {
-    fieldType? = 'date';
+    fieldType? = FInfoFieldType.date;
     values?: DateField[] = [];
     filterable = true;
 }
 class ListInfo extends FInfo {
-    fieldType? = 'list';
+    fieldType? = FInfoFieldType.list;
     values?: List<any>[] = [];
 }
 type BOOLt = BoolInfo | boolean;
@@ -728,7 +738,7 @@ export namespace Docs {
                 {
                     data: '',
                     layout: { view: ComparisonBox, dataField: defaultDataKey },
-                    options: { backgroundColor: 'gray', dropAction: 'move', waitForDoubleClickToClick: 'always', layout_reflowHorizontal: true, layout_reflowVertical: true, layout_nativeDimEditable: true, systemIcon: 'BsLayoutSplit' },
+                    options: { backgroundColor: 'gray', dropAction: dropActionType.move, waitForDoubleClickToClick: 'always', layout_reflowHorizontal: true, layout_reflowVertical: true, layout_nativeDimEditable: true, systemIcon: 'BsLayoutSplit' },
                 },
             ],
             [
@@ -1182,7 +1192,7 @@ export namespace Docs {
             return InstanceFromProto(
                 Prototypes.get(DocumentType.COL),
                 new List(documents),
-                { backgroundColor: 'transparent', dropAction: 'move', _forceActive: true, _freeform_noZoom: true, _freeform_noAutoPan: true, ...options, _type_collection: CollectionViewType.Pile },
+                { backgroundColor: 'transparent', dropAction: dropActionType.move, _forceActive: true, _freeform_noZoom: true, _freeform_noAutoPan: true, ...options, _type_collection: CollectionViewType.Pile },
                 id
             );
         }
diff --git a/src/client/goldenLayout.js b/src/client/goldenLayout.js
index 2b94d35ee..cb1dfd76a 100644
--- a/src/client/goldenLayout.js
+++ b/src/client/goldenLayout.js
@@ -4727,7 +4727,7 @@
                  */
             } else {
                 type = isVertical ? 'column' : 'row';
-                if (this.parent.contentItems.length === 1) {
+                if (this.parent.contentItems.length === 1 && this.contentItems.length === 1) {
                     let grandparent = this.parent.parent;
                     let correctRowOrCol = this.layoutManager.createContentItem({ type: type }, this);
                     grandparent.replaceChild(this.parent, correctRowOrCol);
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index 714e33d25..d396ba815 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -14,7 +14,7 @@ import { SetCachedGroups, SharingPermissions } from "../../fields/util";
 import { GestureUtils } from "../../pen-gestures/GestureUtils";
 import { DocServer } from "../DocServer";
 import { CollectionViewType, DocumentType } from "../documents/DocumentTypes";
-import { DocUtils, Docs, DocumentOptions, FInfo } from "../documents/Documents";
+import { DocUtils, Docs, DocumentOptions, FInfo, FInfoFieldType } from "../documents/Documents";
 import { DashboardView } from "../views/DashboardView";
 import { OverlayView } from "../views/OverlayView";
 import { CollectionTreeView, TreeViewType } from "../views/collections/CollectionTreeView";
@@ -257,11 +257,11 @@ export class CurrentUserUtils {
             {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, treeView_HideUnrendered: true}},
             {key: "ViewSlide",  creator: slideView,                                         opts: { _width: 400, _height: 300, _xMargin: 3, _yMargin: 3,}},
-            {key: "Trail",      creator: Docs.Create.PresDocument,                          opts: { _width: 400, _height: 30, _type_collection: CollectionViewType.Stacking, dropAction: "embed" as dropActionType, treeView_HideTitle: true, _layout_fitWidth:true, layout_boxShadow: "0 0" }},
+            {key: "Trail",      creator: Docs.Create.PresDocument,                          opts: { _width: 400, _height: 30, _type_collection: CollectionViewType.Stacking, dropAction: dropActionType.embed, treeView_HideTitle: true, _layout_fitWidth:true, layout_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, _type_collection: CollectionViewType.Tree, 
                                                                                                 treeView_HasOverlay: true, _text_fontSize: "20px", _layout_autoHeight: true, 
-                                                                                                dropAction:'move', treeView_Type: TreeViewType.outline, 
+                                                                                                dropAction:dropActionType.move, treeView_Type: TreeViewType.outline, 
                                                                                                 backgroundColor: "white", _xMargin: 0, _yMargin: 0, _createDocOnCR: true 
                                                                                             }, funcs: {title: 'this.text?.Text'}},
             ];
@@ -311,7 +311,7 @@ export class CurrentUserUtils {
         const reqdOpts:DocumentOptions = {
             title: "Document Creators", _layout_showTitle: "title", _xMargin: 0, _dragOnlyWithinContainer: true, _layout_hideContextMenu: true, _chromeHidden: true, isSystem: true,
             _layout_autoHeight: true, _width: 500, _height: 300, _layout_fitWidth: true, _columnWidth: 40, ignoreClick: true, _lockedPosition: true, _forceActive: true,
-             childDragAction: 'embed' 
+             childDragAction: dropActionType.embed
         };
         const reqdScripts = { dropConverter: "convertToButtons(dragData)" };
         return DocUtils.AssignScripts(DocUtils.AssignOpts(dragCreatorDoc, reqdOpts, creatorBtns) ?? Docs.Create.MasonryDocument(creatorBtns, reqdOpts), reqdScripts);
@@ -353,7 +353,7 @@ export class CurrentUserUtils {
         });
 
         const reqdStackOpts:DocumentOptions ={
-            title: "menuItemPanel", childDragAction: "same", layout_boxShadow: "rgba(0,0,0,0)",  dontRegisterView: true, ignoreClick: true,
+            title: "menuItemPanel", childDragAction: dropActionType.same, layout_boxShadow: "rgba(0,0,0,0)",  dontRegisterView: true, ignoreClick: true,
             _chromeHidden: true, _gridGap: 0, _yMargin: 0, _xMargin: 0, _layout_autoHeight: false, _width: 60, _columnWidth: 60, _lockedPosition: true, isSystem: true,
         };
         return DocUtils.AssignDocField(doc, field, (opts, items) => Docs.Create.StackingDocument(items??[], opts), reqdStackOpts, menuBtns, { dropConverter: "convertToButtons(dragData)" });
@@ -436,7 +436,7 @@ export class CurrentUserUtils {
     /// Search option on the left side button panel
     static setupSearcher(doc: Doc, field:string) {
         return DocUtils.AssignDocField(doc, field, (opts, items) => Docs.Create.SearchDocument(opts), {
-                dontRegisterView: true, backgroundColor: "dimgray", ignoreClick: true, title: "Search Panel", isSystem: true, childDragAction: "embed", 
+                dontRegisterView: true, backgroundColor: "dimgray", ignoreClick: true, title: "Search Panel", isSystem: true, childDragAction: dropActionType.embed, 
                 _lockedPosition: true, _type_collection: CollectionViewType.Schema  });
     }
 
@@ -476,8 +476,8 @@ export class CurrentUserUtils {
         const childContextMenuIcons = ["tv", "camera", "users", "times", "trash"]; // entries must be kept in synch with childContextMenuScripts, childContextMenuLabels, and childContextMenuFilters
         const reqdOpts:DocumentOptions = {
             title: "My Dashboards", childHideLinkButton: true, treeView_FreezeChildren: "remove|add", treeView_HideTitle: true, layout_boxShadow: "0 0", childDontRegisterViews: true,
-            dropAction: "inSame", treeView_Type: TreeViewType.fileSystem, isFolder: true, isSystem: true,  treeView_TruncateTitleWidth: 350, ignoreClick: true, 
-            layout_headerButton: newDashboardButton, childDragAction: "inSame",
+            dropAction: dropActionType.inPlace, treeView_Type: TreeViewType.fileSystem, isFolder: true, isSystem: true,  treeView_TruncateTitleWidth: 350, ignoreClick: true, 
+            layout_headerButton: newDashboardButton, childDragAction: dropActionType.inPlace,
             _layout_showTitle: "title", _height: 400, _gridGap: 5, _forceActive: true, _lockedPosition: true,
             contextMenuLabels:new List<string>(contextMenuLabels),
             contextMenuIcons:new List<string>(contextMenuIcons),
@@ -510,9 +510,9 @@ export class CurrentUserUtils {
         const newFolderButton = DocUtils.AssignScripts(DocUtils.AssignOpts(DocCast(myFilesystem?.layout_headerButton), newFolderOpts) ?? Docs.Create.FontIconDocument(newFolderOpts), newFolderScript);
         
         const reqdOpts:DocumentOptions = { _layout_showTitle: "title", _height: 100, _forceActive: true, 
-            title: "My Documents", layout_headerButton: newFolderButton, treeView_HideTitle: true, dropAction: 'add', isSystem: true,
+            title: "My Documents", layout_headerButton: newFolderButton, treeView_HideTitle: true, dropAction: dropActionType.add, isSystem: true,
             isFolder: true, treeView_Type: TreeViewType.fileSystem, childHideLinkButton: true, layout_boxShadow: "0 0", childDontRegisterViews: true, 
-            treeView_TruncateTitleWidth: 350, ignoreClick: true, childDragAction: "embed",
+            treeView_TruncateTitleWidth: 350, ignoreClick: true, childDragAction: dropActionType.embed,
             layout_explainer: "This is your file manager where you can create folders to keep track of documents independently of your dashboard."
         };
         const fileFolders = new Set(DocListCast(DocCast(doc[field])?.data));
@@ -522,8 +522,8 @@ export class CurrentUserUtils {
     /// initializes the panel displaying docs that have been recently closed
     static setupRecentlyClosed(doc: Doc, field:string) {
         const reqdOpts:DocumentOptions = { _layout_showTitle: "title", _lockedPosition: true, _gridGap: 5, _forceActive: true, isFolder: true,
-            title: "My Recently Closed", childHideLinkButton: true, treeView_HideTitle: true, childDragAction: "move", isSystem: true,
-            treeView_TruncateTitleWidth: 350, ignoreClick: true,  layout_boxShadow: "0 0", childDontRegisterViews: true, dropAction: "same",
+            title: "My Recently Closed", childHideLinkButton: true, treeView_HideTitle: true, childDragAction: dropActionType.move, isSystem: true,
+            treeView_TruncateTitleWidth: 350, ignoreClick: true,  layout_boxShadow: "0 0", childDontRegisterViews: true, dropAction: dropActionType.same,
             contextMenuLabels: new List<string>(["Empty recently closed"]),
             contextMenuIcons:new List<string>(["trash"]),
             layout_explainer: "Recently closed documents appear in this menu. They will only be deleted if you explicity empty this list."
@@ -546,7 +546,7 @@ export class CurrentUserUtils {
     static setupUserDocView(doc: Doc, field:string) {
         const reqdOpts:DocumentOptions = {
             _lockedPosition: true, _gridGap: 5, _forceActive: true, title: Doc.CurrentUserEmail +"-view",
-            layout_boxShadow: "0 0", childDontRegisterViews: true, dropAction: "same", ignoreClick: true, isSystem: true,
+            layout_boxShadow: "0 0", childDontRegisterViews: true, dropAction: dropActionType.same, ignoreClick: true, isSystem: true,
             treeView_HideTitle: true, treeView_TruncateTitleWidth: 350
         };
         if (!doc[field]) DocUtils.AssignOpts(doc, {treeView_Open: true, treeView_ExpandedView: "fields" });
@@ -587,7 +587,7 @@ export class CurrentUserUtils {
         ];
         const btns = btnDescs.map(desc => dockBtn({_width: 30, _height: 30, defaultDoubleClick: 'ignore', undoIgnoreFields: new List<string>(['opacity']), _dragOnlyWithinContainer: true, ...desc.opts}, desc.scripts));
         const dockBtnsReqdOpts:DocumentOptions = {
-            title: "docked buttons", _height: 40, flexGap: 0, layout_boxShadow: "standard", childDragAction: 'move',
+            title: "docked buttons", _height: 40, flexGap: 0, layout_boxShadow: "standard", childDragAction: dropActionType.move,
             childDontRegisterViews: true, linearView_IsOpen: true, linearView_Expandable: true, ignoreClick: true
         };
         reaction(() => UndoManager.redoStack.slice(), () => Doc.GetProto(btns.find(btn => btn.title === "Redo")!).opacity = UndoManager.CanRedo() ? 1 : 0.4, { fireImmediately: true });
@@ -743,7 +743,7 @@ 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", undoIgnoreFields:new List<string>(['width', "linearView_IsOpen"]), flexGap: 0, childDragAction: 'embed', childDontRegisterViews: true,  linearView_IsOpen: true, ignoreClick: true, linearView_Expandable: false, _height: 35 };
+        const reqdCtxtOpts:DocumentOptions = { title: "context menu buttons", undoIgnoreFields:new List<string>(['width', "linearView_IsOpen"]), flexGap: 0, childDragAction: dropActionType.embed, childDontRegisterViews: true,  linearView_IsOpen: true, ignoreClick: true, linearView_Expandable: false, _height: 35 };
         const ctxtMenuBtnsDoc = DocUtils.AssignDocField(doc, field, (opts, items) => this.linearButtonList(opts, items??[]), reqdCtxtOpts, undefined);
         const ctxtMenuBtns = CurrentUserUtils.contextMenuTools().map(params => this.setupContextMenuBtn(params, ctxtMenuBtnsDoc) );
         return DocUtils.AssignOpts(ctxtMenuBtnsDoc, reqdCtxtOpts, ctxtMenuBtns);
@@ -769,7 +769,7 @@ export class CurrentUserUtils {
         ];
         const btns = btnDescs.map(desc => dockBtn({_width: desc.opts.width??30, _height: 30, defaultDoubleClick: 'ignore', undoIgnoreFields: new List<string>(['opacity']), _dragOnlyWithinContainer: true, ...desc.opts}, desc.scripts, desc.funcs));
         const dockBtnsReqdOpts:DocumentOptions = {
-            title: "docked buttons", _height: 40, flexGap: 0, layout_boxShadow: "standard", childDragAction: 'move',
+            title: "docked buttons", _height: 40, flexGap: 0, layout_boxShadow: "standard", childDragAction: dropActionType.move,
             childDontRegisterViews: true, linearView_IsOpen: true, linearView_Expandable: false, ignoreClick: true
         };
         return DocUtils.AssignDocField(doc, field, (opts, items) => this.linearButtonList(opts, items??[]), dockBtnsReqdOpts, btns);
@@ -809,7 +809,7 @@ export class CurrentUserUtils {
             // childContextMenuLabels: new List<string>(["Add to Dashboards",]),
             // childContextMenuIcons: new List<string>(["user-plus",]),
             "acl-Guest": SharingPermissions.Augment, "_acl-Guest": SharingPermissions.Augment,
-            childDragAction: "embed", isSystem: true, childContentPointerEvents: "none", childLimitHeight: 0, _yMargin: 0, _gridGap: 15, childDontRegisterViews:true,
+            childDragAction: dropActionType.embed, isSystem: true, childContentPointerEvents: "none", childLimitHeight: 0, _yMargin: 0, _gridGap: 15, childDontRegisterViews:true,
             // NOTE: treeView_HideTitle & _layout_showTitle is for a TreeView's editable title, _layout_showTitle is for DocumentViews title bar
             _layout_showTitle: "title", treeView_HideTitle: true, ignoreClick: true, _lockedPosition: true, layout_boxShadow: "0 0", _chromeHidden: true, dontRegisterView: true, 
             layout_explainer: "This is where documents or dashboards that other users have shared with you will appear. To share a document or dashboard right click and select 'Share'"
@@ -824,7 +824,7 @@ export class CurrentUserUtils {
         const reqdOpts:DocumentOptions = {
             title: "My Imports", _forceActive: true, _layout_showTitle: "title", childLayoutString: ImportElementBox.LayoutString('data'),
             _dragOnlyWithinContainer: true, _layout_hideContextMenu: true, childLimitHeight: 0, onClickScriptDisable:"never",
-            childDragAction: "copy", _layout_autoHeight: true, _yMargin: 50, _gridGap: 15, layout_boxShadow: "0 0", _lockedPosition: true, isSystem: true, _chromeHidden: true,
+            childDragAction: dropActionType.copy, _layout_autoHeight: true, _yMargin: 50, _gridGap: 15, layout_boxShadow: "0 0", _lockedPosition: true, isSystem: true, _chromeHidden: true,
             dontRegisterView: true, layout_explainer: "This is where documents that are Imported into Dash will go."
         };
         const myImports = DocUtils.AssignDocField(doc, field, (opts) => Docs.Create.MasonryDocument([], opts), reqdOpts, undefined, {onClick: "deselectAll()"});
@@ -886,7 +886,7 @@ export class CurrentUserUtils {
         this.setupDocTemplates(doc); // sets up the template menu of templates
         //this.setupFieldInfos(doc); // sets up the collection of field info descriptions for each possible DocumentOption
         DocUtils.AssignDocField(doc, "globalScriptDatabase", (opts) =>  Docs.Prototypes.MainScriptDocument(), {});
-        DocUtils.AssignDocField(doc, "myHeaderBar", (opts) => Docs.Create.MulticolumnDocument([], opts), { title: "My Header Bar", isSystem: true, _chromeHidden:true, childLayoutFitWidth:false, childDocumentsActive:false, dropAction: 'move'}); // drop down panel at top of dashboard for stashing documents
+        DocUtils.AssignDocField(doc, "myHeaderBar", (opts) => Docs.Create.MulticolumnDocument([], opts), { title: "My Header Bar", isSystem: true, _chromeHidden:true, childLayoutFitWidth:false, childDocumentsActive:false, dropAction: dropActionType.move}); // drop down panel at top of dashboard for stashing documents
         
         Doc.AddDocToList(Doc.MyFilesystem, undefined, Doc.MyDashboards)
         Doc.AddDocToList(Doc.MyFilesystem, undefined, Doc.MySharedDocs)
@@ -909,9 +909,9 @@ export class CurrentUserUtils {
                 const options = pair[1] as FInfo;
                 const opts:DocumentOptions = { isSystem: true, title: pair[0], ...OmitKeys(options, ["values"]).omit, fieldIsLayout: pair[0].startsWith("_")};
                 switch (options.fieldType) {
-                    case "boolean": opts.fieldValues = new List<boolean>(options.values as any); break;
-                    case "number": opts.fieldValues = new List<number>(options.values as any); break;
-                    case Doc.name: opts.fieldValues = new List<Doc>(options.values as any); break;
+                    case FInfoFieldType.boolean: opts.fieldValues = new List<boolean>(options.values as any); break;
+                    case FInfoFieldType.number: opts.fieldValues = new List<number>(options.values as any); break;
+                    case FInfoFieldType.Doc: opts.fieldValues = new List<Doc>(options.values as any); break;
                     default: opts.fieldValues = new List<string>(options.values as any); break;// string, pointerEvents, dimUnit, dropActionType
                 }
                 DocUtils.AssignDocField(infos, pair[0], opts => Doc.assign(new Doc(), OmitKeys(opts,["values"]).omit), opts);
diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts
index 1f093a33c..f6af6196f 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -1,10 +1,24 @@
+/**
+ * The DragManager handles all dragging interactions that occur entirely within Dash (as opposed to external drag operations from the file system, etc)
+ *
+ * Events are generated for
+ *  a pause in the drag movement (dashDragMovePause) as a Doc(s) is dragged,
+ *  just before (dashPreDrop) a Doc(s) is dropped,
+ *  and just after (dashDropEvent) a Doc(s) is dropped
+ * If the document is dragged and paused over the golden layout header tabs, the
+ *   drag interaction will switch to a golden layout tab drag.
+ *
+ * All drag operations can be aborted by hitting the Esc key
+ *
+ */
+
 import { action, observable, runInAction } from 'mobx';
 import { DateField } from '../../fields/DateField';
 import { Doc, Field, Opt, StrListCast } from '../../fields/Doc';
 import { List } from '../../fields/List';
 import { PrefetchProxy } from '../../fields/Proxy';
 import { ScriptField } from '../../fields/ScriptField';
-import { ScriptCast, StrCast } from '../../fields/Types';
+import { ScriptCast } from '../../fields/Types';
 import { emptyFunction, Utils } from '../../Utils';
 import { Docs, DocUtils } from '../documents/Documents';
 import { CollectionFreeFormDocumentView } from '../views/nodes/CollectionFreeFormDocumentView';
@@ -16,7 +30,15 @@ import { UndoManager } from './UndoManager';
 import { DocData } from '../../fields/DocSymbols';
 const { default : { contextMenuZindex } } = require('../views/global/globalCssVariables.module.scss'); // prettier-ignore
 
-export type dropActionType = 'embed' | 'copy' | 'move' | 'add' | 'same' | 'inSame' | 'proto' | 'none' | undefined; // undefined = move, "same" = move but don't call dropPropertiesToRemove
+export enum dropActionType {
+    embed = 'embed', // create a new embedding of the dragged document for the new location
+    copy = 'copy', // copy the dragged document
+    move = 'move', // move the dragged document to the drop location after removing it from where it was
+    add = 'add', // add the dragged document to the drop location without removing it from where it was
+    same = 'same', // only allow drop within same collection (or same hierarchical tree collection)
+    inPlace = 'inSame', // keep document in place (unless overridden by a drag modifier)
+    proto = 'proto',
+} // undefined = move, same = move but doesn't call dropPropertiesToRemove
 
 /**
  * Initialize drag
@@ -129,9 +151,9 @@ export namespace DragManager {
         treeViewDoc?: Doc;
         offset: number[];
         canEmbed?: boolean;
-        userDropAction: dropActionType; // the user requested drop action -- this will be honored as specified by modifier keys
+        userDropAction?: dropActionType; // the user requested drop action -- this will be honored as specified by modifier keys
         defaultDropAction?: dropActionType; // an optionally specified default drop action when there is no user drop actionl - this will be honored if there is no user drop action
-        dropAction: dropActionType; // a drop action request by the initiating code.  the actual drop action may be different -- eg, if the request is 'embed', but the document is dropped within the same collection, the drop action will be switched to 'move'
+        dropAction?: dropActionType; // a drop action request by the initiating code.  the actual drop action may be different -- eg, if the request is 'embed', but the document is dropped within the same collection, the drop action will be switched to 'move'
         dropPropertiesToRemove?: string[];
         moveDocument?: MoveFunction;
         removeDocument?: RemoveFunction;
@@ -170,8 +192,8 @@ export namespace DragManager {
         dropDocCreator: (annotationOn: Doc | undefined) => Doc;
         dropDocument?: Doc;
         offset: number[];
-        dropAction: dropActionType;
-        userDropAction: dropActionType;
+        dropAction?: dropActionType;
+        userDropAction?: dropActionType;
     }
 
     let defaultPreDropFunc = (e: Event, de: DragManager.DropEvent, targetAction: dropActionType) => {
@@ -189,7 +211,7 @@ export namespace DragManager {
         const handler = (e: Event) => dropFunc(e, (e as CustomEvent<DropEvent>).detail);
         const preDropHandler = (e: Event) => {
             const de = (e as CustomEvent<DropEvent>).detail;
-            (preDropFunc ?? defaultPreDropFunc)(e, de, StrCast(doc.dropAction) as dropActionType);
+            (preDropFunc ?? defaultPreDropFunc)(e, de, doc.dropAction as any as dropActionType);
         };
         element.addEventListener('dashOnDrop', handler);
         element.addEventListener('dashPreDrop', preDropHandler);
@@ -218,19 +240,19 @@ export namespace DragManager {
                         dragData.draggedDocuments.map(async d =>
                             !dragData.isDocDecorationMove && !dragData.userDropAction && ScriptCast(d.onDragStart)
                                 ? addAudioTag(ScriptCast(d.onDragStart).script.run({ this: d }).result)
-                                : docDragData.dropAction === 'embed'
+                                : docDragData.dropAction === dropActionType.embed
                                   ? Doc.BestEmbedding(d)
-                                  : docDragData.dropAction === 'add'
+                                  : docDragData.dropAction === dropActionType.add
                                     ? d
-                                    : docDragData.dropAction === 'proto'
+                                    : docDragData.dropAction === dropActionType.proto
                                       ? d[DocData]
-                                      : docDragData.dropAction === 'copy'
+                                      : docDragData.dropAction === dropActionType.copy
                                         ? (await Doc.MakeClone(d)).clone
                                         : d
                         )
                     )
                 ).filter(d => d);
-                !['same', 'proto'].includes(docDragData.dropAction as any) &&
+                ![dropActionType.same, dropActionType.proto].includes(docDragData.dropAction as any) &&
                     docDragData.droppedDocuments
                         // .filter(drop => !drop.dragOnlyWithinContainer || ['embed', 'copy'].includes(docDragData.dropAction as any))
                         .forEach((drop: Doc, i: number) => {
@@ -504,7 +526,7 @@ export namespace DragManager {
         const moveHandler = (e: PointerEvent) => {
             e.preventDefault(); // required or dragging text menu link item ends up dragging the link button as native drag/drop
             if (dragData instanceof DocumentDragData) {
-                dragData.userDropAction = e.ctrlKey && e.altKey ? 'copy' : e.shiftKey ? 'move' : e.ctrlKey ? 'embed' : dragData.defaultDropAction;
+                dragData.userDropAction = e.ctrlKey && e.altKey ? dropActionType.copy : e.shiftKey ? dropActionType.move : e.ctrlKey ? dropActionType.embed : dragData.defaultDropAction;
             }
             if (['lm_tab', 'lm_title_wrap', 'lm_tabs', 'lm_header'].includes(typeof (e.target as any).className === 'string' ? (e.target as any)?.className : '') && dragData.draggedDocuments.length === 1) {
                 if (!startWindowDragTimer) {
@@ -530,7 +552,7 @@ export namespace DragManager {
             if (target && !Doc.UserDoc()._noAutoscroll && !options?.noAutoscroll && !dragData.draggedDocuments?.some((d: any) => d._freeform_noAutoPan)) {
                 const autoScrollHandler = () => {
                     target.dispatchEvent(
-                        new CustomEvent<React.DragEvent>('dashDragAutoScroll', {
+                        new CustomEvent<React.DragEvent>('dashDragMovePause', {
                             bubbles: true,
                             detail: {
                                 shiftKey: e.shiftKey,
@@ -552,7 +574,7 @@ export namespace DragManager {
                                 screenY: e.screenY,
                                 detail: e.detail,
                                 view: e.view ? e.view : (new Window() as any),
-                                nativeEvent: new DragEvent('dashDragAutoScroll'),
+                                nativeEvent: new DragEvent('dashDragMovePause'),
                                 currentTarget: target,
                                 target: target,
                                 bubbles: true,
@@ -566,7 +588,7 @@ export namespace DragManager {
                                 isPropagationStopped: () => ('not implemented for this event' ? false : false),
                                 persist: emptyFunction,
                                 timeStamp: e.timeStamp,
-                                type: 'dashDragAutoScroll',
+                                type: 'dashDragMovePause',
                             },
                         })
                     );
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index b6cb845a6..3be52597a 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -19,7 +19,7 @@ import { Docs } from '../documents/Documents';
 import { CalendarManager } from '../util/CalendarManager';
 import { CaptureManager } from '../util/CaptureManager';
 import { DocumentManager } from '../util/DocumentManager';
-import { DragManager } from '../util/DragManager';
+import { DragManager, dropActionType } from '../util/DragManager';
 import { GroupManager } from '../util/GroupManager';
 import { HistoryUtil } from '../util/History';
 import { Hypothesis } from '../util/HypothesisUtils';
@@ -623,7 +623,7 @@ export class MainView extends ObservableReactComponent<{}> {
                     isContentActive={returnTrue} // headerBar is awlays contentActive which means its items are always documentActive
                     ScreenToLocalTransform={this.headerBarScreenXf}
                     childHideResizeHandles={true}
-                    childDragAction="move"
+                    childDragAction={dropActionType.move}
                     dontRegisterView={true}
                     hideResizeHandles={true}
                     PanelWidth={this.headerBarDocWidth}
@@ -905,7 +905,7 @@ export class MainView extends ObservableReactComponent<{}> {
                     Document={Doc.MyDockedBtns}
                     docViewPath={returnEmptyDocViewList}
                     fieldKey="data"
-                    dropAction="embed"
+                    dropAction={dropActionType.embed}
                     styleProvider={DefaultStyleProvider}
                     select={emptyFunction}
                     isAnyChildContentActive={returnFalse}
diff --git a/src/client/views/collections/CollectionMenu.tsx b/src/client/views/collections/CollectionMenu.tsx
index 0f90818ef..8729ef549 100644
--- a/src/client/views/collections/CollectionMenu.tsx
+++ b/src/client/views/collections/CollectionMenu.tsx
@@ -11,7 +11,7 @@ import { RichTextField } from '../../../fields/RichTextField';
 import { BoolCast, Cast, DocCast, NumCast, StrCast } from '../../../fields/Types';
 import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue, setupMoveUpEvents, Utils } from '../../../Utils';
 import { CollectionViewType, DocumentType } from '../../documents/DocumentTypes';
-import { DragManager } from '../../util/DragManager';
+import { DragManager, dropActionType } from '../../util/DragManager';
 import { SelectionManager } from '../../util/SelectionManager';
 import { SettingsManager } from '../../util/SettingsManager';
 import { Transform } from '../../util/Transform';
@@ -91,7 +91,7 @@ export class CollectionMenu extends AntimodeMenu<CollectionMenuProps> {
                     Document={selDoc}
                     docViewPath={returnEmptyDocViewList}
                     fieldKey="data"
-                    dropAction="embed"
+                    dropAction={dropActionType.embed}
                     styleProvider={DefaultStyleProvider}
                     select={emptyFunction}
                     isContentActive={returnTrue}
diff --git a/src/client/views/collections/CollectionPileView.tsx b/src/client/views/collections/CollectionPileView.tsx
index d0df77cbe..7d7f0bb61 100644
--- a/src/client/views/collections/CollectionPileView.tsx
+++ b/src/client/views/collections/CollectionPileView.tsx
@@ -13,6 +13,7 @@ import { computePassLayout, computeStarburstLayout } from './collectionFreeForm'
 import { CollectionFreeFormView } from './collectionFreeForm/CollectionFreeFormView';
 import './CollectionPileView.scss';
 import { CollectionSubView } from './CollectionSubView';
+import { dropActionType } from '../../util/DragManager';
 
 @observer
 export class CollectionPileView extends CollectionSubView() {
@@ -72,7 +73,7 @@ export class CollectionPileView extends CollectionSubView() {
                     // pile children never have their contents active, but will be document active whenever the entire pile is.
                     childContentsActive={returnFalse}
                     childDocumentsActive={this._props.isDocumentActive}
-                    childDragAction="move"
+                    childDragAction={dropActionType.move}
                     childClickScript={this.toggleIcon}
                 />
             </div>
diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx
index 786301136..4d60cbefc 100644
--- a/src/client/views/collections/CollectionTreeView.tsx
+++ b/src/client/views/collections/CollectionTreeView.tsx
@@ -153,12 +153,18 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree
         return res;
     }
 
-    protected onInternalPreDrop = (e: Event, de: DragManager.DropEvent, dropAction: dropActionType) => {
+    protected onInternalPreDrop = (e: Event, de: DragManager.DropEvent, targetDropAction: dropActionType) => {
         const dragData = de.complete.docDragData;
         if (dragData) {
-            const sameTree = Doc.AreProtosEqual(dragData.treeViewDoc, this.Document) ? true : false;
+            const sourceDragAction = dragData.dropAction;
+            const sameTree = () => Doc.AreProtosEqual(dragData.treeViewDoc, this.Document);
             const isAlreadyInTree = () => sameTree || dragData.draggedDocuments.some(d => d.embedContainer === this.Document && this.childDocs.includes(d));
-            dragData.dropAction = dropAction && !isAlreadyInTree() ? dropAction : sameTree && dragData.dropAction !== 'inSame' ? 'same' : dragData.dropAction;
+            dragData.dropAction =
+                targetDropAction && !isAlreadyInTree() // if dropped document is not in the tree
+                    ? targetDropAction // then use the target's drop action if it's specified
+                    : !sameTree() || sourceDragAction === dropActionType.inPlace // if doc from another tree, or a non inPlace source drag action is specified
+                      ? sourceDragAction // use the source dragAction
+                      : dropActionType.same; // otherwise use same tree semantics to move within tree
             e.stopPropagation();
         }
     };
@@ -287,7 +293,7 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree
     @observable _renderCount = 1;
     @computed get treeViewElements() {
         TraceMobx();
-        const dragAction = StrCast(this.Document.childDragAction) as dropActionType;
+        const dragAction = StrCast(this.Document.childDragAction) as any as dropActionType;
         const addDoc = (doc: Doc | Doc[], relativeTo?: Doc, before?: boolean) => this.addDoc(doc, relativeTo, before);
         const moveDoc = (d: Doc | Doc[], target: Doc | undefined, addDoc: (doc: Doc | Doc[]) => boolean) => this._props.moveDocument?.(d, target, addDoc) || false;
         if (this._renderCount < this.treeChildren.length) setTimeout(action(() => (this._renderCount = Math.min(this.treeChildren.length, this._renderCount + 20))));
@@ -473,7 +479,7 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree
                         isAnnotationOverlayScrollable={true}
                         childDocumentsActive={this._props.isContentActive}
                         fieldKey={this._props.fieldKey + '_annotations'}
-                        dropAction="move"
+                        dropAction={dropActionType.move}
                         select={emptyFunction}
                         addDocument={this.addAnnotationDocument}
                         removeDocument={this.remAnnotationDocument}
diff --git a/src/client/views/collections/TreeView.tsx b/src/client/views/collections/TreeView.tsx
index 85f7cf7fe..4f2d8b9c0 100644
--- a/src/client/views/collections/TreeView.tsx
+++ b/src/client/views/collections/TreeView.tsx
@@ -439,7 +439,7 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> {
         droppedDocuments: Doc[],
         before: boolean,
         inside: number | boolean,
-        dropAction: dropActionType,
+        dropAction: dropActionType | undefined,
         removeDocument: DragManager.RemoveFunction | undefined,
         moveDocument: DragManager.MoveFunction | undefined,
         forceAdd: boolean,
@@ -1161,7 +1161,7 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> {
         const before = pt[1] < rect.top + rect.height / 2;
         const inside = this.treeView.fileSysMode && !this.Document.isFolder ? false : pt[0] > rect.left + rect.width * 0.33 || (!before && this.treeViewOpen && this.childDocs?.length ? true : false);
 
-        const docs = this.treeView.onTreeDrop(de, (docs: Doc[]) => this.dropDocuments(docs, before, inside, 'copy', undefined, undefined, false, false));
+        this.treeView.onTreeDrop(de, (docs: Doc[]) => this.dropDocuments(docs, before, inside, dropActionType.copy, undefined, undefined, false, false));
     };
 
     render() {
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 50a9feff8..50b3dbd70 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -462,7 +462,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                 const eles = this.childLayoutPairs.map(pair => pair.layout).filter(cd => (this.Document._freeform_useClusters ? NumCast(cd.layout_cluster) : NumCast(cd.group, -1)) === cluster);
                 const clusterDocs = eles.map(ele => DocumentManager.Instance.getDocumentView(ele, this.DocumentView?.())!);
                 const { left, top } = clusterDocs[0].getBounds || { left: 0, top: 0 };
-                const de = new DragManager.DocumentDragData(eles, e.ctrlKey || e.altKey ? 'embed' : undefined);
+                const de = new DragManager.DocumentDragData(eles, e.ctrlKey || e.altKey ? dropActionType.embed : undefined);
                 de.moveDocument = this._props.moveDocument;
                 de.offset = this.screenToFreeformContentsXf.transformDirection(ptsParent.clientX - left, ptsParent.clientY - top);
                 DragManager.StartDocumentDrag(
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index a417d777a..d0e59180d 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -635,8 +635,13 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
     }
     MarqueeRef: HTMLDivElement | null = null;
 
+    /**
+     * This is called for every drag movement when a document is dragged over this collection.
+     * If the document is dragged within 25 pixels of the edge of the collection and paused, this will
+     * auto scroll the collection so that it can be dragged farther (unless auto panning has been disabled)
+     */
     @action
-    onDragAutoScroll = (e: CustomEvent<React.DragEvent>) => {
+    onDragMovePause = (e: CustomEvent<React.DragEvent>) => {
         if ((e as any).handlePan || this._props.isAnnotationOverlay) return;
         (e as any).handlePan = true;
 
@@ -659,7 +664,7 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
             <div
                 className="marqueeView"
                 ref={r => {
-                    r?.addEventListener('dashDragAutoScroll', this.onDragAutoScroll as any);
+                    r?.addEventListener('dashDragMovePause', this.onDragMovePause as any);
                     this.MarqueeRef = r;
                 }}
                 style={{
diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx
index 31b4a2dd4..eee3836d2 100644
--- a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx
+++ b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx
@@ -10,7 +10,7 @@ import { BoolCast, Cast, DocCast, NumCast, StrCast } from '../../../../fields/Ty
 import { emptyFunction, returnEmptyDoclist, returnEmptyString, returnFalse, returnIgnore, returnNever, returnTrue, setupMoveUpEvents, smoothScroll } from '../../../../Utils';
 import { Docs, DocumentOptions, DocUtils, FInfo } from '../../../documents/Documents';
 import { DocumentManager } from '../../../util/DocumentManager';
-import { DragManager } from '../../../util/DragManager';
+import { DragManager, dropActionType } from '../../../util/DragManager';
 import { SelectionManager } from '../../../util/SelectionManager';
 import { undoable, undoBatch } from '../../../util/UndoManager';
 import { ContextMenu } from '../../ContextMenu';
@@ -981,7 +981,7 @@ class CollectionSchemaViewDoc extends ObservableReactComponent<CollectionSchemaV
                 styleProvider={this.noOpacityStyleProvider}
                 waitForDoubleClickToClick={returnNever}
                 defaultDoubleClick={returnIgnore}
-                dragAction="move"
+                dragAction={dropActionType.move}
                 onClickScriptDisable="always"
                 focus={this._props.schema.focusDocument}
                 childFilters={this._props.schema.childDocFilters}
diff --git a/src/client/views/collections/collectionSchema/SchemaTableCell.tsx b/src/client/views/collections/collectionSchema/SchemaTableCell.tsx
index 001ad5ab6..46867665d 100644
--- a/src/client/views/collections/collectionSchema/SchemaTableCell.tsx
+++ b/src/client/views/collections/collectionSchema/SchemaTableCell.tsx
@@ -10,7 +10,7 @@ import { Doc, DocListCast, Field } from '../../../../fields/Doc';
 import { RichTextField } from '../../../../fields/RichTextField';
 import { BoolCast, Cast, DateCast, DocCast, FieldValue, StrCast } from '../../../../fields/Types';
 import { ImageField } from '../../../../fields/URLField';
-import { FInfo } from '../../../documents/Documents';
+import { FInfo, FInfoFieldType } from '../../../documents/Documents';
 import { DocFocusOrOpen } from '../../../util/DocumentManager';
 import { Transform } from '../../../util/Transform';
 import { undoBatch, undoable } from '../../../util/UndoManager';
@@ -29,6 +29,7 @@ import { Popup, Size, Type } from 'browndash-components';
 import { IconLookup, faCaretDown } from '@fortawesome/free-solid-svg-icons';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { SettingsManager } from '../../../util/SettingsManager';
+import { dropActionType } from '../../../util/DragManager';
 
 export interface SchemaTableCellProps {
     Document: Doc;
@@ -86,7 +87,7 @@ export class SchemaTableCell extends ObservableReactComponent<SchemaTableCellPro
             isSelected: returnFalse,
             setHeight: returnFalse,
             select: emptyFunction,
-            dragAction: 'move',
+            dragAction: dropActionType.move,
             renderDepth: 1,
             isContentActive: returnFalse,
             whenChildContentsActiveChanged: emptyFunction,
@@ -149,7 +150,7 @@ export class SchemaTableCell extends ObservableReactComponent<SchemaTableCellPro
         if (cellValue instanceof DateField) return ColumnType.Date;
         if (cellValue instanceof RichTextField) return ColumnType.RTF;
         if (typeof cellValue === 'number') return ColumnType.Any;
-        if (typeof cellValue === 'string' && columnTypeStr !== 'enumeration') return ColumnType.Any;
+        if (typeof cellValue === 'string' && columnTypeStr !== FInfoFieldType.enumeration) return ColumnType.Any;
         if (typeof cellValue === 'boolean') return ColumnType.Boolean;
 
         if (columnTypeStr && columnTypeStr in FInfotoColType) {
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index d131f72d5..abb1aa59f 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -413,7 +413,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
         if (!Utils.isClick(e.clientX, e.clientY, this._downX, this._downY, Date.now())) {
             this.cleanupPointerEvents();
             this._longPressSelector && clearTimeout(this._longPressSelector);
-            this.startDragging(this._downX, this._downY, ((e.ctrlKey || e.altKey) && 'embed') || ((this.Document.dragAction || this._props.dragAction || undefined) as dropActionType));
+            this.startDragging(this._downX, this._downY, ((e.ctrlKey || e.altKey) && dropActionType.embed) || ((this.Document.dragAction || this._props.dragAction || undefined) as dropActionType));
         }
     };
 
diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx
index b8f6575dd..918987034 100644
--- a/src/client/views/nodes/trails/PresBox.tsx
+++ b/src/client/views/nodes/trails/PresBox.tsx
@@ -36,6 +36,7 @@ import { FocusViewOptions, FieldView, FieldViewProps } from '../FieldView';
 import { ScriptingBox } from '../ScriptingBox';
 import './PresBox.scss';
 import { PresEffect, PresEffectDirection, PresMovement, PresStatus } from './PresEnums';
+import { dropActionType } from '../../../util/DragManager';
 export interface pinDataTypes {
     scrollable?: boolean;
     dataviz?: number[];
@@ -1074,7 +1075,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
     };
 
     childLayoutTemplate = () => Docs.Create.PresElementBoxDocument();
-    removeDocument = (doc: Doc) => Doc.RemoveDocFromList(this.Document, this.fieldKey, doc);
+    removeDocument = (doc: Doc | Doc[]) => !(doc instanceof Doc ? [doc] : doc).map(d => Doc.RemoveDocFromList(this.Document, this.fieldKey, d)).some(p => !p);
     getTransform = () => this.ScreenToLocalBoxXf().translate(-5, -65); // listBox padding-left and pres-box-cont minHeight
     panelHeight = () => this._props.PanelHeight() - 40;
     /**
@@ -2607,7 +2608,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                                 childIgnoreNativeSize={true}
                                 moveDocument={returnFalse}
                                 ignoreUnrendered={true}
-                                childDragAction="move"
+                                childDragAction={dropActionType.move}
                                 setContentViewBox={emptyFunction}
                                 //childLayoutFitWidth={returnTrue}
                                 childOpacity={returnOne}
diff --git a/src/client/views/topbar/TopBar.tsx b/src/client/views/topbar/TopBar.tsx
index 4155800b8..0952dda20 100644
--- a/src/client/views/topbar/TopBar.tsx
+++ b/src/client/views/topbar/TopBar.tsx
@@ -25,6 +25,7 @@ import { Colors } from '../global/globalEnums';
 import { DocumentViewInternal, returnEmptyDocViewList } from '../nodes/DocumentView';
 import { DefaultStyleProvider } from '../StyleProvider';
 import './TopBar.scss';
+import { dropActionType } from '../../util/DragManager';
 
 /**
  * ABOUT: This is the topbar in Dash, which included the current Dashboard as well as access to information on the user
@@ -102,7 +103,7 @@ export class TopBar extends React.Component {
                     Document={selDoc}
                     docViewPath={returnEmptyDocViewList}
                     fieldKey="data"
-                    dropAction="embed"
+                    dropAction={dropActionType.embed}
                     styleProvider={DefaultStyleProvider}
                     select={emptyFunction}
                     isContentActive={returnTrue}
diff --git a/src/fields/documentSchemas.ts b/src/fields/documentSchemas.ts
index 8eeb52709..1cacfe30c 100644
--- a/src/fields/documentSchemas.ts
+++ b/src/fields/documentSchemas.ts
@@ -96,9 +96,9 @@ export const documentSchema = createSchema({
     // drag drop properties
     _dragOnlyWithinContainer: 'boolean', // whether document can be dropped into a different collection
     dragFactory: Doc, // the document that serves as the "template" for the onDragStart script.  ie, to drag out copies of the dragFactory document.
-    dropAction: 'string', // override specifying what should happen when something is dropped on this document (can be "embed", "copy", "move")
-    dragAction: 'string', // override specifying what should happen when this document s dragged (can be "embed", "copy", "move")
-    childDragAction: 'string', // specify the override for what should happen when the child of a collection is dragged from it and dropped (can be "embed" or "copy")
+    dropAction: 'string', // override specifying what should happen when something is dropped on this document (dropActionType)
+    dragAction: 'string', // override specifying what should happen when this document s dragged (dropActionType)
+    childDragAction: 'string', // specify the override for what should happen when the child of a collection is dragged from it and dropped (dropActionType)
     dropPropertiesToRemove: listSpec('string'), // properties that should be removed from the embed/copy/etc of this document when it is dropped
 });
 
-- 
cgit v1.2.3-70-g09d2


From e973de1476194fe68a2f218e458e4619fb71c75e Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Thu, 29 Feb 2024 09:34:56 -0500
Subject: fixed minimap to show at least as much as the freeformview is showing
 (was a problem for example when there was just a single document)

---
 src/client/views/collections/TabDocView.tsx                       | 8 ++++++++
 .../collections/collectionFreeForm/CollectionFreeFormView.tsx     | 4 ++--
 2 files changed, 10 insertions(+), 2 deletions(-)

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

diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx
index 02aa76d82..b52f2e7b6 100644
--- a/src/client/views/collections/TabDocView.tsx
+++ b/src/client/views/collections/TabDocView.tsx
@@ -564,6 +564,12 @@ export class TabMinimapView extends ObservableReactComponent<TabMinimapViewProps
         const dim = Math.max(xbounds, ybounds);
         return { l: bounds.x + xbounds / 2 - dim / 2, t: bounds.y + ybounds / 2 - dim / 2, cx: bounds.x + xbounds / 2, cy: bounds.y + ybounds / 2, dim };
     }
+    @computed get xPadding() {
+        return !this.renderBounds ? 0 : Math.max(0, this._props.PanelWidth() / NumCast(this._props.document._freeform_scale, 1) - 2 * (this.renderBounds.cx - this.renderBounds.l));
+    }
+    @computed get yPadding() {
+        return !this.renderBounds ? 0 : Math.max(0, this._props.PanelHeight() / NumCast(this._props.document._freeform_scale, 1) - 2 * (this.renderBounds.cy - this.renderBounds.l));
+    }
     childLayoutTemplate = () => Cast(this._props.document.childLayoutTemplate, Doc, null);
     returnMiniSize = () => NumCast(this._props.document._miniMapSize, 150);
     miniDown = (e: React.PointerEvent) => {
@@ -620,6 +626,8 @@ export class TabMinimapView extends ObservableReactComponent<TabMinimapViewProps
                     childFiltersByRanges={CollectionDockingView.Instance?.childDocRangeFilters ?? returnEmptyDoclist}
                     searchFilterDocs={CollectionDockingView.Instance?.searchFilterDocs ?? returnEmptyDoclist}
                     fitContentsToBox={returnTrue}
+                    xPadding={this.xPadding}
+                    yPadding={this.yPadding}
                 />
                 <div className="miniOverlay" onPointerDown={this.miniDown}>
                     <TabMiniThumb miniLeft={miniLeft} miniTop={miniTop} miniWidth={miniWidth} miniHeight={miniHeight} />
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 50b3dbd70..e48656f5e 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -159,8 +159,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
             ? { x: cb[0], y: cb[1], r: cb[2], b: cb[3] }
             : aggregateBounds(
                   this._layoutElements.filter(e => e.bounds?.width && !e.bounds.z).map(e => e.bounds!),
-                  NumCast(this.layoutDoc._xPadding, 10),
-                  NumCast(this.layoutDoc._yPadding, 10)
+                  NumCast(this.layoutDoc._xPadding, this._props.xPadding ?? 10),
+                  NumCast(this.layoutDoc._yPadding, this._props.yPadding ?? 10)
               );
     }
     @computed get nativeWidth() {
-- 
cgit v1.2.3-70-g09d2


From 49cfa95fdb3653a1224d4e5e3380e111c8e2014f Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Mon, 4 Mar 2024 12:55:05 -0500
Subject: changes to restore template menu and fix freeform templates to always
 show contents.   Fix for alt-dropping an image to replace another image.

---
 src/client/views/DocumentButtonBar.tsx             | 48 +++++++++-------------
 src/client/views/DocumentDecorations.tsx           |  4 +-
 src/client/views/InkingStroke.tsx                  |  2 +-
 src/client/views/TemplateMenu.tsx                  |  2 +-
 src/client/views/collections/TabDocView.tsx        |  2 +-
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  9 ++--
 src/client/views/nodes/ImageBox.tsx                |  1 +
 7 files changed, 30 insertions(+), 38 deletions(-)

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

diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx
index 8a4b42ae0..d65e0b406 100644
--- a/src/client/views/DocumentButtonBar.tsx
+++ b/src/client/views/DocumentButtonBar.tsx
@@ -4,12 +4,12 @@ import { Tooltip } from '@mui/material';
 import { action, computed, makeObservable, observable, runInAction } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
-import { emptyFunction, returnFalse, setupMoveUpEvents, simulateMouseClick } from '../../Utils';
+import { emptyFunction, returnFalse, returnTrue, setupMoveUpEvents, simulateMouseClick } from '../../Utils';
 import { Doc } from '../../fields/Doc';
 import { Cast, DocCast } from '../../fields/Types';
 import { DocUtils } from '../documents/Documents';
 import { CalendarManager } from '../util/CalendarManager';
-import { DragManager } from '../util/DragManager';
+import { DragManager, dropActionType } from '../util/DragManager';
 import { IsFollowLinkScript } from '../util/LinkFollower';
 import { SelectionManager } from '../util/SelectionManager';
 import { SharingManager } from '../util/SharingManager';
@@ -24,6 +24,9 @@ import { DocumentView, DocumentViewInternal, OpenWhere } from './nodes/DocumentV
 import { DashFieldView } from './nodes/formattedText/DashFieldView';
 import { PinProps } from './nodes/trails';
 import { faCalendarDays } from '@fortawesome/free-solid-svg-icons';
+import { Popup } from 'browndash-components';
+import { TemplateMenu } from './TemplateMenu';
+import { FaEdit } from 'react-icons/fa';
 
 @observer
 export class DocumentButtonBar extends ObservableReactComponent<{ views: () => (DocumentView | undefined)[]; stack?: any }> {
@@ -306,7 +309,7 @@ export class DocumentButtonBar extends ObservableReactComponent<{ views: () => (
             const dragDocView = this.view0!;
             const dragData = new DragManager.DocumentDragData([dragDocView.Document]);
             const [left, top] = dragDocView.screenToContentsTransform().inverse().transformPoint(0, 0);
-            dragData.defaultDropAction = 'embed';
+            dragData.defaultDropAction = dropActionType.embed;
             dragData.canEmbed = true;
             DragManager.StartDocumentDrag([dragDocView.ContentDiv!], dragData, left, top, { hideSource: false });
             return true;
@@ -316,35 +319,24 @@ export class DocumentButtonBar extends ObservableReactComponent<{ views: () => (
 
     _ref = React.createRef<HTMLDivElement>();
     @observable _tooltipOpen: boolean = false;
+    @computed get templateMenu() {
+        return (
+            <div ref={this._ref}>
+                <TemplateMenu
+                    docViews={this._props
+                        .views()
+                        .filter(v => v)
+                        .map(v => v as DocumentView)}
+                />
+            </div>
+        );
+    }
     @computed
     get templateButton() {
-        const view0 = this.view0;
-        const views = this._props.views();
-        return !view0 ? null : (
+        return !this.view0 ? null : (
             <Tooltip title={<div className="dash-tooltip">Tap to Customize Layout. Drag an embedding</div>} open={this._tooltipOpen} onClose={action(() => (this._tooltipOpen = false))} placement="bottom">
                 <div className="documentButtonBar-linkFlyout" ref={this._dragRef} onPointerEnter={action(() => !this._ref.current?.getBoundingClientRect().width && (this._tooltipOpen = true))}>
-                    {
-                        /* <Flyout
-                        anchorPoint={anchorPoints.LEFT_TOP}
-                        onOpen={action(() => (this._embedDown = true))}
-                        onClose={action(() => (this._embedDown = false))}
-                        content={
-                            !this._embedDown ? null : (
-                                <div ref={this._ref}>
-                                    {' '}
-                                    <TemplateMenu docViews={views.filter(v => v).map(v => v as DocumentView)} />
-                                </div>
-                            )
-                        }>
-                        <div className={'documentButtonBar-linkButton-empty'} ref={this._dragRef} onPointerDown={this.onTemplateButton}>
-                            <FontAwesomeIcon className="documentdecorations-icon" icon="edit" size="sm" />
-                        </div>
-                    </Flyout> */
-
-                        <div className={'documentButtonBar-linkButton-empty'} ref={this._dragRef} onPointerDown={this.onTemplateButton}>
-                            <FontAwesomeIcon className="documentdecorations-icon" icon="edit" size="sm" />
-                        </div>
-                    }
+                    <Popup icon={<FaEdit />} popup={this.templateMenu} popupContainsPt={returnTrue} />
                 </div>
             </Tooltip>
         );
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index e9c4d9cc5..9d53c58c0 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -77,7 +77,7 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
                 const {x,y} = Utils.rotPt(e.clientX - center.x, 
                                           e.clientY - center.y, 
                                           NumCast(SelectionManager.Views.lastElement()?.screenToViewTransform().Rotate));
-                (this._showNothing = !(this.Bounds.x !== Number.MAX_VALUE && //
+                (this._showNothing = !DocumentButtonBar.Instance?._tooltipOpen && !(this.Bounds.x !== Number.MAX_VALUE && //
                     (this.Bounds.x > center.x+x || this.Bounds.r < center.x+x || 
                      this.Bounds.y > center.y+y || this.Bounds.b < center.y+y )));
             })); // prettier-ignore
@@ -514,7 +514,7 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
     //
     // determines if anything being dragged directly or via a group has a fixed aspect ratio (in which case we resize uniformly)
     //
-    hasFixedAspect = (doc: Doc): boolean => (doc.isGroup ? DocListCast(doc.data).some(this.hasFixedAspect) : !BoolCast(doc.layout_nativeDimEditable));
+    hasFixedAspect = (doc: Doc): boolean => (doc.isGroup ? DocListCast(doc.data).some(this.hasFixedAspect) : !BoolCast(doc._layout_nativeDimEditable));
 
     //
     // resize a single DocumentView about the specified reference point, possibly setting/updating the native dimensions of the Doc
diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx
index 92644d3c5..122e5c4c3 100644
--- a/src/client/views/InkingStroke.tsx
+++ b/src/client/views/InkingStroke.tsx
@@ -438,7 +438,7 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() impleme
                 <svg
                     className="inkStroke"
                     style={{
-                        transform: isInkMask ? `rotate(-${NumCast(this._props.CollectionFreeFormDocumentView?.()._props.w_Rotation?.() ?? 0)}deg) translate(${InkingStroke.MaskDim / 2}px, ${InkingStroke.MaskDim / 2}px)` : undefined,
+                        transform: isInkMask ? `rotate(-${NumCast(this._props.CollectionFreeFormDocumentView?.()._props.rotation ?? 0)}deg) translate(${InkingStroke.MaskDim / 2}px, ${InkingStroke.MaskDim / 2}px)` : undefined,
                         // mixBlendMode: this.layoutDoc.tool === InkTool.Highlighter ? 'multiply' : 'unset',
                         cursor: this._props.isSelected() ? 'default' : undefined,
                     }}
diff --git a/src/client/views/TemplateMenu.tsx b/src/client/views/TemplateMenu.tsx
index 5fc33207e..f7729dbc7 100644
--- a/src/client/views/TemplateMenu.tsx
+++ b/src/client/views/TemplateMenu.tsx
@@ -81,7 +81,7 @@ export class TemplateMenu extends React.Component<TemplateMenuProps> {
     componentDidMount() {
         !this._addedKeys && (this._addedKeys = new ObservableSet());
         [...Array.from(Object.keys(this.props.docViews[0].Document[DocData])), ...Array.from(Object.keys(this.props.docViews[0].Document))]
-            .filter(key => key.startsWith('layout_'))
+            .filter(key => key.startsWith('layout_') && key !== 'layout_fieldKey')
             .map(key => runInAction(() => this._addedKeys.add(key.replace('layout_', ''))));
     }
 
diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx
index b52f2e7b6..699ec3b95 100644
--- a/src/client/views/collections/TabDocView.tsx
+++ b/src/client/views/collections/TabDocView.tsx
@@ -638,7 +638,7 @@ export class TabMinimapView extends ObservableReactComponent<TabMinimapViewProps
     render() {
         return this._props.document.layout !== CollectionView.LayoutString(Doc.LayoutFieldKey(this._props.document)) || this._props.document?._type_collection !== CollectionViewType.Freeform ? null : (
             <div className="miniMap-hidden">
-                <Popup icon={<FontAwesomeIcon icon="globe-asia" size="lg" />} color={SettingsManager.userVariantColor} type={Type.TERT} onPointerDown={e => e.stopPropagation()} placement={'top-end'} popup={this.popup} />
+                <Popup icon={<FontAwesomeIcon icon="globe-asia" size="lg" />} color={SettingsManager.userVariantColor} type={Type.TERT} onPointerDown={e => e.stopPropagation()} placement="top-end" popup={this.popup} />
             </div>
         );
     }
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index e48656f5e..877569ab3 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -17,7 +17,7 @@ import { BoolCast, Cast, DocCast, NumCast, ScriptCast, StrCast } from '../../../
 import { ImageField } from '../../../../fields/URLField';
 import { TraceMobx } from '../../../../fields/util';
 import { GestureUtils } from '../../../../pen-gestures/GestureUtils';
-import { aggregateBounds, DashColor, emptyFunction, intersectRect, lightOrDark, OmitKeys, returnFalse, returnZero, setupMoveUpEvents, Utils } from '../../../../Utils';
+import { aggregateBounds, DashColor, emptyFunction, intersectRect, lightOrDark, numberValue, OmitKeys, returnFalse, returnZero, setupMoveUpEvents, Utils } from '../../../../Utils';
 import { CognitiveServices } from '../../../cognitive_services/CognitiveServices';
 import { Docs, DocUtils } from '../../../documents/Documents';
 import { CollectionViewType, DocumentType } from '../../../documents/DocumentTypes';
@@ -760,7 +760,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         this.onGesture(e, new GestureUtils.GestureEvent(gesture, points, GestureOverlay.getBounds(points), text));
     };
 
-    @action
     onPointerMove = (e: PointerEvent) => {
         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
@@ -1344,7 +1343,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
 
     renderCutoffProvider = computedFn(
         function renderCutoffProvider(this: any, doc: Doc) {
-            return !this._renderCutoffData.get(doc[Id] + '');
+            return this.Document.isTemplateDoc ? false : !this._renderCutoffData.get(doc[Id] + '');
         }.bind(this)
     );
 
@@ -1467,7 +1466,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                 if (!code.includes('dashDiv')) {
                     const script = CompileScript(code, { params: { docView: 'any' }, typecheck: false, editable: true });
                     if (script.compiled) script.run({ this: this.DocumentView?.() });
-                } else code && !first && eval(code);
+                } else code && !first && eval?.(code);
             },
             { fireImmediately: true }
         );
@@ -1712,7 +1711,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     incrementalRender = action(() => {
         if (!LightboxView.LightboxDoc || LightboxView.Contains(this.DocumentView?.())) {
             const layout_unrendered = this.childDocs.filter(doc => !this._renderCutoffData.get(doc[Id]));
-            const loadIncrement = 5;
+            const loadIncrement = this.Document.isTemplateDoc ? Number.MAX_VALUE : 5;
             for (var i = 0; i < Math.min(layout_unrendered.length, loadIncrement); i++) {
                 this._renderCutoffData.set(layout_unrendered[i][Id] + '', true);
             }
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 923aead64..251235b93 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -141,6 +141,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl
                 const targetField = Doc.LayoutFieldKey(layoutDoc);
                 const targetDoc = layoutDoc[DocData];
                 if (targetDoc[targetField] instanceof ImageField) {
+                    added = true;
                     this.dataDoc[this.fieldKey] = ObjectField.MakeCopy(targetDoc[targetField] as ImageField);
                     Doc.SetNativeWidth(this.dataDoc, Doc.NativeWidth(targetDoc), this.fieldKey);
                     Doc.SetNativeHeight(this.dataDoc, Doc.NativeHeight(targetDoc), this.fieldKey);
-- 
cgit v1.2.3-70-g09d2


From b938ac2909290bd5dd11eeb0a0056f4679dbb25e Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Thu, 7 Mar 2024 10:24:56 -0500
Subject: fixed several things related to templates for labels (used as icons)
 and  FontIconboxes. Also made labels non uniformly  resizable .

---
 src/client/documents/Documents.ts                        | 16 +++++++---------
 src/client/util/CurrentUserUtils.ts                      | 16 +++++++++-------
 src/client/views/DocumentDecorations.tsx                 | 10 +++++-----
 .../collectionFreeForm/CollectionFreeFormView.tsx        | 12 ++++++++----
 src/client/views/nodes/FontIconBox/FontIconBox.tsx       |  8 ++++++--
 src/client/views/nodes/LabelBox.tsx                      | 10 +++-------
 6 files changed, 38 insertions(+), 34 deletions(-)

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

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index d41cd24d4..b7d8222b9 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -277,8 +277,8 @@ export class DocumentOptions {
     layout_borderRounding?: string;
     _layout_modificationDate?: DATEt = new DateInfo('last modification date of doc layout', false);
     _layout_nativeDimEditable?: BOOLt = new BoolInfo('native dimensions can be modified using document decoration reizers', false);
-    _layout_reflowVertical?: BOOLt = new BoolInfo('native height can be changed independent of width by dragging decoration resizers');
-    _layout_reflowHorizontal?: BOOLt = new BoolInfo('whether a doc with a native size can be horizonally resized, causing some form of reflow');
+    _layout_reflowVertical?: BOOLt = new BoolInfo('permit vertical resizing with content "reflow"');
+    _layout_reflowHorizontal?: BOOLt = new BoolInfo('permit horizontal resizing with content reflow');
     layout_boxShadow?: string; // box-shadow css string OR "standard" to use dash standard box shadow
     layout_maxShown?: NUMt = new NumInfo('maximum number of children to display at one time (see multicolumnview)');
     _layout_autoHeight?: BOOLt = new BoolInfo('whether document automatically resizes vertically to display contents');
@@ -317,7 +317,6 @@ export class DocumentOptions {
     _label_minFontSize?: NUMt = new NumInfo('minimum font size for labelBoxes', false);
     _label_maxFontSize?: NUMt = new NumInfo('maximum font size for labelBoxes', false);
     stroke_width?: NUMt = new NumInfo('width of an ink stroke', false);
-    icon_label?: STRt = new StrInfo('label to use for a fontIcon doc (otherwise, the title is used)', false);
     mediaState?: STRt = new StrInfo(`status of audio/video media document: ${media_state.PendingRecording}, ${media_state.Recording}, ${media_state.Paused}, ${media_state.Playing}`, false);
     recording?: BOOLt = new BoolInfo('whether WebCam is recording or not');
     slides?: DOCt = new DocInfo('presentation slide associated with video recording (bcz: should be renamed!!)');
@@ -652,8 +651,8 @@ export namespace Docs {
             [
                 DocumentType.LABEL,
                 {
-                    layout: { view: LabelBox, dataField: defaultDataKey },
-                    options: { _singleLine: true },
+                    layout: { view: LabelBox, dataField: 'title' },
+                    options: { _singleLine: true, layout_nativeDimEditable: true, layout_reflowHorizontal: true, layout_reflowVertical: true },
                 },
             ],
             [
@@ -684,8 +683,8 @@ export namespace Docs {
             [
                 DocumentType.BUTTON,
                 {
-                    layout: { view: LabelBox, dataField: 'onClick' },
-                    options: {},
+                    layout: { view: LabelBox, dataField: 'title' },
+                    options: { layout_nativeDimEditable: true, layout_reflowHorizontal: true, layout_reflowVertical: true },
                 },
             ],
             [
@@ -1881,8 +1880,7 @@ export namespace DocUtils {
         const hasContextAnchor = LinkManager.Links(doc).some(l => (l.link_anchor_2 === doc && Cast(l.link_anchor_1, Doc, null)?.annotationOn === context) || (l.link_anchor_1 === doc && Cast(l.link_anchor_2, Doc, null)?.annotationOn === context));
         if (context && !hasContextAnchor && (context.type === DocumentType.VID || context.type === DocumentType.WEB || context.type === DocumentType.PDF || context.type === DocumentType.IMG)) {
             const pushpin = Docs.Create.FontIconDocument({
-                title: 'pushpin',
-                icon_label: '',
+                title: '',
                 annotationOn: Cast(doc.annotationOn, Doc, null),
                 followLinkToggle: true,
                 icon: 'map-pin',
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index 310613bc9..cfba003fc 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -22,7 +22,7 @@ import { CollectionTreeView, TreeViewType } from "../views/collections/Collectio
 import { Colors } from "../views/global/globalEnums";
 import { media_state } from "../views/nodes/AudioBox";
 import { OpenWhere } from "../views/nodes/DocumentView";
-import { ButtonType } from "../views/nodes/FontIconBox/FontIconBox";
+import { ButtonType, FontIconBox } from "../views/nodes/FontIconBox/FontIconBox";
 import { ImportElementBox } from "../views/nodes/importBox/ImportElementBox";
 import { DragManager, dropActionType } from "./DragManager";
 import { MakeTemplate } from "./DropConverter";
@@ -32,6 +32,8 @@ import { ScriptingGlobals } from "./ScriptingGlobals";
 import { ColorScheme } from "./SettingsManager";
 import { SnappingManager } from "./SnappingManager";
 import { UndoManager } from "./UndoManager";
+import { LabelBox } from "../views/nodes/LabelBox";
+import { ImageBox } from "../views/nodes/ImageBox";
 
 interface Button {
     // DocumentOptions fields a button can set
@@ -156,14 +158,14 @@ export class CurrentUserUtils {
             }
             const allopts = {isSystem: true, onClickScriptDisable: "never",  ...opts};
             return DocUtils.AssignScripts(  (curIcon?.iconTemplate === opts.iconTemplate ? 
-                      DocUtils.AssignOpts(curIcon, allopts):undefined) ?? ((templateIconsDoc[iconFieldName] = MakeTemplate(creator(allopts), true, iconFieldName, templateField))),
+                      DocUtils.AssignOpts(curIcon, allopts):undefined) ?? ((templateIconsDoc[iconFieldName] = MakeTemplate(creator(allopts, templateField), true, iconFieldName, templateField))),
                 {onClick:"deiconifyView(documentView)", onDoubleClick: "deiconifyViewToLightbox(documentView)", });
         };
-        const labelBox = (opts: DocumentOptions, data?:string) => Docs.Create.LabelDocument({
-            textTransform: "unset", letterSpacing: "unset", _singleLine: false, _label_minFontSize: 14, _label_maxFontSize: 14, layout_borderRounding: "5px",  _width: 150, _height: 70, _xPadding: 10, _yPadding: 10, ...opts
+        const labelBox = (opts: DocumentOptions, fieldKey:string) => Docs.Create.LabelDocument({
+            layout: LabelBox.LayoutString(fieldKey), textTransform: "unset", letterSpacing: "unset", _singleLine: false, _label_minFontSize: 14, _label_maxFontSize: 14, layout_borderRounding: "5px",  _width: 150, _height: 70, _xPadding: 10, _yPadding: 10, ...opts
         });
-        const imageBox = (opts: DocumentOptions, url?:string) => Docs.Create.ImageDocument(url ?? "http://www.cs.brown.edu/~bcz/noImage.png", { "icon_nativeWidth": 360 / 4, "icon_nativeHeight": 270 / 4, iconTemplate:DocumentType.IMG,  _width: 360 / 4, _height: 270 / 4,  _layout_showTitle: "title", ...opts });
-        const fontBox = (opts:DocumentOptions, data?:string) => Docs.Create.FontIconDocument({ _nativeHeight: 30, _nativeWidth: 30, _width: 30, _height: 30, ...opts  });
+        const imageBox = (opts: DocumentOptions, fieldKey:string) => Docs.Create.ImageDocument( "http://www.cs.brown.edu/~bcz/noImage.png", { layout:ImageBox.LayoutString(fieldKey), "icon_nativeWidth": 360 / 4, "icon_nativeHeight": 270 / 4, iconTemplate:DocumentType.IMG,  _width: 360 / 4, _height: 270 / 4,  _layout_showTitle: "title", ...opts });
+        const fontBox = (opts:DocumentOptions, fieldKey:string) => Docs.Create.FontIconDocument({ layout:FontIconBox.LayoutString(fieldKey), _nativeHeight: 30, _nativeWidth: 30, _width: 30, _height: 30, ...opts  });
         const iconTemplates = [
             makeIconTemplate(undefined,          "title", { iconTemplate:DocumentType.LABEL, backgroundColor: "dimgray"}),
             makeIconTemplate(DocumentType.AUDIO, "title", { iconTemplate:DocumentType.LABEL, backgroundColor: "lightgreen"}),
@@ -174,7 +176,7 @@ export class CurrentUserUtils {
             makeIconTemplate(DocumentType.COL,   "icon",  { iconTemplate:DocumentType.IMG}),
             makeIconTemplate(DocumentType.COL,   "icon",  { iconTemplate:DocumentType.IMG}),
             makeIconTemplate(DocumentType.VID,   "icon",  { iconTemplate:DocumentType.IMG}),
-            makeIconTemplate(DocumentType.BUTTON,"data",  { iconTemplate:DocumentType.FONTICON}),
+            makeIconTemplate(DocumentType.BUTTON,"title", { iconTemplate:DocumentType.FONTICON}),
             //nasty hack .. templates are looked up exclusively by type -- but we want a template for a document with a certain field (transcription) .. so this hack and the companion hack in createCustomView does this for now
             makeIconTemplate("transcription" as any, "transcription", { iconTemplate:DocumentType.LABEL, backgroundColor: "orange" }),
             //makeIconTemplate(DocumentType.PDF, "icon", {iconTemplate:DocumentType.IMG}, (opts) => imageBox("http://www.cs.brown.edu/~bcz/noImage.png", opts))
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 87ee962a0..2fb9f0fc1 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -323,7 +323,7 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
                 SelectionManager.Docs.map(doc => {
                     const docMax = Math.min(NumCast(doc.width) / 2, NumCast(doc.height) / 2);
                     const radius = Math.min(1, dist / maxDist) * docMax; // set radius based on ratio of drag distance to half diagonal distance of bounding box
-                    doc.layout_borderRounding = `${radius}px`;
+                    doc._layout_borderRounding = `${radius}px`;
                 });
                 return false;
             },
@@ -533,9 +533,9 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
             const [initWidth, initHeight] = [NumCast(doc._width, 1), NumCast(doc._height)];
 
             const modifyNativeDim =
-                (opts.ctrlKey && doc.layout_nativeDimEditable) || // e.g., PDF or web page
-                (doc.layout_reflowHorizontal && opts.dragHdl !== 'bottom' && opts.dragHdl !== 'top') || // eg rtf or some web pages
-                (doc.layout_reflowVertical && (opts.dragHdl === 'bottom' || opts.dragHdl === 'top' || opts.ctrlKey)); // eg rtf, web, pdf
+                (opts.ctrlKey && doc._layout_nativeDimEditable) || // e.g., PDF or web page
+                (doc._layout_reflowHorizontal && opts.dragHdl !== 'bottom' && opts.dragHdl !== 'top') || // eg rtf or some web pages
+                (doc._layout_reflowVertical && (opts.dragHdl === 'bottom' || opts.dragHdl === 'top' || opts.ctrlKey)); // eg rtf, web, pdf
             if (nwidth && nheight && !modifyNativeDim) {
                 // eg., dragging right resizer on PDF -- enforce native dimensions because not expliclty overridden with ctrl or bottom resize drag
                 scale.x === 1 ? (scale.x = scale.y) : (scale.y = scale.x);
@@ -545,7 +545,7 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
                 const setData = Doc.NativeWidth(doc[DocData]) === doc.nativeWidth;
                 doc.nativeWidth = scale.x * Doc.NativeWidth(doc);
                 if (setData) Doc.SetNativeWidth(doc[DocData], NumCast(doc.nativeWidth));
-                if (doc.layout_reflowVertical && !NumCast(doc.nativeHeight)) {
+                if (doc._layout_reflowVertical && !NumCast(doc.nativeHeight)) {
                     doc._nativeHeight = (initHeight / initWidth) * nwidth; // initializes the nativeHeight for a PDF
                 }
             }
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 877569ab3..4eb946939 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -1,6 +1,6 @@
 import { Bezier } from 'bezier-js';
 import { Colors } from 'browndash-components';
-import { action, computed, IReactionDisposer, makeObservable, observable, reaction, runInAction } from 'mobx';
+import { action, computed, IReactionDisposer, makeObservable, observable, reaction, runInAction, trace } from 'mobx';
 import { observer } from 'mobx-react';
 import { computedFn } from 'mobx-utils';
 import * as React from 'react';
@@ -840,10 +840,14 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                     if (excludeT < startSegmentT || excludeT > docCurveTVal) {
                         const localStartTVal = startSegmentT - Math.floor(i / 4);
                         t !== (localStartTVal < 0 ? 0 : localStartTVal) && segment.push(inkSegment.split(localStartTVal < 0 ? 0 : localStartTVal, t));
-                        segment.length && segments.push(segment);
+                        if (segment.length && (Math.abs(segment[0].points[0].x - segment[0].points.lastElement().x) > 0.5 || Math.abs(segment[0].points[0].y - segment[0].points.lastElement().y) > 0.5)) segments.push(segment);
                     }
                     // start a new segment from the intersection t value
-                    segment = tVals.length - 1 === index ? [inkSegment.split(t).right] : [];
+                    if (tVals.length - 1 === index) {
+                        const split = inkSegment.split(t).right;
+                        if (split && (Math.abs(split.points[0].x - split.points.lastElement().x) > 0.5 || Math.abs(split.points[0].y - split.points.lastElement().y) > 0.5)) segment = [split];
+                        else segment = [];
+                    } else segment = [];
                     startSegmentT = docCurveTVal;
                 });
             } else {
@@ -1647,7 +1651,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         !this._props.isAnnotationOverlay &&
             !Doc.noviceMode &&
             optionItems.push({ description: (this._showAnimTimeline ? 'Close' : 'Open') + ' Animation Timeline', event: action(() => (this._showAnimTimeline = !this._showAnimTimeline)), icon: 'eye' });
-        this._props.renderDepth && optionItems.push({ description: 'Use Background Color as Default', event: () => (Cast(Doc.UserDoc().emptyCollection, Doc, null)._backgroundColor = StrCast(this.layoutDoc._backgroundColor)), icon: 'palette' });
+        this._props.renderDepth && optionItems.push({ description: 'Use Background Color as Default', event: () => (Cast(Doc.UserDoc().emptyCollection, Doc, null).backgroundColor = StrCast(this.layoutDoc.backgroundColor)), icon: 'palette' });
         this._props.renderDepth && optionItems.push({ description: 'Fit Content Once', event: this.fitContentOnce, icon: 'object-group' });
         if (!Doc.noviceMode) {
             optionItems.push({ description: (!Doc.NativeWidth(this.layoutDoc) || !Doc.NativeHeight(this.layoutDoc) ? 'Freeze' : 'Unfreeze') + ' Aspect', event: this.toggleNativeDimensions, icon: 'snowflake' });
diff --git a/src/client/views/nodes/FontIconBox/FontIconBox.tsx b/src/client/views/nodes/FontIconBox/FontIconBox.tsx
index d24c79b10..46043eefb 100644
--- a/src/client/views/nodes/FontIconBox/FontIconBox.tsx
+++ b/src/client/views/nodes/FontIconBox/FontIconBox.tsx
@@ -81,9 +81,13 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() {
         }
     };
 
-    // Determining UI Specs
+    /**
+     * this chooses the appropriate title for the label
+     * if the Document is a template, then we use the title of the data doc that it renders
+     * otherwise, we use the Document's title itself.
+     */
     @computed get label() {
-        return StrCast(this.dataDoc.icon_label, StrCast(this.Document.title));
+        return StrCast(this.Document.isTemplateDoc ? this.dataDoc.title : this.Document.title);
     }
     Icon = (color: string, iconFalse?: boolean) => {
         let icon;
diff --git a/src/client/views/nodes/LabelBox.tsx b/src/client/views/nodes/LabelBox.tsx
index fd3074a88..be20b5934 100644
--- a/src/client/views/nodes/LabelBox.tsx
+++ b/src/client/views/nodes/LabelBox.tsx
@@ -17,12 +17,8 @@ import './LabelBox.scss';
 import { PinProps, PresBox } from './trails';
 import { Docs } from '../../documents/Documents';
 
-export interface LabelBoxProps extends FieldViewProps {
-    label?: string;
-}
-
 @observer
-export class LabelBox extends ViewBoxBaseComponent<LabelBoxProps>() {
+export class LabelBox extends ViewBoxBaseComponent<FieldViewProps>() {
     public static LayoutString(fieldKey: string) {
         return FieldView.LayoutString(LabelBox, fieldKey);
     }
@@ -32,7 +28,7 @@ export class LabelBox extends ViewBoxBaseComponent<LabelBoxProps>() {
     private dropDisposer?: DragManager.DragDropDisposer;
     private _timeout: any;
 
-    constructor(props: LabelBoxProps) {
+    constructor(props: FieldViewProps) {
         super(props);
         makeObservable(this);
     }
@@ -45,7 +41,7 @@ export class LabelBox extends ViewBoxBaseComponent<LabelBoxProps>() {
     }
 
     @computed get Title() {
-        return this.dataDoc.title_custom ? StrCast(this.Document.title) : this._props.label ? this._props.label : Field.toString(this.dataDoc[this.fieldKey] as Field);
+        return Field.toString(this.dataDoc[this.fieldKey] as Field);
     }
 
     protected createDropTarget = (ele: HTMLDivElement) => {
-- 
cgit v1.2.3-70-g09d2


From f1ed1cff137c06afc4d4db8a8778f7827404889b Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Sat, 9 Mar 2024 19:05:30 -0500
Subject: fixed up default text that uses a template to process an initial
 carriage return properly.  fixed text with inherited templates to be able to
 show fields with a default dashField value from template that can be
 overidden on instance.

---
 src/client/documents/Documents.ts                  | 11 +++---
 src/client/views/MarqueeAnnotator.tsx              |  4 +--
 src/client/views/PreviewCursor.tsx                 |  2 +-
 src/client/views/collections/TreeView.tsx          |  2 +-
 .../collectionFreeForm/CollectionFreeFormView.tsx  | 36 +++++++++++--------
 .../collections/collectionFreeForm/MarqueeView.tsx |  2 +-
 .../collectionSchema/SchemaTableCell.tsx           |  2 +-
 src/client/views/nodes/MapBox/MapBox.tsx           |  4 +--
 .../views/nodes/MapboxMapBox/MapboxContainer.tsx   |  4 +--
 .../views/nodes/formattedText/FormattedTextBox.tsx | 40 ++++++++++------------
 10 files changed, 55 insertions(+), 52 deletions(-)

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

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index e6969d1f3..7a3b965fe 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -3,7 +3,7 @@ import { action, reaction, runInAction } from 'mobx';
 import { basename } from 'path';
 import { DateField } from '../../fields/DateField';
 import { Doc, DocListCast, Field, LinkedTo, Opt, StrListCast, updateCachedAcls } from '../../fields/Doc';
-import { Initializing } from '../../fields/DocSymbols';
+import { DocData, Initializing } from '../../fields/DocSymbols';
 import { Id } from '../../fields/FieldSymbols';
 import { HtmlField } from '../../fields/HtmlField';
 import { InkField, PointData } from '../../fields/InkField';
@@ -1981,10 +1981,8 @@ export namespace DocUtils {
         }
     }
 
-    export function GetNewTextDoc(title: string, x: number, y: number, width?: number, height?: number, noMargins?: boolean, annotationOn?: Doc, backgroundColor?: string) {
+    export function GetNewTextDoc(title: string, x: number, y: number, width?: number, height?: number, annotationOn?: Doc, backgroundColor?: string) {
         const tbox = Docs.Create.TextDocument('', {
-            _xMargin: noMargins ? 0 : undefined,
-            _yMargin: noMargins ? 0 : undefined,
             annotationOn,
             backgroundColor,
             _width: width || 200,
@@ -1997,11 +1995,14 @@ export namespace DocUtils {
             _layout_enableAltContentUI: BoolCast(Doc.UserDoc().defaultToFlashcards),
             title,
         });
+
         const template = Doc.UserDoc().defaultTextLayout;
         if (template instanceof Doc) {
+            // if a default text template is specified
             tbox._width = NumCast(template._width);
             tbox.layout_fieldKey = 'layout_' + StrCast(template.title);
-            Doc.GetProto(tbox)[StrCast(tbox.layout_fieldKey)] = template;
+            Doc.GetProto(tbox)[StrCast(tbox.layout_fieldKey)] = template; // set the text doc's layout to render with the text template
+            tbox[DocData].proto = template; // and also set the text doc to inherit from the template (this allows the template to specify default field values)
         }
         return tbox;
     }
diff --git a/src/client/views/MarqueeAnnotator.tsx b/src/client/views/MarqueeAnnotator.tsx
index a4303c3aa..f59042b04 100644
--- a/src/client/views/MarqueeAnnotator.tsx
+++ b/src/client/views/MarqueeAnnotator.tsx
@@ -88,7 +88,7 @@ export class MarqueeAnnotator extends ObservableReactComponent<MarqueeAnnotatorP
 
         const textRegionAnno = Docs.Create.HTMLMarkerDocument([], {
             annotationOn: this.props.Document,
-            text: this.props.selectionText(),
+            text: this.props.selectionText() as any, // text want an RTFfield, but strings are acceptable, too.
             backgroundColor: 'transparent',
             presentation_duration: 2100,
             presentation_transition: 500,
@@ -198,7 +198,7 @@ export class MarqueeAnnotator extends ObservableReactComponent<MarqueeAnnotatorP
             const sourceAnchorCreator = () => this.highlight(this.props.highlightDragSrcColor ?? 'rgba(173, 216, 230, 0.75)', true, undefined, true); // hyperlink color
 
             const targetCreator = (annotationOn: Doc | undefined) => {
-                const target = DocUtils.GetNewTextDoc('Note linked to ' + this.props.Document.title, 0, 0, 100, 100, undefined, annotationOn, 'yellow');
+                const target = DocUtils.GetNewTextDoc('Note linked to ' + this.props.Document.title, 0, 0, 100, 100, annotationOn, 'yellow');
                 FormattedTextBox.SetSelectOnLoad(target);
                 return target;
             };
diff --git a/src/client/views/PreviewCursor.tsx b/src/client/views/PreviewCursor.tsx
index 456b753b4..a94c18295 100644
--- a/src/client/views/PreviewCursor.tsx
+++ b/src/client/views/PreviewCursor.tsx
@@ -85,7 +85,7 @@ export class PreviewCursor extends ObservableReactComponent<{}> {
                 } else {
                     FormattedTextBox.PasteOnLoad = e;
                     if (e.clipboardData.getData('dash/pdfAnchor')) e.preventDefault();
-                    UndoManager.RunInBatch(() => this._addLiveTextDoc?.(DocUtils.GetNewTextDoc('', newPoint[0], newPoint[1], 500, undefined, undefined, undefined)), 'paste');
+                    UndoManager.RunInBatch(() => this._addLiveTextDoc?.(DocUtils.GetNewTextDoc('', newPoint[0], newPoint[1], 500, undefined, undefined)), 'paste');
                 }
             }
             //pasting in images
diff --git a/src/client/views/collections/TreeView.tsx b/src/client/views/collections/TreeView.tsx
index 0c0e49411..c6bbcb0a5 100644
--- a/src/client/views/collections/TreeView.tsx
+++ b/src/client/views/collections/TreeView.tsx
@@ -884,7 +884,7 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> {
         // prettier-ignore
         switch (property.split(':')[0]) {
             case StyleProp.Opacity:         return this.treeView.outlineMode ? undefined : 1;
-            case StyleProp.BackgroundColor: return this.selected ? '#7089bb' : StrCast(doc._backgroundColor, StrCast(doc.backgroundColor));
+            case StyleProp.BackgroundColor: return this.selected ? '#7089bb' : undefined;//StrCast(doc._backgroundColor, StrCast(doc.backgroundColor));
             case StyleProp.Highlighting:    if (this.treeView.outlineMode) return undefined;
             case StyleProp.BoxShadow:       return undefined;
             case StyleProp.DocContents:
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 4eb946939..9500b918a 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -30,7 +30,7 @@ import { SelectionManager } from '../../../util/SelectionManager';
 import { freeformScrollMode } from '../../../util/SettingsManager';
 import { SnappingManager } from '../../../util/SnappingManager';
 import { Transform } from '../../../util/Transform';
-import { undoBatch, UndoManager } from '../../../util/UndoManager';
+import { undoable, undoBatch, UndoManager } from '../../../util/UndoManager';
 import { Timeline } from '../../animationtimeline/Timeline';
 import { ContextMenu } from '../../ContextMenu';
 import { GestureOverlay } from '../../GestureOverlay';
@@ -1146,23 +1146,29 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
 
     isContentActive = () => this._props.isContentActive();
 
-    @undoBatch
+    /**
+     * Create a new text note of the same style as the one being typed into.
+     * If the text doc is be part of a larger templated doc, the new Doc will be a copy of the templated Doc
+     *
+     * @param fieldProps render props for the text doc being typed into
+     * @param below whether to place the new text Doc below or to the right of the one being typed into.
+     * @returns whether the new text doc was created and added successfully
+     */
+    createTextDocCopy = undoable((fieldProps: FieldViewProps, below: boolean) => {
+        const textDoc = DocCast(fieldProps.Document.rootDocument, fieldProps.Document);
+        const newDoc = Doc.MakeCopy(textDoc, true);
+        newDoc[DocData][Doc.LayoutFieldKey(newDoc, fieldProps.LayoutTemplateString)] = undefined; // the copy should not copy the text contents of it source, just the render style
+        newDoc.x = NumCast(textDoc.x) + (below ? 0 : NumCast(textDoc._width) + 10);
+        newDoc.y = NumCast(textDoc.y) + (below ? NumCast(textDoc._height) + 10 : 0);
+        FormattedTextBox.SetSelectOnLoad(newDoc);
+        FormattedTextBox.DontSelectInitialText = true;
+        return this.addDocument?.(newDoc);
+    }, 'copied text note');
+
     onKeyDown = (e: React.KeyboardEvent, fieldProps: FieldViewProps) => {
         if ((e.metaKey || e.ctrlKey || e.altKey || fieldProps.Document._createDocOnCR) && ['Tab', 'Enter'].includes(e.key)) {
             e.stopPropagation?.();
-            const below = !e.altKey && e.key !== 'Tab';
-            const layout_fieldKey = StrCast(fieldProps.fieldKey);
-            const newDoc = Doc.MakeCopy(fieldProps.Document, true);
-            const dataField = fieldProps.Document[Doc.LayoutFieldKey(newDoc)];
-            newDoc[DocData][Doc.LayoutFieldKey(newDoc)] = dataField === undefined || Cast(dataField, listSpec(Doc), null)?.length !== undefined ? new List<Doc>([]) : undefined;
-            if (below) newDoc.y = NumCast(fieldProps.Document.y) + NumCast(fieldProps.Document._height) + 10;
-            else newDoc.x = NumCast(fieldProps.Document.x) + NumCast(fieldProps.Document._width) + 10;
-            if (layout_fieldKey !== 'layout' && fieldProps.Document[layout_fieldKey] instanceof Doc) {
-                newDoc[layout_fieldKey] = fieldProps.Document[layout_fieldKey];
-            }
-            newDoc[DocData].text = undefined;
-            FormattedTextBox.SetSelectOnLoad(newDoc);
-            return this.addDocument?.(newDoc);
+            return this.createTextDocCopy(fieldProps, !e.altKey && e.key !== 'Tab');
         }
     };
     @computed get childPointerEvents() {
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index e22b83307..b913e05ad 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -178,7 +178,7 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
         } else if (!e.ctrlKey && !e.metaKey && SelectionManager.Views.length < 2) {
             FormattedTextBox.SelectOnLoadChar = Doc.UserDoc().defaultTextLayout && !this._props.childLayoutString ? e.key : '';
             FormattedTextBox.LiveTextUndo = UndoManager.StartBatch('type new note');
-            this._props.addLiveTextDocument(DocUtils.GetNewTextDoc('-typed text-', x, y, 200, 100, this._props.xPadding === 0));
+            this._props.addLiveTextDocument(DocUtils.GetNewTextDoc('-typed text-', x, y, 200, 100));
             e.stopPropagation();
         }
     };
diff --git a/src/client/views/collections/collectionSchema/SchemaTableCell.tsx b/src/client/views/collections/collectionSchema/SchemaTableCell.tsx
index 46867665d..ed1b519b4 100644
--- a/src/client/views/collections/collectionSchema/SchemaTableCell.tsx
+++ b/src/client/views/collections/collectionSchema/SchemaTableCell.tsx
@@ -134,7 +134,7 @@ export class SchemaTableCell extends ObservableReactComponent<SchemaTableCellPro
                         if (shiftDown && enterKey) {
                             this._props.setColumnValues(this._props.fieldKey.replace(/^_/, ''), value);
                         }
-                        const ret = KeyValueBox.SetField(this._props.Document, this._props.fieldKey.replace(/^_/, ''), value);
+                        const ret = KeyValueBox.SetField(this._props.Document, this._props.fieldKey.replace(/^_/, ''), value, Doc.IsDataProto(this._props.Document) ? true : undefined);
                         this._props.finishEdit?.();
                         return ret;
                     }, 'edit schema cell')}
diff --git a/src/client/views/nodes/MapBox/MapBox.tsx b/src/client/views/nodes/MapBox/MapBox.tsx
index c185c66fc..927e6fad4 100644
--- a/src/client/views/nodes/MapBox/MapBox.tsx
+++ b/src/client/views/nodes/MapBox/MapBox.tsx
@@ -379,7 +379,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
         });
 
         const targetCreator = (annotationOn: Doc | undefined) => {
-            const target = DocUtils.GetNewTextDoc('Note linked to ' + this.Document.title, 0, 0, 100, 100, undefined, annotationOn, 'yellow');
+            const target = DocUtils.GetNewTextDoc('Note linked to ' + this.Document.title, 0, 0, 100, 100, annotationOn, 'yellow');
             FormattedTextBox.SetSelectOnLoad(target);
             return target;
         };
@@ -592,7 +592,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
         /// this should use SELECTED pushpin for lat/long  if there is a selection, otherwise CENTER
         const anchor = Docs.Create.ConfigDocument({
             title: 'MapAnchor:' + this.Document.title,
-            text: StrCast(this.selectedPinOrRoute?.map) || StrCast(this.Document.map) || 'map location',
+            text: (StrCast(this.selectedPinOrRoute?.map) || StrCast(this.Document.map) || 'map location') as any,
             config_latitude: NumCast((existingPin ?? this.selectedPinOrRoute)?.latitude ?? this.dataDoc.latitude),
             config_longitude: NumCast((existingPin ?? this.selectedPinOrRoute)?.longitude ?? this.dataDoc.longitude),
             config_map_zoom: NumCast(this.dataDoc.map_zoom),
diff --git a/src/client/views/nodes/MapboxMapBox/MapboxContainer.tsx b/src/client/views/nodes/MapboxMapBox/MapboxContainer.tsx
index 8a5bd7ce6..3eb051dbf 100644
--- a/src/client/views/nodes/MapboxMapBox/MapboxContainer.tsx
+++ b/src/client/views/nodes/MapboxMapBox/MapboxContainer.tsx
@@ -232,7 +232,7 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent<FieldViewProps>
         });
 
         const targetCreator = (annotationOn: Doc | undefined) => {
-            const target = DocUtils.GetNewTextDoc('Note linked to ' + this.Document.title, 0, 0, 100, 100, undefined, annotationOn, 'yellow');
+            const target = DocUtils.GetNewTextDoc('Note linked to ' + this.Document.title, 0, 0, 100, 100, annotationOn, 'yellow');
             FormattedTextBox.SetSelectOnLoad(target);
             return target;
         };
@@ -466,7 +466,7 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent<FieldViewProps>
         /// this should use SELECTED pushpin for lat/long  if there is a selection, otherwise CENTER
         const anchor = Docs.Create.ConfigDocument({
             title: 'MapAnchor:' + this.Document.title,
-            text: StrCast(this.selectedPin?.map) || StrCast(this.Document.map) || 'map location',
+            text: (StrCast(this.selectedPin?.map) || StrCast(this.Document.map) || 'map location') as any,
             config_latitude: NumCast((existingPin ?? this.selectedPin)?.latitude ?? this.dataDoc.latitude),
             config_longitude: NumCast((existingPin ?? this.selectedPin)?.longitude ?? this.dataDoc.longitude),
             config_map_zoom: NumCast(this.dataDoc.map_zoom),
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 54e3e7b44..1ff7274f8 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -67,7 +67,6 @@ import { RichTextMenu, RichTextMenuPlugin } from './RichTextMenu';
 import { RichTextRules } from './RichTextRules';
 import { schema } from './schema_rts';
 import { SummaryView } from './SummaryView';
-import Select from 'react-select';
 // import * as applyDevTools from 'prosemirror-dev-tools';
 @observer
 export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implements ViewBoxInterface {
@@ -100,7 +99,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
     private _dropDisposer?: DragManager.DragDropDisposer;
     private _recordingStart: number = 0;
     private _ignoreScroll = false;
-    private _lastText = '';
     private _hadDownFocus = false;
     private _focusSpeed: Opt<number>;
     private _keymap: any = undefined;
@@ -306,7 +304,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
             e.preventDefault();
             e.stopPropagation();
             const targetCreator = (annotationOn?: Doc) => {
-                const target = DocUtils.GetNewTextDoc('Note linked to ' + this.Document.title, 0, 0, 100, 100, undefined, annotationOn);
+                const target = DocUtils.GetNewTextDoc('Note linked to ' + this.Document.title, 0, 0, 100, 100, annotationOn);
                 FormattedTextBox.SetSelectOnLoad(target);
                 return target;
             };
@@ -1457,39 +1455,37 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
         }
 
         const selectOnLoad = Doc.AreProtosEqual(this._props.TemplateDataDocument ?? this.Document, FormattedTextBox.SelectOnLoad) && (!LightboxView.LightboxDoc || LightboxView.Contains(this.DocumentView?.()));
-        if (this._editorView && selectOnLoad && !this._props.dontRegisterView && !this._props.dontSelectOnLoad && this.isActiveTab(this.ProseRef)) {
-            const selLoadChar = FormattedTextBox.SelectOnLoadChar;
+        const selLoadChar = FormattedTextBox.SelectOnLoadChar;
+        if (selectOnLoad) {
             FormattedTextBox.SelectOnLoad = undefined;
+            FormattedTextBox.SelectOnLoadChar = '';
+        }
+        if (this._editorView && selectOnLoad && !this._props.dontRegisterView && !this._props.dontSelectOnLoad && this.isActiveTab(this.ProseRef)) {
             this._props.select(false);
             if (selLoadChar) {
                 const $from = this._editorView.state.selection.anchor ? this._editorView.state.doc.resolve(this._editorView.state.selection.anchor - 1) : undefined;
                 const mark = schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.floor(Date.now() / 1000) });
                 const curMarks = this._editorView.state.storedMarks ?? $from?.marksAcross(this._editorView.state.selection.$head) ?? [];
                 const storedMarks = [...curMarks.filter(m => m.type !== mark.type), mark];
-                const tr = this._editorView.state.tr
-                    .setStoredMarks(storedMarks)
-                    .insertText(FormattedTextBox.SelectOnLoadChar, this._editorView.state.doc.content.size - 1, this._editorView.state.doc.content.size)
-                    .setStoredMarks(storedMarks);
+                const tr1 = this._editorView.state.tr.setStoredMarks(storedMarks);
+                const tr2 = selLoadChar === 'Enter' ? tr1.insert(this._editorView.state.doc.content.size - 1, schema.nodes.paragraph.create()) : tr1.insertText(selLoadChar, this._editorView.state.doc.content.size - 1);
+                const tr = tr2.setStoredMarks(storedMarks);
+
                 this._editorView.dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(tr.doc.content.size))));
             } else if (curText && !FormattedTextBox.DontSelectInitialText) {
                 selectAll(this._editorView.state, this._editorView?.dispatch);
             }
         }
-        selectOnLoad && this._editorView!.focus();
+        if (selectOnLoad) {
+            FormattedTextBox.DontSelectInitialText = false;
+            this._editorView!.focus();
+        }
         if (this._props.isContentActive()) this.prepareForTyping();
-        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 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);
-
-            if (FormattedTextBox.PasteOnLoad) {
-                const pdfAnchorId = FormattedTextBox.PasteOnLoad.clipboardData?.getData('dash/pdfAnchor');
-                FormattedTextBox.PasteOnLoad = undefined;
-                pdfAnchorId && this.addPdfReference(pdfAnchorId);
-            }
+        if (this._editorView && FormattedTextBox.PasteOnLoad) {
+            const pdfAnchorId = FormattedTextBox.PasteOnLoad.clipboardData?.getData('dash/pdfAnchor');
+            FormattedTextBox.PasteOnLoad = undefined;
+            pdfAnchorId && this.addPdfReference(pdfAnchorId);
         }
-        FormattedTextBox.DontSelectInitialText = false;
     }
 
     // add user mark for any first character that was typed since the user mark that gets set in KeyPress won't have been called yet.
-- 
cgit v1.2.3-70-g09d2


From a5d81be52ec685aede35791a706e5e1cb7f40613 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Thu, 14 Mar 2024 08:45:16 -0400
Subject: fixed disabling info ui to keep it off -- use settings manager to
 turn it back on.

---
 src/client/util/SettingsManager.tsx                                   | 3 ++-
 .../collections/collectionFreeForm/CollectionFreeFormInfoState.tsx    | 4 ++--
 .../views/collections/collectionFreeForm/CollectionFreeFormInfoUI.tsx | 2 +-
 .../views/collections/collectionFreeForm/CollectionFreeFormView.tsx   | 4 ++--
 src/fields/Doc.ts                                                     | 2 ++
 5 files changed, 9 insertions(+), 6 deletions(-)

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

diff --git a/src/client/util/SettingsManager.tsx b/src/client/util/SettingsManager.tsx
index 682704770..8594a1c92 100644
--- a/src/client/util/SettingsManager.tsx
+++ b/src/client/util/SettingsManager.tsx
@@ -483,7 +483,8 @@ export class SettingsManager extends React.Component<{}> {
                             toggleStatus={BoolCast(Doc.defaultAclPrivate)}
                             onClick={action(() => (Doc.defaultAclPrivate = !Doc.defaultAclPrivate))}
                         />
-                        <Toggle toggleType={ToggleType.SWITCH} formLabel={'Enable Sharing UI'} color={SettingsManager.userColor} toggleStatus={BoolCast(Doc.IsSharingEnabled)} onClick={action(() => (Doc.IsSharingEnabled = !Doc.IsSharingEnabled))} />
+                        <Toggle toggleType={ToggleType.SWITCH} formLabel="Enable Sharing UI" color={SettingsManager.userColor} toggleStatus={BoolCast(Doc.IsSharingEnabled)} onClick={action(() => (Doc.IsSharingEnabled = !Doc.IsSharingEnabled))} />
+                        <Toggle toggleType={ToggleType.SWITCH} formLabel="Disable Info UI" color={SettingsManager.userColor} toggleStatus={BoolCast(Doc.IsInfoUIDisabled)} onClick={action(() => (Doc.IsInfoUIDisabled = !Doc.IsInfoUIDisabled))} />
                     </div>
                 </div>
             </div>
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormInfoState.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormInfoState.tsx
index 24aedfa56..73dd7fea3 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormInfoState.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormInfoState.tsx
@@ -5,6 +5,7 @@ import * as React from 'react';
 import { SettingsManager } from '../../../util/SettingsManager';
 import { ObservableReactComponent } from '../../ObservableReactComponent';
 import './CollectionFreeFormView.scss';
+import { Doc } from '../../../../fields/Doc';
 
 /**
  * An Fsa Arc.  The first array element is a test condition function that will be observed.
@@ -57,7 +58,6 @@ export interface CollectionFreeFormInfoStateProps {
 @observer
 export class CollectionFreeFormInfoState extends ObservableReactComponent<CollectionFreeFormInfoStateProps> {
     _disposers: IReactionDisposer[] = [];
-    @observable _hide = false;
     @observable _expanded = false;
 
     constructor(props: any) {
@@ -97,7 +97,7 @@ export class CollectionFreeFormInfoState extends ObservableReactComponent<Collec
     render() {
         const gif = this.State?.[StateMessageGIF];
         return (
-            <div className={'collectionFreeform-' + (this._hide ? 'hidden' : 'infoUI')}>
+            <div className={'collectionFreeform-infoUI'}>
                 <p className="collectionFreeform-infoUI-msg">
                     {this.State?.[StateMessage]}
                     <button className={'collectionFreeform-' + (!gif ? 'hidden' : 'infoUI-button')} onClick={action(() => (this._expanded = !this._expanded))}>
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormInfoUI.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormInfoUI.tsx
index f8fe2bb0b..cc729decc 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormInfoUI.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormInfoUI.tsx
@@ -8,10 +8,10 @@ import { DocumentManager } from '../../../util/DocumentManager';
 import { LinkManager } from '../../../util/LinkManager';
 import { ObservableReactComponent } from '../../ObservableReactComponent';
 import { DocButtonState, DocumentLinksButton } from '../../nodes/DocumentLinksButton';
+import { TopBar } from '../../topbar/TopBar';
 import { CollectionFreeFormInfoState, InfoState, StateEntryFunc, infoState } from './CollectionFreeFormInfoState';
 import { CollectionFreeFormView } from './CollectionFreeFormView';
 import './CollectionFreeFormView.scss';
-import { TopBar } from '../../topbar/TopBar';
 
 export interface CollectionFreeFormInfoUIProps {
     Document: Doc;
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 9500b918a..7bfbbf3f9 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -1417,8 +1417,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         return anchor;
     };
 
-    @action closeInfo = () => (this.Document._hideInfo = true);
-    infoUI = () => (this.Document._hideInfo || this.Document.annotationOn || this._props.renderDepth ? null : <CollectionFreeFormInfoUI Document={this.Document} Freeform={this} close={this.closeInfo} />);
+    @action closeInfo = () => (Doc.IsInfoUIDisabled = true);
+    infoUI = () => (Doc.IsInfoUIDisabled || this.Document.annotationOn || this._props.renderDepth ? null : <CollectionFreeFormInfoUI Document={this.Document} Freeform={this} close={this.closeInfo} />);
 
     componentDidMount() {
         this._props.setContentViewBox?.(this);
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index e47bfcbed..7060202d7 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -182,6 +182,8 @@ export class Doc extends RefField {
     public static set noviceMode(val)         { Doc.UserDoc().noviceMode = val; } // prettier-ignore
     public static get IsSharingEnabled()      { return BoolCast(Doc.UserDoc().isSharingEnabled); } // prettier-ignore
     public static set IsSharingEnabled(val)   { Doc.UserDoc().isSharingEnabled = val; } // prettier-ignore
+    public static get IsInfoUIDisabled()      { return Doc.UserDoc().isInfoUIDisabled ? true : false; } // prettier-ignore
+    public static set IsInfoUIDisabled(val)   { Doc.UserDoc().isInfoUIDisabled = val; } // prettier-ignore
     public static get defaultAclPrivate()     { return Doc.UserDoc().defaultAclPrivate; } // prettier-ignore
     public static set defaultAclPrivate(val)  { Doc.UserDoc().defaultAclPrivate = val; } // prettier-ignore
     public static get ActivePage()            { return StrCast(Doc.UserDoc().activePage); } // prettier-ignore
-- 
cgit v1.2.3-70-g09d2


From 606088e419f0e146715244d00840349b587c80ba Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Sun, 17 Mar 2024 11:50:15 -0400
Subject: use metakey to edit computedfield result instead of expression in
 schema cell,  set default new field values on data doc. fixed stacking view
 from autoresizing when switching to a different collection view.  changed
 syntax for setting fields in text docs to use ':=' for computed fields.  
 Added call to Chat in computed functions when (( )) is used.  Added caching
 of computed function result when a function called by ComputedField uses the
 _setCacheResult_ method (currently only gptCallChat).

---
 src/client/documents/Documents.ts                  |   2 +-
 src/client/util/SnappingManager.ts                 |   5 +-
 src/client/views/GlobalKeyHandler.ts               |   2 +
 .../views/collections/CollectionStackingView.tsx   |   1 +
 .../collectionFreeForm/CollectionFreeFormView.tsx  |   2 +-
 .../collectionSchema/CollectionSchemaView.tsx      |  11 ++-
 .../collectionSchema/SchemaTableCell.tsx           |  23 +++--
 src/client/views/nodes/ComparisonBox.tsx           |  35 ++++---
 src/client/views/nodes/DocumentView.tsx            |   2 +-
 src/client/views/nodes/FieldView.tsx               |   6 +-
 src/client/views/nodes/KeyValueBox.tsx             |  65 ++++++++-----
 src/client/views/nodes/KeyValuePair.tsx            |   2 +-
 .../views/nodes/formattedText/DashFieldView.tsx    |   2 +-
 .../views/nodes/formattedText/RichTextRules.ts     |  36 +++++---
 src/client/views/nodes/formattedText/nodes_rts.ts  |   1 -
 src/fields/Doc.ts                                  |  21 ++++-
 src/fields/ScriptField.ts                          | 102 +++++++++++++--------
 src/server/public/assets/documentation.png         | Bin 0 -> 4526 bytes
 18 files changed, 203 insertions(+), 115 deletions(-)
 create mode 100644 src/server/public/assets/documentation.png

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

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index a13edec77..1d7c73306 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -175,7 +175,7 @@ class DateInfo extends FInfo {
 }
 class RtfInfo extends FInfo {
     constructor(d: string, filterable?: boolean) {
-        super(d, true);
+        super(d);
         this.filterable = filterable;
     }
     fieldType? = FInfoFieldType.rtf;
diff --git a/src/client/util/SnappingManager.ts b/src/client/util/SnappingManager.ts
index 40c3f76fb..359140732 100644
--- a/src/client/util/SnappingManager.ts
+++ b/src/client/util/SnappingManager.ts
@@ -9,6 +9,7 @@ export class SnappingManager {
 
     @observable _shiftKey = false;
     @observable _ctrlKey = false;
+    @observable _metaKey = false;
     @observable _isLinkFollowing = false;
     @observable _isDragging: boolean = false;
     @observable _isResizing: Doc | undefined = undefined;
@@ -32,6 +33,7 @@ export class SnappingManager {
     public static get VertSnapLines()  { return this.Instance._vertSnapLines; } // prettier-ignore
     public static get ShiftKey()       { return this.Instance._shiftKey; } // prettier-ignore
     public static get CtrlKey()        { return this.Instance._ctrlKey; } // prettier-ignore
+    public static get MetaKey()        { return this.Instance._metaKey; } // prettier-ignore
     public static get IsLinkFollowing(){ return this.Instance._isLinkFollowing; } // prettier-ignore
     public static get IsDragging()     { return this.Instance._isDragging; } // prettier-ignore
     public static get IsResizing()     { return this.Instance._isResizing; } // prettier-ignore
@@ -39,7 +41,8 @@ export class SnappingManager {
     public static get ExploreMode()    { return this.Instance._exploreMode; } // prettier-ignore
     public static SetShiftKey       = (down: boolean) => runInAction(() => (this.Instance._shiftKey = down)); // prettier-ignore
     public static SetCtrlKey        = (down: boolean) => runInAction(() => (this.Instance._ctrlKey = down)); // prettier-ignore
-    public static SetIsLinkFollowing= (follow: boolean) => runInAction(() => (this.Instance._isLinkFollowing = follow)); // prettier-ignore
+    public static SetMetaKey        = (down: boolean) => runInAction(() => (this.Instance._metaKey = down)); // prettier-ignore
+    public static SetIsLinkFollowing= (follow:boolean)=> runInAction(() => (this.Instance._isLinkFollowing = follow)); // prettier-ignore
     public static SetIsDragging     = (drag: boolean) => runInAction(() => (this.Instance._isDragging = drag)); // prettier-ignore
     public static SetIsResizing     = (doc: Opt<Doc>) => runInAction(() => (this.Instance._isResizing = doc)); // prettier-ignore
     public static SetCanEmbed       = (embed:boolean) => runInAction(() => (this.Instance._canEmbed = embed)); // prettier-ignore
diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts
index e800798ca..667d8dbb0 100644
--- a/src/client/views/GlobalKeyHandler.ts
+++ b/src/client/views/GlobalKeyHandler.ts
@@ -55,10 +55,12 @@ export class KeyManager {
     public handleModifiers = action((e: KeyboardEvent) => {
         if (e.shiftKey) SnappingManager.SetShiftKey(true);
         if (e.ctrlKey) SnappingManager.SetCtrlKey(true);
+        if (e.metaKey) SnappingManager.SetMetaKey(true);
     });
     public unhandleModifiers = action((e: KeyboardEvent) => {
         if (!e.shiftKey) SnappingManager.SetShiftKey(false);
         if (!e.ctrlKey) SnappingManager.SetCtrlKey(false);
+        if (!e.metaKey) SnappingManager.SetMetaKey(false);
     });
 
     public handle = action((e: KeyboardEvent) => {
diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx
index ea1caf58f..2b23935eb 100644
--- a/src/client/views/collections/CollectionStackingView.tsx
+++ b/src/client/views/collections/CollectionStackingView.tsx
@@ -225,6 +225,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
 
     componentWillUnmount() {
         super.componentWillUnmount();
+        this.observer.disconnect();
         Object.keys(this._disposers).forEach(key => this._disposers[key]());
     }
 
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 7bfbbf3f9..b2fb5848e 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -249,7 +249,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     // this search order, for example, allows icons of cropped images to find the panx/pany/zoom on the cropped image's data doc instead of the usual layout doc because the zoom/panX/panY define the cropped image
     panX = () => this.freeformData()?.bounds.cx ?? NumCast(this.Document[this.panXFieldKey], NumCast(Cast(this.Document.resolvedDataDoc, Doc, null)?.freeform_panX, 1));
     panY = () => this.freeformData()?.bounds.cy ?? NumCast(this.Document[this.panYFieldKey], NumCast(Cast(this.Document.resolvedDataDoc, Doc, null)?.freeform_panY, 1));
-    zoomScaling = () => this.freeformData()?.scale ?? NumCast(Doc.Layout(this.Document)[this.scaleFieldKey], NumCast(Cast(this.Document.resolvedDataDoc, Doc, null)?.[this.scaleFieldKey], 1));
+    zoomScaling = () => this.freeformData()?.scale ?? NumCast(Doc.Layout(this.Document)[this.scaleFieldKey], 1); //, NumCast(DocCast(this.Document.resolvedDataDoc)?.[this.scaleFieldKey], 1));
     PanZoomCenterXf = () =>
         this._props.isAnnotationOverlay && this.zoomScaling() === 1 ? `` : `translate(${this.cachedCenteringShiftX}px, ${this.cachedCenteringShiftY}px) scale(${this.zoomScaling()}) translate(${-this.panX()}px, ${-this.panY()}px)`;
     ScreenToContentsXf = () => this.screenToFreeformContentsXf.copy();
diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx
index 12f0ad5e9..4a0ca8fe5 100644
--- a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx
+++ b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx
@@ -1,5 +1,6 @@
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { action, computed, makeObservable, observable, ObservableMap, observe, trace } from 'mobx';
+import { Popup, PopupTrigger, Type } from 'browndash-components';
+import { action, computed, makeObservable, observable, ObservableMap, observe } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
 import { Doc, DocListCast, Field, NumListCast, Opt, StrListCast } from '../../../../fields/Doc';
@@ -12,12 +13,13 @@ import { Docs, DocumentOptions, DocUtils, FInfo } from '../../../documents/Docum
 import { DocumentManager } from '../../../util/DocumentManager';
 import { DragManager, dropActionType } from '../../../util/DragManager';
 import { SelectionManager } from '../../../util/SelectionManager';
+import { SettingsManager } from '../../../util/SettingsManager';
 import { undoable, undoBatch } from '../../../util/UndoManager';
 import { ContextMenu } from '../../ContextMenu';
 import { EditableView } from '../../EditableView';
 import { Colors } from '../../global/globalEnums';
 import { DocumentView } from '../../nodes/DocumentView';
-import { FocusViewOptions, FieldViewProps } from '../../nodes/FieldView';
+import { FieldViewProps, FocusViewOptions } from '../../nodes/FieldView';
 import { KeyValueBox } from '../../nodes/KeyValueBox';
 import { ObservableReactComponent } from '../../ObservableReactComponent';
 import { DefaultStyleProvider, StyleProp } from '../../StyleProvider';
@@ -25,8 +27,7 @@ import { CollectionSubView } from '../CollectionSubView';
 import './CollectionSchemaView.scss';
 import { SchemaColumnHeader } from './SchemaColumnHeader';
 import { SchemaRowBox } from './SchemaRowBox';
-import { Popup, PopupTrigger, Type } from 'browndash-components';
-import { SettingsManager } from '../../../util/SettingsManager';
+import { DocData } from '../../../../fields/DocSymbols';
 const { default: { SCHEMA_NEW_NODE_HEIGHT } } = require('../../global/globalCssVariables.module.scss'); // prettier-ignore
 
 export enum ColumnType {
@@ -284,7 +285,7 @@ export class CollectionSchemaView extends CollectionSubView() {
     };
 
     @action
-    addNewKey = (key: string, defaultVal: any) => this.childDocs.forEach(doc => (doc[key] = defaultVal));
+    addNewKey = (key: string, defaultVal: any) => this.childDocs.forEach(doc => (doc[DocData][key] = defaultVal));
 
     @undoBatch
     removeColumn = (index: number) => {
diff --git a/src/client/views/collections/collectionSchema/SchemaTableCell.tsx b/src/client/views/collections/collectionSchema/SchemaTableCell.tsx
index ed1b519b4..711ef507c 100644
--- a/src/client/views/collections/collectionSchema/SchemaTableCell.tsx
+++ b/src/client/views/collections/collectionSchema/SchemaTableCell.tsx
@@ -1,8 +1,11 @@
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { Popup, Size, Type } from 'browndash-components';
 import { action, computed, makeObservable, observable } from 'mobx';
 import { observer } from 'mobx-react';
 import { extname } from 'path';
 import * as React from 'react';
 import DatePicker from 'react-datepicker';
+import 'react-datepicker/dist/react-datepicker.css';
 import Select from 'react-select';
 import { Utils, emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnZero } from '../../../../Utils';
 import { DateField } from '../../../../fields/DateField';
@@ -12,6 +15,8 @@ import { BoolCast, Cast, DateCast, DocCast, FieldValue, StrCast } from '../../..
 import { ImageField } from '../../../../fields/URLField';
 import { FInfo, FInfoFieldType } from '../../../documents/Documents';
 import { DocFocusOrOpen } from '../../../util/DocumentManager';
+import { dropActionType } from '../../../util/DragManager';
+import { SettingsManager } from '../../../util/SettingsManager';
 import { Transform } from '../../../util/Transform';
 import { undoBatch, undoable } from '../../../util/UndoManager';
 import { EditableView } from '../../EditableView';
@@ -24,12 +29,7 @@ import { KeyValueBox } from '../../nodes/KeyValueBox';
 import { FormattedTextBox } from '../../nodes/formattedText/FormattedTextBox';
 import { ColumnType, FInfotoColType } from './CollectionSchemaView';
 import './CollectionSchemaView.scss';
-import 'react-datepicker/dist/react-datepicker.css';
-import { Popup, Size, Type } from 'browndash-components';
-import { IconLookup, faCaretDown } from '@fortawesome/free-solid-svg-icons';
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { SettingsManager } from '../../../util/SettingsManager';
-import { dropActionType } from '../../../util/DragManager';
+import { SnappingManager } from '../../../util/SnappingManager';
 
 export interface SchemaTableCellProps {
     Document: Doc;
@@ -129,10 +129,12 @@ export class SchemaTableCell extends ObservableReactComponent<SchemaTableCellPro
                     contents={undefined}
                     fieldContents={fieldProps}
                     editing={this.selected ? undefined : false}
-                    GetValue={() => Field.toKeyValueString(this._props.Document, this._props.fieldKey)}
+                    GetValue={() => Field.toKeyValueString(this._props.Document, this._props.fieldKey, SnappingManager.MetaKey)}
                     SetValue={undoable((value: string, shiftDown?: boolean, enterKey?: boolean) => {
                         if (shiftDown && enterKey) {
                             this._props.setColumnValues(this._props.fieldKey.replace(/^_/, ''), value);
+                            this._props.finishEdit?.();
+                            return true;
                         }
                         const ret = KeyValueBox.SetField(this._props.Document, this._props.fieldKey.replace(/^_/, ''), value, Doc.IsDataProto(this._props.Document) ? true : undefined);
                         this._props.finishEdit?.();
@@ -345,8 +347,7 @@ export class SchemaBoolCell extends ObservableReactComponent<SchemaTableCellProp
                     onChange={undoBatch((value: React.ChangeEvent<HTMLInputElement> | undefined) => {
                         if ((value?.nativeEvent as any).shiftKey) {
                             this._props.setColumnValues(this._props.fieldKey.replace(/^_/, ''), (color === 'black' ? '=' : '') + value?.target?.checked.toString());
-                        }
-                        KeyValueBox.SetField(this._props.Document, this._props.fieldKey.replace(/^_/, ''), (color === 'black' ? '=' : '') + value?.target?.checked.toString());
+                        } else KeyValueBox.SetField(this._props.Document, this._props.fieldKey.replace(/^_/, ''), (color === 'black' ? '=' : '') + value?.target?.checked.toString());
                     })}
                 />
                 <EditableView
@@ -357,8 +358,10 @@ export class SchemaBoolCell extends ObservableReactComponent<SchemaTableCellProp
                     SetValue={undoBatch((value: string, shiftDown?: boolean, enterKey?: boolean) => {
                         if (shiftDown && enterKey) {
                             this._props.setColumnValues(this._props.fieldKey.replace(/^_/, ''), value);
+                            this._props.finishEdit?.();
+                            return true;
                         }
-                        const set = KeyValueBox.SetField(this._props.Document, this._props.fieldKey.replace(/^_/, ''), value);
+                        const set = KeyValueBox.SetField(this._props.Document, this._props.fieldKey.replace(/^_/, ''), value, Doc.IsDataProto(this._props.Document) ? true : undefined);
                         this._props.finishEdit?.();
                         return set;
                     })}
diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx
index e759030f5..715b23fb6 100644
--- a/src/client/views/nodes/ComparisonBox.tsx
+++ b/src/client/views/nodes/ComparisonBox.tsx
@@ -4,6 +4,7 @@ import { observer } from 'mobx-react';
 import * as React from 'react';
 import { emptyFunction, returnFalse, returnNone, returnZero, setupMoveUpEvents } from '../../../Utils';
 import { Doc, Opt } from '../../../fields/Doc';
+import { RichTextField } from '../../../fields/RichTextField';
 import { DocCast, NumCast, RTFCast, StrCast } from '../../../fields/Types';
 import { DocUtils, Docs } from '../../documents/Documents';
 import { DragManager, dropActionType } from '../../util/DragManager';
@@ -13,11 +14,9 @@ import { StyleProp } from '../StyleProvider';
 import './ComparisonBox.scss';
 import { DocumentView } from './DocumentView';
 import { FieldView, FieldViewProps } from './FieldView';
-import { PinProps, PresBox } from './trails';
+import { KeyValueBox } from './KeyValueBox';
 import { FormattedTextBox } from './formattedText/FormattedTextBox';
-import { RichTextField } from '../../../fields/RichTextField';
-import { GPTCallType, gptAPICall } from '../../apis/gpt/GPT';
-import { DocData } from '../../../fields/DocSymbols';
+import { PinProps, PresBox } from './trails';
 
 @observer
 export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implements ViewBoxInterface {
@@ -173,10 +172,17 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
                 </div>
             );
         };
+
+        /**
+         * Display the Docs in the before/after fields of the comparison.  This also supports a GPT flash card use case
+         * where if there are no Docs in the slots, but the main fieldKey contains text, then
+         * @param which
+         * @returns
+         */
         const displayDoc = (which: string) => {
             const whichDoc = DocCast(this.dataDoc[which]);
             const targetDoc = DocCast(whichDoc?.annotationOn, whichDoc);
-            const subjectText = RTFCast(this.Document[this.fieldKey])?.Text;
+            const subjectText = RTFCast(this.Document[this.fieldKey])?.Text.trim();
             // if there is no Doc in the first comparison slot, but the comparison box's fieldKey slot has a RichTextField, then render a text box to show the contents of the document's field key slot
             // of if there is no Doc in the second comparison slot, but the second slot has a RichTextField, then render a text box to show the contents of the document's field key slot
             const layoutTemplateString = !targetDoc
@@ -188,15 +194,18 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
                 : undefined;
 
             // A bit hacky to try out the concept of using GPT to fill in flashcards -- this whole process should probably be packaged into a script to be more generic.
-            //   If the second slot doesn't have anything in it, but the fieldKey slot has text
-            // and the fieldKey + "_alternate" has a text that incldues "--TEXT--", then
-            // treat the fieldKey + "_altenrate" text as a GPT query parameterized by the fieldKey text
-            // Call GPT to fill in an "answer" value in the second slot.
+            //   If the second slot doesn't have anything in it, but the fieldKey slot has text (e.g., this.text is a string)
+            // and the fieldKey + "_alternate" has text, then treat the _alternate's text as a GPT query (indicated by (( && )) ) that is parameterized (optionally)
+            // by the field references in the text (eg., this.text_alternate is
+            //     "((Provide a one sentence definition for (this) that doesn't use any word in (this.excludeWords) ))"
+            //     where (this) is replaced by the text in the fieldKey slot abd this.excludeWords is repalced by the conetnts of the excludeWords field
+            // A GPT call will put the "answer"  in the second slot of the comparison (eg., text_2)
             if (which.endsWith('2') && !layoutTemplateString && !targetDoc) {
-                const queryText = RTFCast(this.Document[this.fieldKey + '_alternate'])?.Text;
-                if (queryText?.includes('--TEXT--') && subjectText) {
-                    this.Document[DocData][this.fieldKey + '_2'] = '';
-                    gptAPICall(queryText?.replace('--TEXT--', subjectText), GPTCallType.COMPLETION).then(value => (this.Document[DocData][this.fieldKey + '_2'] = value.trim()));
+                var queryText = RTFCast(this.Document[this.fieldKey + '_alternate'])
+                    ?.Text.replace('(this)', subjectText) // TODO: this should be done in KeyValueBox.setField but it doesn't know about the fieldKey ...
+                    .trim();
+                if (subjectText && queryText.match(/\(\(.*\)\)/)) {
+                    KeyValueBox.SetField(this.Document, which, ':=' + queryText, false); // make the second slot be a computed field on the data doc that calls ChatGpt
                 }
             }
             return targetDoc || layoutTemplateString ? (
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index bbeacef88..9848f18e0 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -1215,7 +1215,7 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
             if (layout_fieldKey && layout_fieldKey !== 'layout' && layout_fieldKey !== 'layout_icon') this.Document.deiconifyLayout = layout_fieldKey.replace('layout_', '');
         } else {
             const deiconifyLayout = Cast(this.Document.deiconifyLayout, 'string', null);
-            this.switchViews(deiconifyLayout ? true : false, deiconifyLayout, finalFinished);
+            this.switchViews(deiconifyLayout ? true : false, deiconifyLayout, finalFinished, true);
             this.Document.deiconifyLayout = undefined;
             this._props.bringToFront?.(this.Document);
         }
diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx
index 8a49b4757..4ecaaa283 100644
--- a/src/client/views/nodes/FieldView.tsx
+++ b/src/client/views/nodes/FieldView.tsx
@@ -11,6 +11,7 @@ import { ViewBoxInterface } from '../DocComponent';
 import { CollectionFreeFormDocumentView } from './CollectionFreeFormDocumentView';
 import { DocumentView, OpenWhere } from './DocumentView';
 import { PinProps } from './trails';
+import { computed } from 'mobx';
 
 export interface FocusViewOptions {
     willPan?: boolean; // determines whether to pan to target document
@@ -121,9 +122,12 @@ export class FieldView extends React.Component<FieldViewProps> {
     public static LayoutString(fieldType: { name: string }, fieldStr: string) {
         return `<${fieldType.name} {...props} fieldKey={'${fieldStr}'}/>`; //e.g., "<ImageBox {...props} fieldKey={'data'} />"
     }
+    @computed get fieldval() {
+        return this.props.Document[this.props.fieldKey];
+    }
 
     render() {
-        const field = this.props.Document[this.props.fieldKey];
+        const field = this.fieldval;
         // prettier-ignore
         if (field instanceof Doc)  return <p> <b>{field.title?.toString()}</b></p>;
         if (field === undefined) return <p>{'<null>'}</p>;
diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx
index 89a5ac0b8..2257e6455 100644
--- a/src/client/views/nodes/KeyValueBox.tsx
+++ b/src/client/views/nodes/KeyValueBox.tsx
@@ -6,7 +6,7 @@ import { Doc, Field, FieldResult } from '../../../fields/Doc';
 import { List } from '../../../fields/List';
 import { RichTextField } from '../../../fields/RichTextField';
 import { ComputedField, ScriptField } from '../../../fields/ScriptField';
-import { DocCast } from '../../../fields/Types';
+import { DocCast, StrCast } from '../../../fields/Types';
 import { ImageField } from '../../../fields/URLField';
 import { Docs } from '../../documents/Documents';
 import { SetupDrag } from '../../util/DragManager';
@@ -71,34 +71,51 @@ export class KeyValueBox extends ObservableReactComponent<FieldViewProps> {
             }
         }
     };
-    public static CompileKVPScript(value: string): KVPScript | undefined {
-        const eq = value.startsWith('=');
-        value = eq ? value.substring(1) : value;
-        const dubEq = value.startsWith(':=') ? 'computed' : value.startsWith('$=') ? 'script' : false;
-        value = dubEq ? value.substring(2) : value;
-        const options: ScriptOptions = { addReturn: true, typecheck: false, params: { this: Doc.name, self: Doc.name, documentView: 'any', _last_: 'any', _readOnly_: 'boolean' }, editable: true };
-        if (dubEq) options.typecheck = false;
-        const script = CompileScript(value, { ...options, transformer: DocumentIconContainer.getTransformer() });
-        return !script.compiled ? undefined : { script, type: dubEq, onDelegate: eq };
+    /**
+     * this compiles a string as a script after parsing off initial characters that determine script parameters
+     * if the script starts with '=', then it will be stored on the delegate of the Doc, otherise on the data doc
+     * if the script then starts with a ':=', then it will be treated as ComputedField,
+     *                                  '$=', then it will just be a Script
+     * @param value
+     * @returns
+     */
+    public static CompileKVPScript(rawvalue: string): KVPScript | undefined {
+        const onDelegate = rawvalue.startsWith('=');
+        rawvalue = onDelegate ? rawvalue.substring(1) : rawvalue;
+        const type: 'computed' | 'script' | false = rawvalue.startsWith(':=') ? 'computed' : rawvalue.startsWith('$=') ? 'script' : false;
+        rawvalue = type ? rawvalue.substring(2) : rawvalue;
+        rawvalue = rawvalue.replace(/.*\(\((.*)\)\)/, 'dashCallChat(_setCacheResult_, this, "$1")');
+        const value = ["'", '"', '`'].includes(rawvalue.length ? rawvalue[0] : '') || !isNaN(rawvalue as any) ? rawvalue : '`' + rawvalue + '`';
+
+        var script = ScriptField.CompileScript(rawvalue, {}, true, undefined, DocumentIconContainer.getTransformer());
+        if (!script.compiled) {
+            script = ScriptField.CompileScript(value, {}, true, undefined, DocumentIconContainer.getTransformer());
+        }
+        return !script.compiled ? undefined : { script, type, onDelegate };
     }
 
-    public static ApplyKVPScript(doc: Doc, key: string, kvpScript: KVPScript, forceOnDelegate?: boolean): boolean {
+    public static ApplyKVPScript(doc: Doc, key: string, kvpScript: KVPScript, forceOnDelegate?: boolean, setResult?: (value: FieldResult) => void) {
         const { script, type, onDelegate } = kvpScript;
         //const target = onDelegate ? Doc.Layout(doc.layout) : Doc.GetProto(doc); // bcz: TODO need to be able to set fields on layout templates
         const target = forceOnDelegate || onDelegate || key.startsWith('_') ? doc : DocCast(doc.proto, doc);
-        let field: Field;
-        if (type === 'computed') {
-            field = new ComputedField(script);
-        } else if (type === 'script') {
-            field = new ScriptField(script);
-        } else {
-            const res = script.run({ this: Doc.Layout(doc), self: doc }, console.log);
-            if (!res.success) {
-                target[key] = script.originalScript;
-                return true;
+        let field: Field | undefined;
+        switch (type) {
+            case 'computed': field = new ComputedField(script); break; // prettier-ignore
+            case 'script': field = new ScriptField(script); break; // prettier-ignore
+            default: {
+                const _setCacheResult_ = (value: FieldResult) => {
+                    field = value as Field;
+                    setResult?.(value);
+                };
+                const res = script.run({ this: Doc.Layout(doc), self: doc, _setCacheResult_ }, console.log);
+                if (!res.success) {
+                    if (key) target[key] = script.originalScript;
+                    return false;
+                }
+                field === undefined && (field = res.result);
             }
-            field = res.result;
         }
+        if (!key) return field;
         if (Field.IsField(field, true) && (key !== 'proto' || field !== target)) {
             target[key] = field;
             return true;
@@ -107,10 +124,10 @@ export class KeyValueBox extends ObservableReactComponent<FieldViewProps> {
     }
 
     @undoBatch
-    public static SetField(doc: Doc, key: string, value: string, forceOnDelegate?: boolean) {
+    public static SetField(doc: Doc, key: string, value: string, forceOnDelegate?: boolean, setResult?: (value: FieldResult) => void) {
         const script = this.CompileKVPScript(value);
         if (!script) return false;
-        return this.ApplyKVPScript(doc, key, script, forceOnDelegate);
+        return this.ApplyKVPScript(doc, key, script, forceOnDelegate, setResult);
     }
 
     onPointerDown = (e: React.PointerEvent): void => {
diff --git a/src/client/views/nodes/KeyValuePair.tsx b/src/client/views/nodes/KeyValuePair.tsx
index f9e8ce4f3..d59489a78 100644
--- a/src/client/views/nodes/KeyValuePair.tsx
+++ b/src/client/views/nodes/KeyValuePair.tsx
@@ -125,7 +125,7 @@ export class KeyValuePair extends ObservableReactComponent<KeyValuePairProps> {
                                 pinToPres: returnZero,
                             }}
                             GetValue={() => Field.toKeyValueString(this._props.doc, this._props.keyName)}
-                            SetValue={(value: string) => KeyValueBox.SetField(this._props.doc, this._props.keyName, value)}
+                            SetValue={(value: string) => (KeyValueBox.SetField(this._props.doc, this._props.keyName, value) ? true : false)}
                         />
                     </div>
                 </td>
diff --git a/src/client/views/nodes/formattedText/DashFieldView.tsx b/src/client/views/nodes/formattedText/DashFieldView.tsx
index 5c4d850ad..62cb460c2 100644
--- a/src/client/views/nodes/formattedText/DashFieldView.tsx
+++ b/src/client/views/nodes/formattedText/DashFieldView.tsx
@@ -137,7 +137,7 @@ export class DashFieldViewInternal extends ObservableReactComponent<IDashFieldVi
                     col={0}
                     deselectCell={emptyFunction}
                     selectCell={emptyFunction}
-                    maxWidth={this._props.hideKey ? undefined : this.return100}
+                    maxWidth={this._props.hideKey ? undefined : this._props.tbox._props.PanelWidth}
                     columnWidth={this._props.hideKey ? () => this._props.tbox._props.PanelWidth() - 20 : returnZero}
                     selectedCell={() => [this._dashDoc!, 0]}
                     fieldKey={this._fieldKey}
diff --git a/src/client/views/nodes/formattedText/RichTextRules.ts b/src/client/views/nodes/formattedText/RichTextRules.ts
index d5c91fc09..c798ae4b3 100644
--- a/src/client/views/nodes/formattedText/RichTextRules.ts
+++ b/src/client/views/nodes/formattedText/RichTextRules.ts
@@ -1,6 +1,6 @@
 import { ellipsis, emDash, InputRule, smartQuotes, textblockTypeInputRule } from 'prosemirror-inputrules';
 import { NodeSelection, TextSelection } from 'prosemirror-state';
-import { Doc, StrListCast } from '../../../../fields/Doc';
+import { Doc, FieldResult, StrListCast } from '../../../../fields/Doc';
 import { DocData } from '../../../../fields/DocSymbols';
 import { Id } from '../../../../fields/FieldSymbols';
 import { List } from '../../../../fields/List';
@@ -8,13 +8,14 @@ import { NumCast, StrCast } from '../../../../fields/Types';
 import { Utils } from '../../../../Utils';
 import { DocServer } from '../../../DocServer';
 import { Docs, DocUtils } from '../../../documents/Documents';
+import { CollectionViewType } from '../../../documents/DocumentTypes';
+import { CollectionView } from '../../collections/CollectionView';
+import { ContextMenu } from '../../ContextMenu';
+import { KeyValueBox } from '../KeyValueBox';
 import { FormattedTextBox } from './FormattedTextBox';
 import { wrappingInputRule } from './prosemirrorPatches';
 import { RichTextMenu } from './RichTextMenu';
 import { schema } from './schema_rts';
-import { CollectionView } from '../../collections/CollectionView';
-import { CollectionViewType } from '../../../documents/DocumentTypes';
-import { ContextMenu } from '../../ContextMenu';
 
 export class RichTextRules {
     public Document: Doc;
@@ -282,18 +283,28 @@ export class RichTextRules {
                     : tr;
             }),
 
+            new InputRule(new RegExp(/(^|[^=])(\(\(.*\)\))/), (state, match, start, end) => {
+                var count = 0; // ignore first return value which will be the notation that chat is pending a result
+                KeyValueBox.SetField(this.Document, '', match[2], false, (gptval: FieldResult) => {
+                    count && this.TextBox.EditorView?.dispatch(this.TextBox.EditorView!.state.tr.insertText(' ' + (gptval as string)));
+                    count++;
+                });
+                return null;
+            }),
+
             // create a text display of a metadata field on this or another document, or create a hyperlink portal to another document
             // [[<fieldKey> : <Doc>]]
             // [[:docTitle]] => hyperlink
             // [[fieldKey]] => show field
-            // [[fieldKey=value]] => show field and also set its value
+            // [[fieldKey{:,=:}=value]] => show field and also set its value
             // [[fieldKey:docTitle]] => show field of doc
             new InputRule(
-                new RegExp(/\[\[([a-zA-Z_\? \-0-9]*)(=[a-z,A-Z_@\? /\-0-9]*)?(:[a-zA-Z_@:\.\? \-0-9]+)?\]\]$/),
+                new RegExp(/\[\[([a-zA-Z_\? \-0-9]*)((=:|:)?=)([a-z,A-Z_@\?+\-*/\ 0-9\(\)]*)?(:[a-zA-Z_@:\.\? \-0-9]+)?\]\]$/),
                 (state, match, start, end) => {
                     const fieldKey = match[1];
-                    const docTitle = match[3]?.replace(':', '');
-                    const value = match[2]?.substring(1);
+                    const assign = match[2] === '=' ? '' : match[2];
+                    const value = match[4];
+                    const docTitle = match[5]?.replace(':', '');
                     const linkToDoc = (target: Doc) => {
                         const rstate = this.TextBox.EditorView?.state;
                         const selection = rstate?.selection.$from.pos;
@@ -325,13 +336,14 @@ export class RichTextRules {
                         }
                         return state.tr;
                     }
-                    if (value?.includes(',')) {
+                    // if the value has commas assume its an array (unless it's part of a chat gpt call indicated by '((' )
+                    if (value?.includes(',') && !value.startsWith('((')) {
                         const values = value.split(',');
                         const strs = values.some(v => !v.match(/^[-]?[0-9.]$/));
                         this.Document[DocData][fieldKey] = strs ? new List<string>(values) : new List<number>(values.map(v => Number(v)));
-                    } else if (value !== '' && value !== undefined) {
-                        const num = value.match(/^[0-9.]$/);
-                        this.Document[DocData][fieldKey] = value === 'true' ? true : value === 'false' ? false : num ? Number(value) : value;
+                    } else if (value) {
+                        KeyValueBox.SetField(this.Document, fieldKey, assign + value, Doc.IsDataProto(this.Document) ? true : undefined, assign ? undefined: 
+                            (gptval: FieldResult) => this.Document[DocData][fieldKey] = gptval as string ); // prettier-ignore
                     }
                     const target = getTitledDoc(docTitle);
                     const fieldView = state.schema.nodes.dashField.create({ fieldKey, docId: target?.[Id], hideKey: false });
diff --git a/src/client/views/nodes/formattedText/nodes_rts.ts b/src/client/views/nodes/formattedText/nodes_rts.ts
index 4706a97fa..c9115be90 100644
--- a/src/client/views/nodes/formattedText/nodes_rts.ts
+++ b/src/client/views/nodes/formattedText/nodes_rts.ts
@@ -1,4 +1,3 @@
-import * as React from 'react';
 import { DOMOutputSpec, Node, NodeSpec } from 'prosemirror-model';
 import { listItem, orderedList } from 'prosemirror-schema-list';
 import { ParagraphNodeSpec, toParagraphDOM, getParagraphNodeAttrs } from './ParagraphNodeSpec';
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index 921d7aa5d..30f5f716c 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -34,14 +34,29 @@ import * as JSZip from 'jszip';
 import { FieldViewProps } from '../client/views/nodes/FieldView';
 export const LinkedTo = '-linkedTo';
 export namespace Field {
-    export function toKeyValueString(doc: Doc, key: string): string {
-        const onDelegate = Object.keys(doc).includes(key.replace(/^_/, ''));
+    /**
+     * Converts a field to its equivalent input string in the key value box such that if the string
+     * is entered into a keyValueBox it will create an equivalent field (except if showComputedValue is set).
+     * @param doc doc containing key
+     * @param key field key to display
+     * @param showComputedValue whether copmuted function should display its value instead of its function
+     * @returns string representation of the field
+     */
+    export function toKeyValueString(doc: Doc, key: string, showComputedValue?: boolean): string {
+        const onDelegate = !Doc.IsDataProto(doc) && Object.keys(doc).includes(key.replace(/^_/, ''));
         const field = ComputedField.WithoutComputed(() => FieldValue(doc[key]));
         return !Field.IsField(field)
             ? key.startsWith('_')
                 ? '='
                 : ''
-            : (onDelegate ? '=' : '') + (field instanceof ComputedField ? `:=${field.script.originalScript}` : field instanceof ScriptField ? `$=${field.script.originalScript}` : Field.toScriptString(field));
+            : (onDelegate ? '=' : '') +
+                  (field instanceof ComputedField && showComputedValue
+                      ? field._lastComputedResult
+                      : field instanceof ComputedField
+                        ? `:=${field.script.originalScript}`
+                        : field instanceof ScriptField
+                          ? `$=${field.script.originalScript}`
+                          : Field.toScriptString(field));
     }
     export function toScriptString(field: Field) {
         switch (typeof field) {
diff --git a/src/fields/ScriptField.ts b/src/fields/ScriptField.ts
index c7fe72ca6..9021c8896 100644
--- a/src/fields/ScriptField.ts
+++ b/src/fields/ScriptField.ts
@@ -1,15 +1,17 @@
+import { action, makeObservable, observable } from 'mobx';
 import { computedFn } from 'mobx-utils';
 import { createSimpleSchema, custom, map, object, primitive, PropSchema, serializable, SKIP } from 'serializr';
 import { DocServer } from '../client/DocServer';
-import { CompiledScript, CompileScript, ScriptOptions } from '../client/util/Scripting';
+import { CompiledScript, CompileScript, ScriptOptions, Transformer } from '../client/util/Scripting';
 import { scriptingGlobal, ScriptingGlobals } from '../client/util/ScriptingGlobals';
 import { autoObject, Deserializable } from '../client/util/SerializationHelper';
 import { numberRange } from '../Utils';
-import { Doc, Field, Opt } from './Doc';
-import { Copy, Id, ToJavascriptString, ToScriptString, ToString, ToValue } from './FieldSymbols';
+import { Doc, Field, FieldResult, Opt } from './Doc';
+import { Copy, FieldChanged, Id, ToJavascriptString, ToScriptString, ToString, ToValue } from './FieldSymbols';
 import { List } from './List';
 import { ObjectField } from './ObjectField';
 import { Cast, StrCast } from './Types';
+import { GPTCallType, gptAPICall } from '../client/apis/gpt/GPT';
 
 function optional(propSchema: PropSchema) {
     return custom(
@@ -85,6 +87,13 @@ export class ScriptField extends ObjectField {
     readonly script: CompiledScript;
     @serializable(object(scriptSchema))
     readonly setterscript: CompiledScript | undefined;
+    @serializable
+    @observable
+    _cachedResult: FieldResult = undefined;
+    setCacheResult = action((value: FieldResult) => {
+        this._cachedResult = value;
+        this[FieldChanged]?.();
+    });
 
     @serializable(autoObject())
     captures?: List<string>;
@@ -122,21 +131,25 @@ export class ScriptField extends ObjectField {
     [ToString]() {
         return this.script.originalScript;
     }
-    public static CompileScript(script: string, params: object = {}, addReturn = false, capturedVariables?: { [name: string]: Doc | string | number | boolean }) {
+    public static CompileScript(script: string, params: object = {}, addReturn = false, capturedVariables?: { [name: string]: Doc | string | number | boolean }, transformer?: Transformer) {
         return CompileScript(script, {
             params: {
                 this: Doc?.name || 'Doc', // this is the doc that executes the script
                 self: Doc?.name || 'Doc', // self is the root doc of the doc that executes the script
+                documentView: 'any',
                 _last_: 'any', // _last_ is the previous value of a computed field when it is being triggered to re-run.
+                _setCacheResult_: 'any', // set the cached value of the function
                 _readOnly_: 'boolean', // _readOnly_ is set when a computed field is executed to indicate that it should not have mobx side-effects.  used for checking the value of a set function (see FontIconBox)
                 ...params,
             },
+            transformer,
             typecheck: false,
             editable: true,
             addReturn: addReturn,
             capturedVariables,
         });
     }
+
     public static MakeFunction(script: string, params: object = {}, capturedVariables?: { [name: string]: Doc | string | number | boolean }) {
         const compiled = ScriptField.CompileScript(script, params, true, capturedVariables);
         return compiled.compiled ? new ScriptField(compiled) : undefined;
@@ -146,29 +159,62 @@ export class ScriptField extends ObjectField {
         const compiled = ScriptField.CompileScript(script, params, false, capturedVariables);
         return compiled.compiled ? new ScriptField(compiled) : undefined;
     }
+    public static CallGpt(queryText: string, setVal: (val: FieldResult) => void, target: Doc) {
+        if (typeof queryText === 'string' && setVal) {
+            while (queryText.match(/\(this\.[a-zA-Z_]*\)/)?.length) {
+                const fieldRef = queryText.split('(this.')[1].replace(/\).*/, '');
+                queryText = queryText.replace(/\(this\.[a-zA-Z_]*\)/, Field.toString(target[fieldRef] as Field));
+            }
+            setVal(`Chat Pending: ${queryText}`);
+            gptAPICall(queryText, GPTCallType.COMPLETION).then(result => {
+                if (queryText.includes('#')) {
+                    const matches = result.match(/-?[0-9][0-9,]+[.]?[0-9]*/);
+                    if (matches?.length) setVal(Number(matches[0].replace(/,/g, '')));
+                } else setVal(result.trim());
+            });
+        }
+    }
 }
 
 @scriptingGlobal
 @Deserializable('computed', deserializeScript)
 export class ComputedField extends ScriptField {
-    _lastComputedResult: any;
-    //TODO maybe add an observable cache based on what is passed in for doc, considering there shouldn't really be that many possible values for doc
-    value = computedFn((doc: Doc) => this._valueOutsideReaction(doc));
-    _valueOutsideReaction = (doc: Doc) => (this._lastComputedResult = this.script.compiled && this.script.run({ this: doc, self: doc, value: '', _last_: this._lastComputedResult, _readOnly_: true }, console.log).result);
-
-    [ToValue](doc: Doc) {
-        return ComputedField.toValue(doc, this);
+    static undefined = '__undefined';
+    static useComputed = true;
+    static DisableComputedFields() { this.useComputed = false; } // prettier-ignore
+    static EnableComputedFields()  { this.useComputed = true; } // prettier-ignore
+    static WithoutComputed<T>(fn: () => T) {
+        this.DisableComputedFields();
+        try {
+            return fn();
+        } finally {
+            this.EnableComputedFields();
+        }
     }
-    [Copy](): ObjectField {
-        return new ComputedField(this.script, this.setterscript, this.rawscript);
+
+    constructor(script: CompiledScript | undefined, setterscript?: CompiledScript, rawscript?: string) {
+        super(script, setterscript, rawscript);
+        makeObservable(this);
     }
 
+    _lastComputedResult: FieldResult;
+    value = computedFn((doc: Doc) => this._valueOutsideReaction(doc));
+    _valueOutsideReaction = (doc: Doc) => {
+        this._lastComputedResult =
+            this._cachedResult ?? (this.script.compiled && this.script.run({ this: doc, self: doc, value: '', _setCacheResult_: this.setCacheResult, _last_: this._lastComputedResult, _readOnly_: true }, console.log).result);
+        return this._lastComputedResult;
+    };
+
+    [ToValue](doc: Doc)   { if (ComputedField.useComputed) return { value: this._valueOutsideReaction(doc) };  } // prettier-ignore
+    [Copy](): ObjectField { return new ComputedField(this.script, this.setterscript, this.rawscript); } // prettier-ignore
+
     public static MakeFunction(script: string, params: object = {}, capturedVariables?: { [name: string]: Doc | string | number | boolean }, setterscript?: string) {
         const compiled = ScriptField.CompileScript(script, params, true, { value: '', ...capturedVariables });
         const compiledsetter = setterscript ? ScriptField.CompileScript(setterscript, { ...params, value: 'any' }, false, capturedVariables) : undefined;
         const compiledsetscript = compiledsetter?.compiled ? compiledsetter : undefined;
         return compiled.compiled ? new ComputedField(compiled, compiledsetscript) : undefined;
     }
+
     public static MakeInterpolatedNumber(fieldKey: string, interpolatorKey: string, doc: Doc, curTimecode: number, defaultVal: Opt<number>) {
         if (!doc[`${fieldKey}_indexed`]) {
             const flist = new List<number>(numberRange(curTimecode + 1).map(i => undefined) as any as number[]);
@@ -206,33 +252,6 @@ export class ComputedField extends ScriptField {
         return (doc[`${fieldKey}`] = getField.compiled ? new ComputedField(getField, setField?.compiled ? setField : undefined) : undefined);
     }
 }
-export namespace ComputedField {
-    let useComputed = true;
-    export function DisableComputedFields() {
-        useComputed = false;
-    }
-
-    export function EnableComputedFields() {
-        useComputed = true;
-    }
-
-    export const undefined = '__undefined';
-
-    export function WithoutComputed<T>(fn: () => T) {
-        DisableComputedFields();
-        try {
-            return fn();
-        } finally {
-            EnableComputedFields();
-        }
-    }
-
-    export function toValue(doc: any, value: any) {
-        if (useComputed) {
-            return { value: value._valueOutsideReaction(doc) };
-        }
-    }
-}
 
 ScriptingGlobals.add(
     function setIndexVal(list: any[], index: number, value: any) {
@@ -258,3 +277,6 @@ ScriptingGlobals.add(
     'returns the value at a given index of a list',
     '(list: any[], index: number)'
 );
+ScriptingGlobals.add(function dashCallChat(setVal: (val: FieldResult) => void, target: Doc, queryText: string) {
+    ScriptField.CallGpt(queryText, setVal, target);
+}, 'calls chat gpt for the query string and then calls setVal with the result');
diff --git a/src/server/public/assets/documentation.png b/src/server/public/assets/documentation.png
new file mode 100644
index 000000000..95c76b198
Binary files /dev/null and b/src/server/public/assets/documentation.png differ
-- 
cgit v1.2.3-70-g09d2


From 462580bf70ff563dbfe33a84b8cd015e637cdf7e Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Fri, 22 Mar 2024 11:41:22 -0400
Subject: make ink stroke labels visible always for closed gesture strokes.

---
 src/client/documents/Documents.ts                                      | 1 +
 src/client/views/InkingStroke.tsx                                      | 2 +-
 .../views/collections/collectionFreeForm/CollectionFreeFormView.tsx    | 3 ++-
 3 files changed, 4 insertions(+), 2 deletions(-)

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

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 1d7c73306..37db23bdc 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -317,6 +317,7 @@ export class DocumentOptions {
     _label_minFontSize?: NUMt = new NumInfo('minimum font size for labelBoxes', false);
     _label_maxFontSize?: NUMt = new NumInfo('maximum font size for labelBoxes', false);
     stroke_width?: NUMt = new NumInfo('width of an ink stroke', false);
+    stroke_showLabel?: BOOLt = new BoolInfo('show label inside of stroke');
     mediaState?: STRt = new StrInfo(`status of audio/video media document: ${media_state.PendingRecording}, ${media_state.Recording}, ${media_state.Paused}, ${media_state.Playing}`, false);
     recording?: BOOLt = new BoolInfo('whether WebCam is recording or not');
     slides?: DOCt = new DocInfo('presentation slide associated with video recording (bcz: should be renamed!!)');
diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx
index 122e5c4c3..ce70b5052 100644
--- a/src/client/views/InkingStroke.tsx
+++ b/src/client/views/InkingStroke.tsx
@@ -446,7 +446,7 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() impleme
                     {clickableLine(this.onPointerDown, isInkMask)}
                     {isInkMask ? null : inkLine}
                 </svg>
-                {!closed || (!RTFCast(this.dataDoc.text)?.Text && (!this._props.isSelected() || Doc.UserDoc().activeInkHideTextLabels)) ? null : (
+                {!closed || (!RTFCast(this.dataDoc.text)?.Text && !this.dataDoc[this.fieldKey + '_showLabel'] && (!this._props.isSelected() || Doc.UserDoc().activeInkHideTextLabels)) ? null : (
                     <div
                         className="inkStroke-text"
                         style={{
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index b2fb5848e..01d379b96 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -647,7 +647,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                       x: B.x - inkWidth / 2,
                       y: B.y - inkWidth / 2,
                       _width: B.width + inkWidth,
-                      _height: B.height + inkWidth }, // prettier-ignore
+                      _height: B.height + inkWidth,
+                      stroke_showLabel: true}, // prettier-ignore
                     inkWidth
                 );
                 if (Doc.ActiveTool === InkTool.Write) {
-- 
cgit v1.2.3-70-g09d2


From b949608ff69fb66c30bbed439b1c37f8fffd2333 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Sat, 23 Mar 2024 21:13:38 -0400
Subject: fixed linking to parts of a template.  fixed highlting part of a
 template when following a link to it.

---
 .../collectionFreeForm/CollectionFreeFormView.tsx            | 12 +++++++++---
 src/fields/Doc.ts                                            |  3 ++-
 2 files changed, 11 insertions(+), 4 deletions(-)

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

diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 01d379b96..1fd453e96 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -314,7 +314,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
             //     options.didMove = true;
             // }
         }
-        if (anchor.type !== DocumentType.CONFIG && !DocListCast(this.Document[this.fieldKey ?? Doc.LayoutFieldKey(this.Document)]).includes(anchor)) return;
+        if (anchor.type !== DocumentType.CONFIG && !DocListCast(this.Document[this.fieldKey ?? Doc.LayoutFieldKey(this.Document)]).includes(anchor) && !this.childLayoutPairs.map(pair => pair.layout).includes(anchor)) return;
         const xfToCollection = options?.docTransform ?? Transform.Identity();
         const savedState = { panX: NumCast(this.Document[this.panXFieldKey]), panY: NumCast(this.Document[this.panYFieldKey]), scale: options?.willZoomCentered ? this.Document[this.scaleFieldKey] : undefined };
         const cantTransform = this.fitContentsToBox || ((this.Document.isGroup || this.layoutDoc._lockedTransform) && !LightboxView.LightboxDoc);
@@ -335,6 +335,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     getView = async (doc: Doc, options: FocusViewOptions): Promise<Opt<DocumentView>> =>
         new Promise<Opt<DocumentView>>(res => {
             if (doc.hidden && this._lightboxDoc !== doc) options.didMove = !(doc.hidden = false);
+            if (doc === this.Document) return res(this.DocumentView?.());
             const findDoc = (finish: (dv: DocumentView) => void) => DocumentManager.Instance.AddViewRenderedCb(doc, dv => finish(dv));
             findDoc(dv => res(dv));
         });
@@ -1262,7 +1263,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                     this._lightboxDoc = doc;
                     return true;
                 }
-                if (this.childDocList?.includes(doc)) {
+                if (doc === this.Document || this.childDocList?.includes(doc) || this.childLayoutPairs.map(pair => pair.layout)?.includes(doc)) {
                     if (doc.hidden) doc.hidden = false;
                     return true;
                 }
@@ -1405,7 +1406,12 @@ 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.ConfigDocument({ title: 'ViewSpec - ' + StrCast(this.layoutDoc._type_collection), layout_unrendered: true, presentation_transition: 500, annotationOn: this.Document });
+        const anchor = Docs.Create.ConfigDocument({
+            title: 'ViewSpec - ' + StrCast(this.layoutDoc._type_collection),
+            layout_unrendered: true,
+            presentation_transition: 500,
+            annotationOn: this.Document,
+        });
         PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), pannable: !this.Document.isGroup, type_collection: true, filters: true } }, this.Document);
 
         if (addAsAnnotation) {
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index 4b40d11b9..246828709 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -1279,7 +1279,8 @@ export namespace Doc {
             highlightedDocs.add(doc);
             doc[Highlight] = true;
             doc[Animation] = presentation_effect;
-            if (dataAndDisplayDocs) {
+            if (dataAndDisplayDocs && !doc.resolvedDataDoc) {
+                // if doc is a layout template then we don't want to highlight the proto since that will be the entire template, not just the specific layout field
                 highlightedDocs.add(doc[DocData]);
                 doc[DocData][Highlight] = true;
             }
-- 
cgit v1.2.3-70-g09d2


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

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

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

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


From ad1b41beaa0c55487d41381bddd80ab1a76e7fb5 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Sat, 30 Mar 2024 00:36:19 -0400
Subject: added Labels button for turning ink labels on/off.  added Pixels and
 Rotate buttons for images.  enabled ink as template for text : check for ink
 stroke data on layout doc if it's not on data doc & started to have style
 provider check for properties on layout doc instead of data (background
 color, boxShadow).   fixed toggling key/value on and off when node is
 selected.

---
 src/client/documents/Documents.ts                  |  2 ++
 src/client/util/CurrentUserUtils.ts                | 16 ++++++++---
 src/client/views/InkControlPtHandles.tsx           |  4 +--
 src/client/views/InkingStroke.tsx                  | 12 ++++++--
 src/client/views/StyleProvider.tsx                 | 17 ++++++------
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  4 +--
 src/client/views/global/globalScripts.ts           | 21 ++++++++++++--
 src/client/views/nodes/DocumentView.scss           |  1 -
 src/client/views/nodes/MapBox/MapBox.tsx           |  3 ++
 .../views/nodes/formattedText/DashFieldView.tsx    | 32 ++++++++++------------
 .../views/nodes/formattedText/FormattedTextBox.tsx |  2 +-
 .../views/nodes/formattedText/RichTextRules.ts     |  3 +-
 src/client/views/nodes/formattedText/nodes_rts.ts  |  1 -
 13 files changed, 74 insertions(+), 44 deletions(-)

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

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 6275de405..6965ae0ce 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -1804,10 +1804,12 @@ export namespace DocUtils {
         const iconViews = DocListCast(Cast(Doc.UserDoc()['template_icons'], Doc, null)?.data);
         const templBtns = DocListCast(Cast(Doc.UserDoc()['template_buttons'], Doc, null)?.data);
         const noteTypes = DocListCast(Cast(Doc.UserDoc()['template_notes'], Doc, null)?.data);
+        const userTypes = DocListCast(Cast(Doc.UserDoc()['template_user'], Doc, null)?.data);
         const clickFuncs = DocListCast(Cast(Doc.UserDoc()['template_clickFuncs'], Doc, null)?.data);
         const allTemplates = iconViews
             .concat(templBtns)
             .concat(noteTypes)
+            .concat(userTypes)
             .concat(clickFuncs)
             .map(btnDoc => (btnDoc.dragFactory as Doc) || btnDoc)
             .filter(doc => doc.isTemplateDoc);
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index 90368c1eb..3855bdeb6 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -764,6 +764,7 @@ pie title Minerals in my tap water
             { title: "Square", toolTip: "Square (double tap to lock mode)", btnType: ButtonType.ToggleButton, icon: "square",     toolType:GestureUtils.Gestures.Rectangle,   scripts: {onClick:`{ return setActiveTool(this.toolType, false, _readOnly_);}`, onDoubleClick:`{ return setActiveTool(this.toolType, true, _readOnly_);}`} },
             { title: "Line",   toolTip: "Line (double tap to lock mode)",   btnType: ButtonType.ToggleButton, icon: "minus",      toolType:GestureUtils.Gestures.Line,        scripts: {onClick:`{ return setActiveTool(this.toolType, false, _readOnly_);}`, onDoubleClick:`{ return setActiveTool(this.toolType, true, _readOnly_);}`} },
             { title: "Mask",   toolTip: "Mask",                             btnType: ButtonType.ToggleButton, icon: "user-circle",toolType: "inkMask",                        scripts: {onClick:'{ return setInkProperty(this.toolType, value, _readOnly_);}'}, funcs: {hidden:"IsNoviceMode()" } },
+            { title: "Labels", toolTip: "Lab els",                          btnType: ButtonType.ToggleButton, icon: "text-width", toolType: "labels",                         scripts: {onClick:'{ return setInkProperty(this.toolType, value, _readOnly_);}'}, },
             { title: "Width",  toolTip: "Stroke width",                     btnType: ButtonType.NumberSliderButton,               toolType: "strokeWidth", ignoreClick: true, scripts: {script: '{ return setInkProperty(this.toolType, value, _readOnly_);}'}, numBtnMin: 1},
             { title: "Ink",    toolTip: "Stroke color",                     btnType: ButtonType.ColorButton,  icon: "pen",        toolType: "strokeColor", ignoreClick: true, scripts: {script: '{ return setInkProperty(this.toolType, value, _readOnly_);}'} },
         ];
@@ -772,7 +773,7 @@ pie title Minerals in my tap water
     static schemaTools():Button[] {
         return [
             {title: "Preview", toolTip: "Show selection preview", btnType: ButtonType.ToggleButton, icon: "portrait", scripts:{ onClick: '{ return toggleSchemaPreview(_readOnly_); }'} },
-            {title: "1 Line",  toolTip: "Single Line Rows",       btnType: ButtonType.ToggleButton, icon: "eye", scripts:{ onClick: '{ return toggleSingleLineSchema(_readOnly_); }'} },
+            {title: "1 Line",  toolTip: "Single Line Rows",       btnType: ButtonType.ToggleButton, icon: "eye",      scripts:{ onClick: '{ return toggleSingleLineSchema(_readOnly_); }'} },
             {title: "DataViz", toolTip: "Turn Schema Table into Data Visualization Doc",       btnType: ButtonType.ClickButton, icon: "chart-bar", scripts:{ onClick: '{ datavizFromSchema()'} }, ];
     }
 
@@ -780,12 +781,18 @@ pie title Minerals in my tap water
         return [
             { title: "Back",    toolTip: "Go back",         btnType: ButtonType.ClickButton,  icon: "arrow-left",              scripts: { onClick: '{ return webBack(); }' }},
             { title: "Forward", toolTip: "Go forward",      btnType: ButtonType.ClickButton,  icon: "arrow-right",             scripts: { onClick: '{ return webForward(); }'}},
-            { title: "URL",     toolTip: "URL", width: 250, btnType: ButtonType.EditableText, icon: "lock", ignoreClick: true, scripts: { script: '{ return webSetURL(value, _readOnly_); }'} },
+            { title: "URL",     toolTip: "URL", width: 250, btnType: ButtonType.EditableText, icon: "lock", ignoreClick: true, scripts: { script:  '{ return webSetURL(value, _readOnly_); }'} },
         ];
     }
     static videoTools() {
         return [
-            { title: "Snapshot",toolTip: "Take snapshot of current frame", btnType: ButtonType.ClickButton,  icon: "camera",                  scripts: { onClick: '{ return videoSnapshot(); }' }},
+            { title: "Snapshot",toolTip: "Take snapshot of current frame", btnType: ButtonType.ClickButton,  icon: "camera",   scripts: { onClick: '{ return videoSnapshot(); }' }},
+        ];
+    }
+    static imageTools() {
+        return [
+            { title: "Pixels",toolTip: "Set Native Pixel Sizze", btnType: ButtonType.ClickButton,  icon: "portrait",  scripts: { onClick: 'imageSetPixelSize();' }},
+            { title: "Rotate",toolTip: "Rotate 90",              btnType: ButtonType.ClickButton,  icon: "redo-alt",  scripts: { onClick: 'imageRotate90();' }},
         ];
     }
     static contextMenuTools():Button[] {
@@ -810,7 +817,8 @@ pie title Minerals in my tap water
             { title: "View",    icon: "View",          toolTip: "View tools",           subMenu: CurrentUserUtils.viewTools(),   expertMode: false, toolType:CollectionViewType.Freeform, funcs: {hidden: `!SelectionManager_selectedDocType(this.toolType, this.expertMode)`, linearView_IsOpen: `SelectionManager_selectedDocType(this.toolType, this.expertMode)`} }, // Always available
             { title: "Stack",   icon: "View",          toolTip: "Stacking tools",       subMenu: CurrentUserUtils.stackTools(),  expertMode: false, toolType:CollectionViewType.Stacking, funcs: {hidden: `!SelectionManager_selectedDocType(this.toolType, this.expertMode)`, linearView_IsOpen: `SelectionManager_selectedDocType(this.toolType, this.expertMode)`} }, // Always available
             { title: "Web",     icon: "Web",           toolTip: "Web functions",        subMenu: CurrentUserUtils.webTools(),    expertMode: false, toolType:DocumentType.WEB,            funcs: {hidden: `!SelectionManager_selectedDocType(this.toolType, this.expertMode)`, linearView_IsOpen: `SelectionManager_selectedDocType(this.toolType, this.expertMode)`} }, // Only when Web is selected
-            { title: "Video",   icon: "Video",         toolTip: "Video functions",      subMenu: CurrentUserUtils.videoTools(),  expertMode: false, toolType:DocumentType.VID,            funcs: {hidden: `!SelectionManager_selectedDocType(this.toolType, this.expertMode)`, linearView_IsOpen: `SelectionManager_selectedDocType(this.toolType, this.expertMode)`} }, // Only when Web is selected
+            { title: "Video",   icon: "Video",         toolTip: "Video functions",      subMenu: CurrentUserUtils.videoTools(),  expertMode: false, toolType:DocumentType.VID,            funcs: {hidden: `!SelectionManager_selectedDocType(this.toolType, this.expertMode)`, linearView_IsOpen: `SelectionManager_selectedDocType(this.toolType, this.expertMode)`} }, // Only when video is selected
+            { title: "Image",   icon: "Image",         toolTip: "Image functions",      subMenu: CurrentUserUtils.imageTools(),  expertMode: false, toolType:DocumentType.IMG,            funcs: {hidden: `!SelectionManager_selectedDocType(this.toolType, this.expertMode)`, linearView_IsOpen: `SelectionManager_selectedDocType(this.toolType, this.expertMode)`} }, // Only when image is selected
             { title: "Schema",  icon: "Schema",linearBtnWidth:58,toolTip: "Schema functions",subMenu: CurrentUserUtils.schemaTools(),expertMode: false,toolType:CollectionViewType.Schema,funcs: {hidden: `!SelectionManager_selectedDocType(this.toolType, this.expertMode)`, linearView_IsOpen: `SelectionManager_selectedDocType(this.toolType, this.expertMode)`} }, // Only when Schema is selected
            ];
     }
diff --git a/src/client/views/InkControlPtHandles.tsx b/src/client/views/InkControlPtHandles.tsx
index 01d52135a..edc6b404b 100644
--- a/src/client/views/InkControlPtHandles.tsx
+++ b/src/client/views/InkControlPtHandles.tsx
@@ -239,8 +239,8 @@ export class InkEndPtHandles extends ObservableReactComponent<InkEndProps> {
         const hdl = (key: string, pt: PointData, dragFunc: (e: React.PointerEvent) => void) => (
             <circle
                 key={key}
-                cx={pt.X}
-                cy={pt.Y}
+                cx={pt?.X}
+                cy={pt?.Y}
                 r={this._props.screenSpaceLineWidth * 2}
                 fill={this._overStart ? '#aaaaaa' : '#99999977'}
                 stroke={'#00007777'}
diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx
index 345309b4a..5a9f10ba4 100644
--- a/src/client/views/InkingStroke.tsx
+++ b/src/client/views/InkingStroke.tsx
@@ -217,7 +217,7 @@ export class InkingStroke extends ViewBoxAnnotatableComponent<FieldViewProps>()
      * factor for converting between ink and screen space.
      */
     inkScaledData = () => {
-        const inkData = Cast(this.dataDoc[this.fieldKey], InkField)?.inkData ?? [];
+        const inkData = Cast(this.dataDoc[this.fieldKey], InkField, Cast(this.layoutDoc[this.fieldKey], InkField, null))?.inkData ?? [];
         const inkStrokeWidth = NumCast(this.layoutDoc.stroke_width, 1);
         const inkTop = Math.min(...inkData.map(p => p.Y)) - inkStrokeWidth / 2;
         const inkBottom = Math.max(...inkData.map(p => p.Y)) + inkStrokeWidth / 2;
@@ -446,7 +446,7 @@ export class InkingStroke extends ViewBoxAnnotatableComponent<FieldViewProps>()
                     {clickableLine(this.onPointerDown, isInkMask)}
                     {isInkMask ? null : inkLine}
                 </svg>
-                {!closed || (!RTFCast(this.dataDoc.text)?.Text && !this.dataDoc[this.fieldKey + '_showLabel'] && (!this._props.isSelected() || Doc.UserDoc().activeInkHideTextLabels)) ? null : (
+                {!closed || this.dataDoc[this.fieldKey + '_showLabel'] === false || (!RTFCast(this.dataDoc.text)?.Text && !this.dataDoc[this.fieldKey + '_showLabel'] && (!this._props.isSelected() || Doc.UserDoc().activeInkHideTextLabels)) ? null : (
                     <div
                         className="inkStroke-text"
                         style={{
@@ -464,7 +464,7 @@ export class InkingStroke extends ViewBoxAnnotatableComponent<FieldViewProps>()
                             yPadding={10}
                             xPadding={10}
                             fieldKey="text"
-                            dontRegisterView={true}
+                            //dontRegisterView={true}
                             noSidebar={true}
                             dontScale={true}
                             isContentActive={this._props.isContentActive}
@@ -488,6 +488,9 @@ export function SetActiveInkColor(value: string) {
 export function SetActiveIsInkMask(value: boolean) {
     ActiveInkPen() && (ActiveInkPen().activeIsInkMask = value);
 }
+export function SetActiveInkHideTextLabels(value: boolean) {
+    ActiveInkPen() && (ActiveInkPen().activeInkHideTextLabels = value);
+}
 export function SetActiveFillColor(value: string) {
     ActiveInkPen() && (ActiveInkPen().activeFillColor = value);
 }
@@ -515,6 +518,9 @@ export function ActiveFillColor(): string {
 export function ActiveIsInkMask(): boolean {
     return BoolCast(ActiveInkPen()?.activeIsInkMask, false);
 }
+export function ActiveInkHideTextLabels(): boolean {
+    return BoolCast(ActiveInkPen().activeInkHideTextLabels, false);
+}
 export function ActiveArrowStart(): string {
     return StrCast(ActiveInkPen()?.activeArrowStart, '');
 }
diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx
index bf6f2a4fa..8073d0842 100644
--- a/src/client/views/StyleProvider.tsx
+++ b/src/client/views/StyleProvider.tsx
@@ -89,7 +89,8 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps &
     const isNonTransparent = property.includes(':nonTransparent');
     const isNonTransparentLevel = isNonTransparent ? Number(property.replace(/.*:nonTransparent([0-9]+).*/, '$1')) : 0; // property.includes(':nonTransparent');
     const isAnnotated = property.includes(':annotated');
-    const isInk = () => doc?._layout_isSvg && !props?.LayoutTemplateString;
+    const layoutDoc = doc ? Doc.Layout(doc) : doc;
+    const isInk = () => layoutDoc?._layout_isSvg && !props?.LayoutTemplateString;
     const isOpen = property.includes(':open');
     const isEmpty = property.includes(':empty');
     const boxBackground = property.includes(':box');
@@ -129,7 +130,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps &
                         highlightStyle: doc.isGroup ? "dotted": highlightStyle,
                         highlightColor,
                         highlightIndex,
-                        highlightStroke: doc.layout_isSvg,
+                        highlightStroke: layoutDoc?.layout_isSvg,
                     };
                 }
             }
@@ -203,7 +204,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps &
             let docColor: Opt<string> = StrCast(doc?.[fieldKey + 'backgroundColor'], StrCast(doc?.backgroundColor, isCaption ? 'rgba(0,0,0,0.4)' : ''));
             if (doc?.[StrCast(doc?.layout_fieldKey)] instanceof Doc) docColor = StrCast(doc._backgroundColor,docColor)
             // prettier-ignore
-            switch (doc?.type) {
+            switch (layoutDoc?.type) {
                 case DocumentType.PRESELEMENT: docColor = docColor || ""; break;
                 case DocumentType.PRES:        docColor = docColor || 'transparent'; break;
                 case DocumentType.FONTICON:    docColor = boxBackground ? undefined : docColor || Colors.DARK_GRAY; break;
@@ -220,13 +221,13 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps &
                 case DocumentType.SCREENSHOT:
                 case DocumentType.VID:         docColor = docColor || (Colors.LIGHT_GRAY); break;
                 case DocumentType.COL:
-                    docColor = docColor || (Doc.IsSystem(doc)
+                    docColor = docColor || (doc && Doc.IsSystem(doc)
                             ? SettingsManager.userBackgroundColor
-                            : doc.annotationOn
+                            : doc?.annotationOn
                             ? '#00000010' // faint interior for collections on PDFs, images, etc
                             : doc?.isGroup
                             ? undefined
-                            : doc._type_collection === CollectionViewType.Stacking ?
+                            : doc?._type_collection === CollectionViewType.Stacking ?
                             (Colors.DARK_GRAY)
                             : Cast((props?.renderDepth || 0) > 0 ? Doc.UserDoc().activeCollectionNestedBackground : Doc.UserDoc().activeCollectionBackground, 'string') ?? (Colors.MEDIUM_GRAY));
                     break;
@@ -241,7 +242,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps &
         case StyleProp.BoxShadow: {
             if (!doc || opacity() === 0 || doc.noShadow) return undefined; // if it's not visible, then no shadow)
             if (doc.layout_boxShadow === 'standard') return Shadows.STANDARD_SHADOW;
-            if (IsFollowLinkScript(doc?.onClick) && LinkManager.Links(doc).length && !doc.layout_isSvg) return StrCast(doc?._linkButtonShadow, 'lightblue 0em 0em 1em');
+            if (IsFollowLinkScript(doc?.onClick) && LinkManager.Links(doc).length && !layoutDoc?.layout_isSvg) return StrCast(doc?._linkButtonShadow, 'lightblue 0em 0em 1em');
             switch (doc?.type) {
                 case DocumentType.COL:
                     return StrCast(
@@ -266,7 +267,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps &
                         ? undefined // if it's a background & has a cluster color, make the shadow spread really big
                         : fieldKey.includes('_inline')  // if doc is an inline document in a text box
                         ? `${Colors.DARK_GRAY} ${StrCast(doc.layout_boxShadow, '0vw 0vw 0.1vw')}`
-                        : DocCast(doc.embedContainer)?.type=== DocumentType.RTF // if doc is embedded in a text document (but not an inline)
+                        : DocCast(doc.embedContainer)?.type === DocumentType.RTF && !isInk() // if doc is embedded in a text document (but not an inline)
                         ? `${Colors.DARK_GRAY} ${StrCast(doc.layout_boxShadow, '0.2vw 0.2vw 0.8vw')}`
                         : StrCast(doc.layout_boxShadow, '');
             }
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 5495735ea..c53bad509 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -649,7 +649,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                       y: B.y - inkWidth / 2,
                       _width: B.width + inkWidth,
                       _height: B.height + inkWidth,
-                      stroke_showLabel: true}, // prettier-ignore
+                      stroke_showLabel: BoolCast(Doc.UserDoc().activeInkHideTextLabels)}, // prettier-ignore
                     inkWidth
                 );
                 if (Doc.ActiveTool === InkTool.Write) {
@@ -1721,7 +1721,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                 horizLines.push(topLeftInScreen[1], topLeftInScreen[1] + docSize[1] / 2, topLeftInScreen[1] + docSize[1]); // horiz center line
                 vertLines.push(topLeftInScreen[0], topLeftInScreen[0] + docSize[0] / 2, topLeftInScreen[0] + docSize[0]); // right line
             });
-        SnappingManager.addSnapLines(horizLines, vertLines);
+        this.layoutDoc._freeform_snapLines && SnappingManager.addSnapLines(horizLines, vertLines);
     };
 
     incrementalRendering = () => this.childDocs.filter(doc => !this._renderCutoffData.get(doc[Id])).length !== 0;
diff --git a/src/client/views/global/globalScripts.ts b/src/client/views/global/globalScripts.ts
index d785c155b..3ea7bfbcd 100644
--- a/src/client/views/global/globalScripts.ts
+++ b/src/client/views/global/globalScripts.ts
@@ -12,7 +12,7 @@ import { ScriptingGlobals } from '../../util/ScriptingGlobals';
 import { SelectionManager } from '../../util/SelectionManager';
 import { UndoManager, undoable } from '../../util/UndoManager';
 import { GestureOverlay } from '../GestureOverlay';
-import { ActiveFillColor, ActiveInkColor, ActiveInkWidth, ActiveIsInkMask, InkingStroke, SetActiveFillColor, SetActiveInkColor, SetActiveInkWidth, SetActiveIsInkMask } from '../InkingStroke';
+import { ActiveFillColor, ActiveInkColor, ActiveInkHideTextLabels, ActiveInkWidth, ActiveIsInkMask, InkingStroke, SetActiveFillColor, SetActiveInkColor, SetActiveInkHideTextLabels, SetActiveInkWidth, SetActiveIsInkMask } from '../InkingStroke';
 import { CollectionFreeFormView } from '../collections/collectionFreeForm';
 // import { InkTranscription } from '../InkTranscription';
 import { DocData } from '../../../fields/DocSymbols';
@@ -21,6 +21,7 @@ import { DocumentView } from '../nodes/DocumentView';
 import { VideoBox } from '../nodes/VideoBox';
 import { WebBox } from '../nodes/WebBox';
 import { RichTextMenu } from '../nodes/formattedText/RichTextMenu';
+import { ImageBox } from '../nodes/ImageBox';
 
 ScriptingGlobals.add(function IsNoneSelected() {
     return SelectionManager.Views.length <= 0;
@@ -336,15 +337,20 @@ function setActiveTool(tool: InkTool | GestureUtils.Gestures, keepPrim: boolean,
 ScriptingGlobals.add(setActiveTool, 'sets the active ink tool mode');
 
 // toggle: Set overlay status of selected document
-ScriptingGlobals.add(function setInkProperty(option: 'inkMask' | 'fillColor' | 'strokeWidth' | 'strokeColor', value: any, checkResult?: boolean) {
+ScriptingGlobals.add(function setInkProperty(option: 'inkMask' | 'labels' | 'fillColor' | 'strokeWidth' | 'strokeColor', value: any, checkResult?: boolean) {
     const selected = SelectionManager.Docs.lastElement() ?? Doc.UserDoc();
     // prettier-ignore
-    const map: Map<'inkMask' | 'fillColor' | 'strokeWidth' | 'strokeColor', { checkResult: () => any; setInk: (doc: Doc) => void; setMode: () => void }> = new Map([
+    const map: Map<'inkMask' | 'labels' | 'fillColor' | 'strokeWidth' | 'strokeColor', { checkResult: () => any; setInk: (doc: Doc) => void; setMode: () => void }> = new Map([
         ['inkMask', {
             checkResult: () => ((selected?._layout_isSvg ? BoolCast(selected[DocData].stroke_isInkMask) : ActiveIsInkMask())),
             setInk: (doc: Doc) => (doc[DocData].stroke_isInkMask = !doc.stroke_isInkMask),
             setMode: () => selected?.type !== DocumentType.INK && SetActiveIsInkMask(!ActiveIsInkMask()),
         }],
+        ['labels', {
+            checkResult: () => ((selected?._stroke_showLabel ? BoolCast(selected[DocData].stroke_showLabel) : ActiveInkHideTextLabels())),
+            setInk: (doc: Doc) => (doc[DocData].stroke_showLabel = !doc.stroke_showLabel),
+            setMode: () => selected?.type !== DocumentType.INK && SetActiveInkHideTextLabels(!ActiveInkHideTextLabels()),
+        }],
         ['fillColor', {
             checkResult: () => (selected?._layout_isSvg ? StrCast(selected[DocData].fillColor) : ActiveFillColor() ?? "transparent"),
             setInk: (doc: Doc) => (doc[DocData].fillColor = StrCast(value)),
@@ -399,6 +405,15 @@ ScriptingGlobals.add(function videoSnapshot() {
     selected?.Snapshot();
 });
 
+ScriptingGlobals.add(function imageSetPixelSize() {
+    const selected = SelectionManager.Views.lastElement()?.ComponentView as ImageBox;
+    selected?.setNativeSize();
+});
+ScriptingGlobals.add(function imageRotate90() {
+    const selected = SelectionManager.Views.lastElement()?.ComponentView as ImageBox;
+    selected?.rotate();
+});
+
 /** Schema
  *      toggleSchemaPreview
  **/
diff --git a/src/client/views/nodes/DocumentView.scss b/src/client/views/nodes/DocumentView.scss
index 5421c1b50..4fea6b3c6 100644
--- a/src/client/views/nodes/DocumentView.scss
+++ b/src/client/views/nodes/DocumentView.scss
@@ -256,6 +256,5 @@
 
     .documentView-node:first-child {
         position: relative;
-        background: '#B59B66'; //$white;
     }
 }
diff --git a/src/client/views/nodes/MapBox/MapBox.tsx b/src/client/views/nodes/MapBox/MapBox.tsx
index 36dad2747..b73898f59 100644
--- a/src/client/views/nodes/MapBox/MapBox.tsx
+++ b/src/client/views/nodes/MapBox/MapBox.tsx
@@ -1161,6 +1161,9 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
                         <div className="mapBox-searchbar" style={{ width: `${100 / scale}%`, zIndex: 1, position: 'relative', background: 'lightGray' }}>
                             <TextField ref={this._textRef} fullWidth placeholder="Enter a location" onKeyDown={this.searchbarKeyDown} onChange={(e: any) => this.handleSearchChange(e.target.value)} />
                             <IconButton icon={<FontAwesomeIcon icon={faGear as IconLookup} size="1x" />} type={Type.TERT} onClick={e => this.toggleSettings()} />
+                            <div style={{ opacity: 0 }}>
+                                <IconButton icon={<FontAwesomeIcon icon={faGear as IconLookup} size="1x" />} type={Type.TERT} onClick={e => this.toggleSettings()} />
+                            </div>
                         </div>
                     )}
                     {this._settingsOpen && !this._routeToAnimate && (
diff --git a/src/client/views/nodes/formattedText/DashFieldView.tsx b/src/client/views/nodes/formattedText/DashFieldView.tsx
index cdfeebe66..f6ca97ee9 100644
--- a/src/client/views/nodes/formattedText/DashFieldView.tsx
+++ b/src/client/views/nodes/formattedText/DashFieldView.tsx
@@ -29,8 +29,8 @@ export class DashFieldView {
     root: any;
     node: any;
     tbox: FormattedTextBox;
-    @observable expanded = false;
-    Expanded = () => this.expanded;
+    @observable _nodeSelected = false;
+    NodeSelected = () => this._nodeSelected;
 
     unclickable = () => !this.tbox._props.rootSelected?.() && this.node.marks.some((m: any) => m.type === this.tbox.EditorView?.state.schema.marks.linkAnchor && m.attrs.noPreview);
     constructor(node: any, view: any, getPos: any, tbox: FormattedTextBox) {
@@ -71,8 +71,6 @@ export class DashFieldView {
             e.stopPropagation();
         };
 
-        this.expanded = node.attrs.expanded;
-
         this.root = ReactDOM.createRoot(this.dom);
         this.root.render(
             <DashFieldViewInternal
@@ -86,7 +84,7 @@ export class DashFieldView {
                 hideKey={node.attrs.hideKey}
                 hideValue={node.attrs.hideValue}
                 editable={node.attrs.editable}
-                expanded={this.Expanded}
+                nodeSelected={this.NodeSelected}
                 dataDoc={node.attrs.dataDoc}
                 tbox={tbox}
             />
@@ -100,11 +98,11 @@ export class DashFieldView {
         });
     }
     deselectNode() {
-        runInAction(() => (this.expanded = false));
+        runInAction(() => (this._nodeSelected = false));
         this.dom.classList.remove('ProseMirror-selectednode');
     }
     selectNode() {
-        setTimeout(() => runInAction(() => (this.expanded = true)), 100);
+        setTimeout(() => runInAction(() => (this._nodeSelected = true)), 100);
         this.dom.classList.add('ProseMirror-selectednode');
     }
 }
@@ -118,7 +116,7 @@ interface IDashFieldViewInternal {
     width: number;
     height: number;
     editable: boolean;
-    expanded: () => boolean;
+    nodeSelected: () => boolean;
     dataDoc: boolean;
     node: any;
     getPos: any;
@@ -132,7 +130,7 @@ export class DashFieldViewInternal extends ObservableReactComponent<IDashFieldVi
     _fieldKey: string;
     _fieldRef = React.createRef<HTMLDivElement>();
     @observable _dashDoc: Doc | undefined = undefined;
-    @observable _expanded = this._props.expanded();
+    @observable _expanded = this._props.nodeSelected();
 
     constructor(props: IDashFieldViewInternal) {
         super(props);
@@ -158,7 +156,7 @@ export class DashFieldViewInternal extends ObservableReactComponent<IDashFieldVi
     componentWillUnmount() {
         this._reactionDisposer?.();
     }
-    isRowActive = () => (this._props.expanded() || this._expanded) && this._props.editable;
+    isRowActive = () => (this._props.nodeSelected() || this._expanded) && this._props.editable;
 
     finishEdit = action(() => {
         if (this._expanded) {
@@ -181,7 +179,7 @@ export class DashFieldViewInternal extends ObservableReactComponent<IDashFieldVi
                     deselectCell={emptyFunction}
                     selectCell={emptyFunction}
                     maxWidth={this._props.hideKey || this._hideKey ? undefined : this._props.tbox._props.PanelWidth}
-                    columnWidth={this._expanded || this._props.expanded() ? this.columnWidth : returnZero}
+                    columnWidth={this._expanded || this._props.nodeSelected() ? this.columnWidth : returnZero}
                     selectedCell={this.selectedCell}
                     fieldKey={this._fieldKey}
                     rowHeight={returnZero}
@@ -190,7 +188,7 @@ export class DashFieldViewInternal extends ObservableReactComponent<IDashFieldVi
                     getFinfo={emptyFunction}
                     setColumnValues={returnFalse}
                     allowCRs={true}
-                    oneLine={!this._expanded && !this._props.expanded()}
+                    oneLine={!this._expanded && !this._props.nodeSelected()}
                     finishEdit={this.finishEdit}
                     transform={Transform.Identity}
                     menuTarget={null}
@@ -221,7 +219,7 @@ export class DashFieldViewInternal extends ObservableReactComponent<IDashFieldVi
     toggleFieldHide = undoable(
         action(() => {
             const editor = this._props.tbox.EditorView!;
-            editor.dispatch(editor.state.tr.setNodeMarkup(this._props.getPos(), this._props.node.type, { ...this._props.node.attrs, hideKey: !this._props.node.attrs.hideKey ? true : false }));
+            editor.dispatch(editor.state.tr.setNodeMarkup(this._props.getPos(), this._props.node.type, { ...this._props.node.attrs, hideKey: this._props.node.attrs.hideValue ? false : !this._props.node.attrs.hideKey ? true : false }));
         }),
         'hideKey'
     );
@@ -229,18 +227,17 @@ export class DashFieldViewInternal extends ObservableReactComponent<IDashFieldVi
     toggleValueHide = undoable(
         action(() => {
             const editor = this._props.tbox.EditorView!;
-            editor.dispatch(editor.state.tr.setNodeMarkup(this._props.getPos(), this._props.node.type, { ...this._props.node.attrs, hideValue: !this._props.node.attrs.hideValue ? true : false }));
+            editor.dispatch(editor.state.tr.setNodeMarkup(this._props.getPos(), this._props.node.type, { ...this._props.node.attrs, hideValue: this._props.node.attrs.hideKey ? false : !this._props.node.attrs.hideValue ? true : false }));
         }),
         'hideValue'
     );
 
-    @observable _showValue = false;
     @computed get _hideKey() {
         return this._props.hideKey && !this._expanded;
     }
 
     @computed get _hideValue() {
-        return !this._showValue && this._props.hideValue && !this._expanded;
+        return this._props.hideValue && !this._props.nodeSelected();
     }
 
     // clicking on the label creates a pivot view collection of all documents
@@ -251,7 +248,6 @@ export class DashFieldViewInternal extends ObservableReactComponent<IDashFieldVi
             DashFieldViewMenu.toggleFieldHide = this.toggleFieldHide;
             DashFieldViewMenu.toggleValueHide = this.toggleValueHide;
             DashFieldViewMenu.Instance.show(e.clientX, e.clientY + 16, this._fieldKey);
-            this._dashDoc?.[this._fieldKey + '_hideValue'] && runInAction(() => (this._showValue = !this._showValue));
             const editor = this._props.tbox.EditorView!;
             setTimeout(() => editor.dispatch(editor.state.tr.setSelection(new NodeSelection(editor.state.doc.resolve(this._props.getPos())))), 100);
         });
@@ -263,7 +259,7 @@ export class DashFieldViewInternal extends ObservableReactComponent<IDashFieldVi
     };
 
     @computed get values() {
-        if (this._props.expanded()) return [];
+        if (this._props.nodeSelected()) return [];
         const vals = FilterPanel.gatherFieldValues(DocListCast(Doc.ActiveDashboard?.data), this._fieldKey, []);
 
         return vals.strings.map(facet => ({ value: facet, label: facet }));
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index a0de822ac..3b583771a 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -1366,7 +1366,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
             DocServer.GetRefField(pdfAnchorId).then(pdfAnchor => {
                 if (pdfAnchor instanceof Doc) {
                     const dashField = view.state.schema.nodes.paragraph.create({}, [
-                        view.state.schema.nodes.dashField.create({ fieldKey: 'text', docId: pdfAnchor[Id], hideKey: true, hideValue: false, editable: false, expanded: true }, undefined, [
+                        view.state.schema.nodes.dashField.create({ fieldKey: 'text', docId: pdfAnchor[Id], hideKey: true, hideValue: false, editable: false }, undefined, [
                             view.state.schema.marks.linkAnchor.create({
                                 allAnchors: [{ href: `/doc/${this.Document[Id]}`, title: this.Document.title, anchorId: `${this.Document[Id]}` }],
                                 title: StrCast(pdfAnchor.title),
diff --git a/src/client/views/nodes/formattedText/RichTextRules.ts b/src/client/views/nodes/formattedText/RichTextRules.ts
index 682d4f96f..483035e86 100644
--- a/src/client/views/nodes/formattedText/RichTextRules.ts
+++ b/src/client/views/nodes/formattedText/RichTextRules.ts
@@ -250,10 +250,11 @@ export class RichTextRules {
 
                 if (
                     DocListCast((Doc.UserDoc().template_notes as Doc).data)
+                        .concat(DocListCast((Doc.UserDoc().template_user as Doc).data))
                         .map(d => StrCast(d.title))
                         .includes(color)
                 ) {
-                    setTimeout(() => this.TextBox.DocumentView?.().switchViews(true, color));
+                    setTimeout(() => this.TextBox.DocumentView?.().switchViews(true, color, undefined, true));
                     return state.tr.deleteRange(start, end);
                 }
                 if (marks) {
diff --git a/src/client/views/nodes/formattedText/nodes_rts.ts b/src/client/views/nodes/formattedText/nodes_rts.ts
index 6862eecef..e335044ea 100644
--- a/src/client/views/nodes/formattedText/nodes_rts.ts
+++ b/src/client/views/nodes/formattedText/nodes_rts.ts
@@ -266,7 +266,6 @@ export const nodes: { [index: string]: NodeSpec } = {
             hideKey: { default: false },
             hideValue: { default: false },
             editable: { default: true },
-            expanded: { default: null },
             dataDoc: { default: false },
         },
         leafText: node => Field.toString((DocServer.GetCachedRefField(node.attrs.docId as string) as Doc)?.[node.attrs.fieldKey as string] as Field),
-- 
cgit v1.2.3-70-g09d2


From a9baa52c557faaa82fd3f696f0d603dd10b982a8 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Sat, 30 Mar 2024 02:18:07 -0400
Subject: updated mermaids template. updated paintFunc to also render the
 underlying code text so that the paintFunc will update if dashFields in the
 code change.  fixed schema views to render templates properly in preview. 
 fixed text to only modify the data doc.  changed setPixels in imageBox to not
 consider scaling of outer containers.

---
 src/client/util/CurrentUserUtils.ts                          |  4 ++--
 src/client/views/StyleProvider.tsx                           |  2 +-
 .../collectionFreeForm/CollectionFreeFormView.tsx            |  6 ++++--
 .../views/collections/collectionSchema/SchemaTableCell.tsx   |  6 +++---
 src/client/views/nodes/ImageBox.tsx                          |  3 +--
 src/client/views/nodes/formattedText/FormattedTextBox.tsx    | 12 ++++++++----
 6 files changed, 19 insertions(+), 14 deletions(-)

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

diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index 3855bdeb6..dbf9641b6 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -321,9 +321,9 @@ export class CurrentUserUtils {
                         {type:"text",text:" "},
                         {type:"text",text:"Minerals in my tap water"},
                         {type:"text",text:"\n    \"Calcium\" : "},
-                        {type:"dashField",attrs:{fieldKey:"calcium",docId:"",hideKey:false,hideValue:false,editable:true}},
+                        {type:"dashField",attrs:{fieldKey:"calcium",docId:"",hideKey:true,hideValue:false,editable:true}},
                         {type:"text",text:"\n    \"Potassium\" : "},
-                        {type:"dashField",attrs:{fieldKey:"pot",docId:"",hideKey:false,hideValue:false,editable:true}},
+                        {type:"dashField",attrs:{fieldKey:"pot",docId:"",hideKey:true,hideValue:false,editable:true}},
                         {type:"text",text:"\n    \"Magnesium\" : 10.01"}
                     ]}
                 ]},
diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx
index 8073d0842..c8b0c0560 100644
--- a/src/client/views/StyleProvider.tsx
+++ b/src/client/views/StyleProvider.tsx
@@ -139,7 +139,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps &
         case StyleProp.WidgetColor:return isAnnotated ? Colors.LIGHT_BLUE : 'dimgrey';
         case StyleProp.Opacity:    return props?.LayoutTemplateString?.includes(KeyValueBox.name) ? 1 : doc?.text_inlineAnnotations ? 0 :  Cast(doc?._opacity, "number", Cast(doc?.opacity, 'number', null));
         case StyleProp.FontSize:   return StrCast(doc?.[fieldKey + 'fontSize'],   StrCast(Doc.UserDoc().fontSize));
-        case StyleProp.FontFamily: return StrCast(doc?.[fieldKey + 'fontFamilyq'], StrCast(Doc.UserDoc().fontFamily));
+        case StyleProp.FontFamily: return StrCast(doc?.[fieldKey + 'fontFamily'], StrCast(Doc.UserDoc().fontFamily));
         case StyleProp.FontWeight: return StrCast(doc?.[fieldKey + 'fontWeight'],  StrCast(Doc.UserDoc().fontWeight));
         case StyleProp.FillColor:  return StrCast(doc?._fillColor, StrCast(doc?.fillColor, StrCast(doc?.backgroundColor, 'transparent')));
         case StyleProp.ShowCaption:return props?.hideCaptions || doc?._type_collection === CollectionViewType.Carousel ? undefined: StrCast(doc?._layout_showCaption);
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index c53bad509..1b562939b 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -76,7 +76,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
 
     @observable _paintedId = 'id' + Utils.GenerateGuid().replace(/-/g, '');
     @computed get paintFunc() {
-        const field = this.layoutDoc[this.fieldKey];
+        const field = this.dataDoc[this.fieldKey];
         const paintFunc = StrCast(Field.toJavascriptString(Cast(field, RichTextField, null)?.Text as Field)).trim();
         return !paintFunc
             ? ''
@@ -1870,7 +1870,9 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                     width: `${100 / (this.nativeDimScaling || 1)}%`,
                     height: this._props.getScrollHeight?.() ?? `${100 / (this.nativeDimScaling || 1)}%`,
                 }}>
-                {this.paintFunc ? null : this._lightboxDoc ? (
+                {this.paintFunc ? (
+                    <FormattedTextBox {...this.props} /> // need this so that any live dashfieldviews will update the underlying text that the code eval reads
+                ) : this._lightboxDoc ? (
                     <div style={{ padding: 15, width: '100%', height: '100%' }}>
                         <DocumentView
                             {...this._props}
diff --git a/src/client/views/collections/collectionSchema/SchemaTableCell.tsx b/src/client/views/collections/collectionSchema/SchemaTableCell.tsx
index ab05a62d3..bf36b2668 100644
--- a/src/client/views/collections/collectionSchema/SchemaTableCell.tsx
+++ b/src/client/views/collections/collectionSchema/SchemaTableCell.tsx
@@ -98,7 +98,7 @@ export class SchemaTableCell extends ObservableReactComponent<SchemaTableCellPro
             focus: emptyFunction,
             addDocTab: SchemaTableCell.addFieldDoc,
             pinToPres: returnZero,
-            Document,
+            Document: DocCast(Document.rootDocument, Document),
             fieldKey: fieldKey,
             PanelWidth: columnWidth,
             PanelHeight: props.rowHeight,
@@ -134,14 +134,14 @@ export class SchemaTableCell extends ObservableReactComponent<SchemaTableCellPro
                     contents={undefined}
                     fieldContents={fieldProps}
                     editing={this.selected ? undefined : false}
-                    GetValue={() => Field.toKeyValueString(this._props.Document, this._props.fieldKey, SnappingManager.MetaKey)}
+                    GetValue={() => Field.toKeyValueString(fieldProps.Document, this._props.fieldKey, SnappingManager.MetaKey)}
                     SetValue={undoable((value: string, shiftDown?: boolean, enterKey?: boolean) => {
                         if (shiftDown && enterKey) {
                             this._props.setColumnValues(this._props.fieldKey.replace(/^_/, ''), value);
                             this._props.finishEdit?.();
                             return true;
                         }
-                        const ret = KeyValueBox.SetField(this._props.Document, this._props.fieldKey.replace(/^_/, ''), value, Doc.IsDataProto(this._props.Document) ? true : undefined);
+                        const ret = KeyValueBox.SetField(fieldProps.Document, this._props.fieldKey.replace(/^_/, ''), value, Doc.IsDataProto(fieldProps.Document) ? true : undefined);
                         this._props.finishEdit?.();
                         return ret;
                     }, 'edit schema cell')}
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 86e8ed60a..469869e21 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -165,8 +165,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl
 
     @undoBatch
     setNativeSize = action(() => {
-        const scaling = (this.DocumentView?.().screenToViewTransform().Scale || 1) / NumCast(this.layoutDoc._freeform_scale, 1);
-        const nscale = NumCast(this._props.PanelWidth()) / scaling;
+        const nscale = NumCast(this._props.PanelWidth()) * NumCast(this.layoutDoc._freeform_scale, 1);
         const nw = nscale / NumCast(this.dataDoc[this.fieldKey + '_nativeWidth']);
         this.dataDoc[this.fieldKey + '_nativeHeight'] = NumCast(this.dataDoc[this.fieldKey + '_nativeHeight']) * nw;
         this.dataDoc[this.fieldKey + '_nativeWidth'] = NumCast(this.dataDoc[this.fieldKey + '_nativeWidth']) * nw;
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 3b583771a..b815342e3 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -326,10 +326,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
 
     leafText = (node: Node) => {
         if (node.type === this._editorView?.state.schema.nodes.dashField) {
-            const refDoc = !node.attrs.docId ? this.Document : (DocServer.GetCachedRefField(node.attrs.docId as string) as Doc);
+            const refDoc = !node.attrs.docId ? DocCast(this.Document.rootDocument, this.Document) : (DocServer.GetCachedRefField(node.attrs.docId as string) as Doc);
             const fieldKey = StrCast(node.attrs.fieldKey);
             return (
-                (node.attrs.hideKey ? '' : '"' + fieldKey + '"' + ':') + //
+                (node.attrs.hideKey ? '' : fieldKey + ':') + //
                 (node.attrs.hideValue ? '' : Field.toJavascriptString(refDoc[fieldKey] as Field))
             );
         }
@@ -346,7 +346,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
     tryUpdateDoc = (force: boolean) => {
         if (this._editorView && (this._editorView as any).docView) {
             const state = this._editorView.state;
-            const dataDoc = Doc.IsDelegateField(DocCast(this.layoutDoc.proto), this.fieldKey) ? DocCast(this.layoutDoc.proto) : this.dataDoc;
+            const dataDoc = this.dataDoc;
             const newText = state.doc.textBetween(0, state.doc.content.size, ' \n', this.leafText);
             const newJson = JSON.stringify(state.toJSON());
             const prevData = Cast(this.layoutDoc[this.fieldKey], RichTextField, null); // the actual text in the text box
@@ -1608,7 +1608,11 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
                 while (target && !target.dataset?.targethrefs) target = target.parentElement;
                 FormattedTextBoxComment.update(this, editor, undefined, target?.dataset?.targethrefs, target?.dataset.linkdoc, target?.dataset.nopreview === 'true');
             } else {
-                editor.dispatch(state.tr.setSelection(new NodeSelection(state.doc.resolve(xpos))));
+                try {
+                    editor.dispatch(state.tr.setSelection(new NodeSelection(state.doc.resolve(xpos))));
+                } catch (e) {
+                    editor.dispatch(state.tr.setSelection(new NodeSelection(state.doc.resolve(xpos - 1))));
+                }
             }
         }
     };
-- 
cgit v1.2.3-70-g09d2


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

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

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

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


From 47ae05389d7ce564efde19b7a639b38842759cc1 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Sun, 31 Mar 2024 00:09:33 -0400
Subject: fixed dropdown in title bar location.  moved rotate button up to not
 interfere with button bar.  don't blur title when editing it.  fix drawgging
 rotate center on templates.  support setting alternate colors for text docs
 -- pretty hacky.   fixed tabbing through dashFieldViews.

---
 src/client/views/DocumentDecorations.scss           |  2 +-
 src/client/views/DocumentDecorations.tsx            | 21 +++++++++++++++------
 src/client/views/StyleProvider.tsx                  |  5 ++++-
 .../collectionFreeForm/CollectionFreeFormView.tsx   |  2 +-
 src/client/views/global/globalScripts.ts            |  4 +++-
 src/client/views/nodes/DocumentView.scss            |  6 ++++++
 src/client/views/nodes/DocumentView.tsx             |  6 +++++-
 .../views/nodes/formattedText/DashFieldView.tsx     | 17 ++++++++++-------
 .../views/nodes/formattedText/FormattedTextBox.tsx  |  7 +++++--
 9 files changed, 50 insertions(+), 20 deletions(-)

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

diff --git a/src/client/views/DocumentDecorations.scss b/src/client/views/DocumentDecorations.scss
index ac0ef054c..239c0a977 100644
--- a/src/client/views/DocumentDecorations.scss
+++ b/src/client/views/DocumentDecorations.scss
@@ -14,7 +14,7 @@ $resizeHandler: 8px;
         height: 30;
         width: 30;
         right: -40;
-        bottom: -40;
+        bottom: -20;
         //top: calc(50% - 15px);
         position: absolute;
         pointer-events: all;
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 6698cd5bc..951e0912c 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -73,11 +73,18 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
         DocumentDecorations.Instance = this;
         document.addEventListener('pointermove', // show decorations whenever pointer moves outside of selection bounds.
             action(e => {
+                let inputting = false;
+                if (this._titleControlString.startsWith('$')) {
+                    const titleFieldKey = this._titleControlString.substring(1);
+                    if (SelectionManager.Views[0]?.Document[titleFieldKey] !== this._accumulatedTitle) {
+                        inputting = true;
+                    }
+                }
                 const center = {x: (this.Bounds.x+this.Bounds.r)/2, y: (this.Bounds.y+this.Bounds.b)/2};
                 const {x,y} = Utils.rotPt(e.clientX - center.x, 
                                           e.clientY - center.y, 
                                           NumCast(SelectionManager.Views.lastElement()?.screenToViewTransform().Rotate));
-                (this._showNothing = !DocumentButtonBar.Instance?._tooltipOpen && !(this.Bounds.x !== Number.MAX_VALUE && //
+                (this._showNothing = !inputting && !DocumentButtonBar.Instance?._tooltipOpen && !(this.Bounds.x !== Number.MAX_VALUE && //
                     (this.Bounds.x > center.x+x || this.Bounds.r < center.x+x || 
                      this.Bounds.y > center.y+y || this.Bounds.b < center.y+y )));
             })); // prettier-ignore
@@ -336,8 +343,8 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
         const newloccentern = seldocview.screenToContentsTransform().transformPoint(rotCenter[0], rotCenter[1]);
         const newlocenter = [newloccentern[0] - NumCast(seldocview.layoutDoc._width) / 2, newloccentern[1] - NumCast(seldocview.layoutDoc._height) / 2];
         const final = Utils.rotPt(newlocenter[0], newlocenter[1], -(NumCast(seldocview.Document._rotation) / 180) * Math.PI);
-        seldocview.Document.rotation_centerX = final.x / NumCast(seldocview.layoutDoc._width);
-        seldocview.Document.rotation_centerY = final.y / NumCast(seldocview.layoutDoc._height);
+        seldocview.Document._rotation_centerX = final.x / NumCast(seldocview.layoutDoc._width);
+        seldocview.Document._rotation_centerY = final.y / NumCast(seldocview.layoutDoc._height);
     };
 
     @action
@@ -350,8 +357,10 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
             (e: PointerEvent, down: number[], delta: number[]) => // return false to keep getting events
                 this.setRotateCenter(seldocview, [this.rotCenter[0] + delta[0], this.rotCenter[1] + delta[1]]) as any as boolean, 
             action(e => (this._isRotating = false)), // upEvent
-            action(e => (seldocview.Document.rotation_centerX = seldocview.Document.rotation_centerY = 0))
+            action(e => (seldocview.Document._rotation_centerX = seldocview.Document._rotation_centerY = 0)),
+            true
         ); // prettier-ignore
+        e.stopPropagation();
     };
 
     @action
@@ -617,7 +626,7 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
         if (lastView) {
             const invXf = lastView.screenToContentsTransform().inverse();
             const seldoc = lastView.layoutDoc;
-            const loccenter = Utils.rotPt(NumCast(seldoc.rotation_centerX) * NumCast(seldoc._width), NumCast(seldoc.rotation_centerY) * NumCast(seldoc._height), invXf.Rotate);
+            const loccenter = Utils.rotPt(NumCast(seldoc._rotation_centerX) * NumCast(seldoc._width), NumCast(seldoc._rotation_centerY) * NumCast(seldoc._height), invXf.Rotate);
             return invXf.transformPoint(loccenter.x + NumCast(seldoc._width) / 2, loccenter.y + NumCast(seldoc._height) / 2);
         }
         return this._rotCenter;
@@ -721,7 +730,7 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
                     value={hideTitle ? '' : this._accumulatedTitle}
                     onBlur={action((e: React.FocusEvent) => {
                         this._editingTitle = false;
-                        !hideTitle && this.titleBlur();
+                        this.titleBlur();
                     })}
                     onChange={action(e => !hideTitle && (this._accumulatedTitle = e.target.value))}
                     onKeyDown={hideTitle ? emptyFunction : this.titleEntered}
diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx
index 749198fe6..dcec2fe3d 100644
--- a/src/client/views/StyleProvider.tsx
+++ b/src/client/views/StyleProvider.tsx
@@ -200,7 +200,10 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps &
                 : 0;
         case StyleProp.BackgroundColor: {
             if (SettingsManager.Instance.LastPressedBtn === doc) return SettingsManager.userColor; // hack to indicate active menu panel item
-            let docColor: Opt<string> = StrCast(doc?.[fieldKey + 'backgroundColor'], StrCast(doc?.backgroundColor, isCaption ? 'rgba(0,0,0,0.4)' : ''));
+            const dataKey = doc ? Doc.LayoutFieldKey(doc) : "";
+            const usePath = StrCast(doc?.[dataKey + "_usePath"]);
+            const alternate = usePath.includes(":hover") ? ( doc?.isHovering ? '_' + usePath.replace(":hover","") : "") : usePath ? "_" +usePath:usePath;
+            let docColor: Opt<string> = StrCast(doc?.[fieldKey+alternate], StrCast(doc?.['backgroundColor' +alternate], isCaption ? 'rgba(0,0,0,0.4)' : ''));
             if (doc?.[StrCast(doc?.layout_fieldKey)] instanceof Doc) docColor = StrCast(doc._backgroundColor,docColor)
             // prettier-ignore
             switch (layoutDoc?.type) {
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index d435173f3..791124f50 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -1288,7 +1288,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         const rotation = Cast(_rotation,'number',
             !this.layoutDoc._rotation_jitter ? null
                 : NumCast(this.layoutDoc._rotation_jitter) * random(-1, 1, NumCast(x), NumCast(y)) );
-        const childProps = { ...this._props, styleProvider: this.clusterStyleProvider };
+        const childProps = { ...this._props, fieldKey: '', styleProvider: this.clusterStyleProvider };
         return {
             x: Number.isNaN(NumCast(x)) ? 0 : NumCast(x),
             y: Number.isNaN(NumCast(y)) ? 0 : NumCast(y),
diff --git a/src/client/views/global/globalScripts.ts b/src/client/views/global/globalScripts.ts
index 3ea7bfbcd..dab642499 100644
--- a/src/client/views/global/globalScripts.ts
+++ b/src/client/views/global/globalScripts.ts
@@ -59,7 +59,9 @@ ScriptingGlobals.add(function setBackgroundColor(color?: string, checkResult?: b
                 obj[fieldKey] = color;
                 CollectionFreeFormDocumentView.setStringValues(contentFrameNumber, dv.Document, obj);
             } else {
-                dv.Document[DocData][fieldKey] = color;
+                const dataKey = Doc.LayoutFieldKey(dv.Document);
+                const alternate = (dv.layoutDoc[dataKey + '_usePath'] ? '_' + dv.layoutDoc[dataKey + '_usePath'] : '').replace(':hover', '');
+                dv.dataDoc[fieldKey + alternate] = color;
             }
         });
     } else {
diff --git a/src/client/views/nodes/DocumentView.scss b/src/client/views/nodes/DocumentView.scss
index 4fea6b3c6..23dada260 100644
--- a/src/client/views/nodes/DocumentView.scss
+++ b/src/client/views/nodes/DocumentView.scss
@@ -234,6 +234,12 @@
     }
 }
 
+.documntViewInternal-dropdown {
+    > div {
+        transform-origin: top left !important;
+    }
+}
+
 .contentFittingDocumentView {
     position: relative;
     display: flex;
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index a04030a5f..fc2da18d9 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -843,7 +843,11 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
                     background,
                     pointerEvents: (!this.disableClickScriptFunc && this.onClickHandler) || this.Document.ignoreClick ? 'none' : this.isContentActive() || this._props.isDocumentActive?.() ? 'all' : undefined,
                 }}>
-                {!dropdownWidth ? null : <div style={{ width: dropdownWidth }}>{this.fieldsDropdown(showTitle)}</div>}
+                {!dropdownWidth ? null : (
+                    <div className="documntViewInternal-dropdown" style={{ width: dropdownWidth }}>
+                        {this.fieldsDropdown(showTitle)}
+                    </div>
+                )}
                 <div
                     style={{
                         width: `calc(100% - ${dropdownWidth}px)`,
diff --git a/src/client/views/nodes/formattedText/DashFieldView.tsx b/src/client/views/nodes/formattedText/DashFieldView.tsx
index f6ca97ee9..22a0cbe5e 100644
--- a/src/client/views/nodes/formattedText/DashFieldView.tsx
+++ b/src/client/views/nodes/formattedText/DashFieldView.tsx
@@ -29,6 +29,7 @@ export class DashFieldView {
     root: any;
     node: any;
     tbox: FormattedTextBox;
+    getpos: any;
     @observable _nodeSelected = false;
     NodeSelected = () => this._nodeSelected;
 
@@ -38,6 +39,7 @@ export class DashFieldView {
         const self = this;
         this.node = node;
         this.tbox = tbox;
+        this.getpos = getPos;
         this.dom = document.createElement('div');
         this.dom.style.width = node.attrs.width;
         this.dom.style.height = node.attrs.height;
@@ -54,13 +56,13 @@ export class DashFieldView {
                 const editor = tbox.EditorView;
                 if (editor) {
                     const state = editor.state;
-                    for (var i = state.selection.to; i < state.doc.content.size; i++) {
+                    for (var i = self.getpos() + 1; i < state.doc.content.size; i++) {
                         if (state.doc.nodeAt(i)?.type.name === state.schema.nodes.dashField.name) {
                             editor.dispatch(state.tr.setSelection(new NodeSelection(state.doc.resolve(i))));
                             return;
                         }
                     }
-                    tBox.setFocus(state.selection.to);
+                    // tBox.setFocus(state.selection.to);
                 }
             }
         };
@@ -137,7 +139,7 @@ export class DashFieldViewInternal extends ObservableReactComponent<IDashFieldVi
         makeObservable(this);
         this._fieldKey = this._props.fieldKey;
         this._textBoxDoc = this._props.tbox.Document;
-        const setDoc = (doc: Doc) => (this._dashDoc = this._props.dataDoc ? doc[DocData] : doc);
+        const setDoc = action((doc: Doc) => (this._dashDoc = this._props.dataDoc ? doc[DocData] : doc));
 
         if (this._props.docId) {
             DocServer.GetRefField(this._props.docId).then(dashDoc => dashDoc instanceof Doc && setDoc(dashDoc));
@@ -255,7 +257,7 @@ export class DashFieldViewInternal extends ObservableReactComponent<IDashFieldVi
 
     @undoBatch
     selectVal = (event: React.ChangeEvent<HTMLSelectElement> | undefined) => {
-        event && this._dashDoc && (this._dashDoc[this._fieldKey] = event.target.value);
+        event && this._dashDoc && (this._dashDoc[this._fieldKey] = event.target.value === '-unset-' ? undefined : event.target.value);
     };
 
     @computed get values() {
@@ -281,13 +283,14 @@ export class DashFieldViewInternal extends ObservableReactComponent<IDashFieldVi
                     </span>
                 )}
                 {this._props.fieldKey.startsWith('#') || this._hideValue ? null : this.fieldValueContent}
-                {/* {!this.values.length ? null : (
-                    <select className="dashFieldView-select" tabIndex={-1} onChange={this.selectVal}>
+                {!this.values.length ? null : (
+                    <select className="dashFieldView-select" tabIndex={-1} defaultValue={Field.toKeyValueString(this._dashDoc!, this._fieldKey)} onChange={this.selectVal}>
+                        <option value="-unset-">-unset-</option>
                         {this.values.map(val => (
                             <option value={val.value}>{val.label}</option>
                         ))}
                     </select>
-                )} */}
+                )}
             </div>
         );
     }
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 66df1eaf2..3700b08d6 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -2064,8 +2064,11 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
         return styleFromLayoutString?.height === '0px' ? null : (
             <div
                 className="formattedTextBox"
-                onPointerEnter={action(() => (this._isHovering = true))}
-                onPointerLeave={action(() => (this._isHovering = false))}
+                onPointerEnter={action(() => {
+                    this._isHovering = true;
+                    this.layoutDoc[`_${this._props.fieldKey}_usePath`] && (this.Document.isHovering = true);
+                })}
+                onPointerLeave={action(() => (this.Document.isHovering = this._isHovering = false))}
                 ref={r => {
                     this._oldWheel?.removeEventListener('wheel', this.onPassiveWheel);
                     this._oldWheel = r;
-- 
cgit v1.2.3-70-g09d2

