From 98fd2ecbbf5168986a5eba889bff6aeea0eebb49 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Wed, 8 Mar 2023 21:54:03 -0500
Subject: some cleanup of presBox properties panels.  fixes for undoing some
 slider actions.

---
 src/client/views/MainView.tsx | 2 --
 1 file changed, 2 deletions(-)

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

diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index e1ba5943d..945cd61db 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -553,8 +553,6 @@ export class MainView extends React.Component {
             Doc.MyTrails && (Doc.ActivePresentation = pres);
             Doc.AddDocToList(Doc.MyTrails, 'data', pres);
             this.closeFlyout();
-        } else {
-            PresBox.NavigateToDoc(DocCast(pres.presentationTargetDoc), pres);
         }
     };
 
-- 
cgit v1.2.3-70-g09d2


From 20c0190e820f2bd343693368b7ef55a91f19c880 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Tue, 14 Mar 2023 20:31:06 -0400
Subject: simplified Deserialize code.  streamlined currentUserUtils to need
 fewer compiled functions by parameterizing more functions.  .

---
 src/client/documents/Documents.ts                  |  13 +-
 src/client/util/CurrentUserUtils.ts                | 145 +++++------
 src/client/util/Scripting.ts                       |   4 +-
 src/client/util/SelectionManager.ts                |   7 +-
 src/client/util/SerializationHelper.ts             |  86 +------
 src/client/views/DashboardView.tsx                 |   1 -
 src/client/views/GestureOverlay.tsx                |   7 +-
 src/client/views/MainView.tsx                      |  20 --
 src/client/views/OverlayView.tsx                   |   1 +
 .../views/collections/CollectionDockingView.tsx    |  19 +-
 src/client/views/collections/CollectionView.tsx    |   2 +-
 src/client/views/collections/TreeView.tsx          |  10 +-
 src/client/views/nodes/DocumentView.tsx            |   2 +
 src/client/views/nodes/WebBox.tsx                  |   8 +-
 src/client/views/nodes/button/FontIconBox.tsx      | 272 +++++++--------------
 src/fields/Doc.ts                                  |  57 ++---
 src/fields/ScriptField.ts                          |  31 +--
 src/fields/util.ts                                 |  22 +-
 18 files changed, 253 insertions(+), 454 deletions(-)

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

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 251e432ef..457811e26 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -200,6 +200,8 @@ export class DocumentOptions {
     'icon-nativeWidth'?: NUMt = new NumInfo('native width of icon view');
     'icon-nativeHeight'?: NUMt = new NumInfo('native height of icon view');
     'dragFactory-count'?: NUMt = new NumInfo('number of items created from a drag button (used for setting title with incrementing index)');
+    openFactoryLocation?: string; // an OpenWhere value to place the factory created document
+    openFactoryAsDelegate?: boolean; //
     lat?: number;
     lng?: number;
     infoWindowOpen?: boolean;
@@ -220,6 +222,8 @@ export class DocumentOptions {
     autoPlayAnchors?: boolean; // whether to play audio/video when an anchor is clicked in a stackedTimeline.
     dontPlayLinkOnSelect?: boolean; // whether an audio/video should start playing when a link is followed to it.
     toolTip?: string; // tooltip to display on hover
+    toolType?: string; // type of pen tool
+    expertMode?: boolean; // something available only in expert (not novice) mode
     contextMenuFilters?: List<ScriptField>;
     contextMenuScripts?: List<ScriptField>;
     contextMenuLabels?: List<string>;
@@ -1353,6 +1357,8 @@ export namespace DocUtils {
                 const script = scripts[key];
                 if (ScriptCast(doc[key])?.script.originalScript !== scripts[key] && script) {
                     doc[key] = ScriptField.MakeScript(script, {
+                        self: Doc.name,
+                        this: Doc.name,
                         dragData: DragManager.DocumentDragData.name,
                         value: 'any',
                         _readOnly_: 'boolean',
@@ -1862,11 +1868,8 @@ export namespace DocUtils {
 }
 
 ScriptingGlobals.add('Docs', Docs);
-ScriptingGlobals.add(function copyDragFactory(dragFactory: Doc) {
-    return DocUtils.copyDragFactory(dragFactory);
-});
-ScriptingGlobals.add(function delegateDragFactory(dragFactory: Doc) {
-    return DocUtils.delegateDragFactory(dragFactory);
+ScriptingGlobals.add(function copyDragFactory(dragFactory: Doc, asDelegate?: boolean) {
+    return dragFactory instanceof Doc ? (asDelegate ? DocUtils.delegateDragFactory(dragFactory) : DocUtils.copyDragFactory(dragFactory)) : dragFactory;
 });
 ScriptingGlobals.add(function makeDelegate(proto: any) {
     const d = Docs.Create.DelegateDocument(proto, { title: 'child of ' + proto.title });
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index c4fb4788c..2820c66ee 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -21,6 +21,7 @@ import { DashboardView } from "../views/DashboardView";
 import { Colors } from "../views/global/globalEnums";
 import { MainView } from "../views/MainView";
 import { ButtonType, NumButtonType } from "../views/nodes/button/FontIconBox";
+import { OpenWhere } from "../views/nodes/DocumentView";
 import { OverlayView } from "../views/OverlayView";
 import { DragManager } from "./DragManager";
 import { MakeTemplate } from "./DropConverter";
@@ -40,6 +41,8 @@ interface Button {
     numBtnMax?: number;
     switchToggle?: boolean;
     width?: number;
+    toolType?: string; // type of pen tool
+    expertMode?: boolean;// available only in expert mode
     btnList?: List<string>;
     ignoreClick?: boolean;
     buttonText?: string;
@@ -78,7 +81,7 @@ export class CurrentUserUtils {
         ];
         const requiredTypes = requiredTypeNameFields.map(({ btnOpts, template, templateOpts }) => {
             const tempBtn =  DocListCast(tempDocs?.data)?.find(doc => doc.title === btnOpts.title);
-            const reqdScripts = { onDragStart: '{ return copyDragFactory(this.dragFactory); }' };
+            const reqdScripts = { onDragStart:  '{ return copyDragFactory(this.dragFactory,this.openFactoryAsDelegate); }' };
             const assignBtnAndTempOpts = (templateBtn:Opt<Doc>, btnOpts:DocumentOptions, templateOptions:DocumentOptions) => {
                 if (templateBtn) {
                     DocUtils.AssignOpts(templateBtn,btnOpts);
@@ -105,7 +108,7 @@ export class CurrentUserUtils {
         const reqdClickOpts:DocumentOptions = {_width: 300, _height:200, system: true};
         const reqdTempOpts:{opts:DocumentOptions, script: string}[] = [
             { opts: { title: "Open In Target",        targetScriptKey: "onChildClick"},       script: "docCast(thisContainer.target).then((target) => target && (target.proto.data = new List([self])))"},
-            { opts: { title: "Open Detail On Right",  targetScriptKey: "onChildDoubleClick"}, script: "openOnRight(self.doubleClickView)"}];
+            { opts: { title: "Open Detail On Right",  targetScriptKey: "onChildDoubleClick"}, script: `openDoc(self.doubleClickView.${OpenWhere.addRight})`}];
         const reqdClickList = reqdTempOpts.map(opts => {
             const allOpts = {...reqdClickOpts, ...opts.opts};
             const clickDoc = tempClicks ? DocListCast(tempClicks.data).find(doc => doc.title === opts.opts.title): undefined;
@@ -121,11 +124,11 @@ export class CurrentUserUtils {
         const tempClicks = DocCast(doc[field]);
         const reqdClickOpts:DocumentOptions = { _width: 300, _height:200, system: true};
         const reqdTempOpts:{opts:DocumentOptions, script: string}[] = [
-            { opts: { title: "onClick"},             script: "console.log( 'click')"},
-            { opts: { title: "onDoubleClick"},       script: "console.log( 'double click')"},
-            { opts: { title: "onChildClick"},        script: "console.log( 'child click')"},
-            { opts: { title: "onChildDoubleClick"},  script: "console.log( 'child double click')"},
-            { opts: { title: "onCheckedClick"},      script: "console.log( heading, checked, containingTreeView)"},
+            { opts: { title: "onClick"},             script: "console.log('click')"},
+            { opts: { title: "onDoubleClick"},       script: "console.log('click')"},
+            { opts: { title: "onChildClick"},        script: "console.log('click')"},
+            { opts: { title: "onChildDoubleClick"},  script: "console.log('click')"},
+            { opts: { title: "onCheckedClick"},      script: "console.log(heading, checked, containingTreeView)"},
         ];
         const reqdClickList = reqdTempOpts.map(opts => {
             const allOpts = {...reqdClickOpts, ...opts.opts};
@@ -183,7 +186,7 @@ export class CurrentUserUtils {
             const allopts = {system: true, ...opts};
             return DocUtils.AssignScripts(  (curIcon?.iconTemplate === opts.iconTemplate ? 
                       DocUtils.AssignOpts(curIcon, allopts):undefined) ?? ((templateIconsDoc[iconFieldName] = MakeTemplate(creator(allopts), true, iconFieldName, templateField))),
-                {onClick:"deiconifyView(documentView)", onDoubleClick: "deiconifyViewToLightbox(documentView"});
+                {onClick:"deiconifyView(documentView)", onDoubleClick: "deiconifyViewToLightbox(documentView)"});
         };
         const labelBox = (opts: DocumentOptions, data?:string) => Docs.Create.LabelDocument({
             textTransform: "unset", letterSpacing: "unset", _singleLine: false, _minFontSize: 14, _maxFontSize: 24, borderRounding: "5px",  _width: 150, _height: 70, _xPadding: 10, _yPadding: 10, ...opts
@@ -211,7 +214,7 @@ export class CurrentUserUtils {
     /// initalizes the set of "empty<DocType>" versions of each document type with default fields.  e.g.,. emptyNote, emptyTrail
     static creatorBtnDescriptors(doc: Doc): {
         title: string, toolTip: string, icon: string,  ignoreClick?: boolean, dragFactory?: Doc, 
-        backgroundColor?: string,  clickFactory?: Doc, scripts?: { onClick?: string, onDragStart?: string}, funcs?: {onDragStart?:string, hidden?: string},
+        backgroundColor?: string, openFactoryAsDelegate?:boolean, openFactoryLocation?:string, clickFactory?: Doc, scripts?: { onClick?: string, onDragStart?: string}, funcs?: {onDragStart?:string, hidden?: string},
     }[] {
         const standardOps = (key:string) => ({ title : "Untitled "+ key, _fitWidth: false, system: true, "dragFactory-count": 0, cloneFieldFilter: new List<string>(["system"]) });
         const json = {
@@ -257,13 +260,13 @@ export class CurrentUserUtils {
                           creator:(opts:DocumentOptions)=> any // how to create the empty thing if it doesn't exist
               }[] = [
             {key: "Note",       creator: opts => Docs.Create.TextDocument("", opts),        opts: { _width: 200, _autoHeight: true }},
-            {key: "Noteboard",  creator: opts => Docs.Create.NoteTakingDocument([], opts),  opts: { _width: 250, _height: 200 }},
+            {key: "Noteboard",  creator: opts => Docs.Create.NoteTakingDocument([], opts),  opts: { _width: 250, _height: 200, _fitWidth: true}},
             {key: "Collection", creator: opts => Docs.Create.FreeformDocument([], opts),    opts: { _width: 150, _height: 100, _fitWidth: true }},
             {key: "Equation",   creator: opts => Docs.Create.EquationDocument(opts),        opts: { _width: 300, _height: 35, _backgroundGridShow: true, }},
             {key: "Webpage",    creator: opts => Docs.Create.WebDocument("",opts),          opts: { _width: 400, _height: 512, _nativeWidth: 850, useCors: true, }},
             {key: "Comparison", creator: Docs.Create.ComparisonDocument,                    opts: { _width: 300, _height: 300 }},
             {key: "Audio",      creator: opts => Docs.Create.AudioDocument(nullAudio, opts),opts: { _width: 200, _height: 100, }},
-            {key: "Map",        creator: opts => Docs.Create.MapDocument([], opts),         opts: { _width: 800, _height: 600, _showSidebar: true,  }},
+            {key: "Map",        creator: opts => Docs.Create.MapDocument([], opts),         opts: { _width: 800, _height: 600, _fitWidth: true, _showSidebar: true,  }},
             {key: "Screengrab", creator: Docs.Create.ScreenshotDocument,                    opts: { _width: 400, _height: 200 }},
             {key: "WebCam",     creator: opts => Docs.Create.WebCamDocument("", opts),      opts: { _width: 400, _height: 200, recording:true, system: true, cloneFieldFilter: new List<string>(["system"]) }},
             {key: "Button",     creator: Docs.Create.ButtonDocument,                        opts: { _width: 150, _height: 50, _xPadding: 10, _yPadding: 10, _isLinkButton: true }},
@@ -282,30 +285,34 @@ export class CurrentUserUtils {
         emptyThings.forEach(thing => DocUtils.AssignDocField(doc, "empty"+thing.key, (opts) => thing.creator(opts), {...standardOps(thing.key), ...thing.opts}, undefined, undefined, thing.funcs));
         
         return [
-            { toolTip: "Tap or drag to create a note",            title: "Note", icon: "sticky-note",   dragFactory: doc.emptyNote as Doc, },
-            { toolTip: "Tap or drag to create a note board",      title: "Notes", icon: "folder",       dragFactory: doc.emptyNoteboard as Doc, },
-            { toolTip: "Tap or drag to create a collection",      title: "Col", icon: "folder",         dragFactory: doc.emptyCollection as Doc, clickFactory: DocCast(doc.emptyTab),  scripts: { onClick: 'openOnRight(copyDragFactory(this.clickFactory))', onDragStart: '{ return copyDragFactory(this.dragFactory);}'}, },
-            { toolTip: "Tap or drag to create an equation",       title: "Math", icon: "calculator",    dragFactory: doc.emptyEquation as Doc, },
-            { toolTip: "Tap or drag to create a webpage",         title: "Web", icon: "globe-asia",     dragFactory: doc.emptyWebpage as Doc, },
-            { toolTip: "Tap or drag to create a comparison box",  title: "Compare", icon: "columns",    dragFactory: doc.emptyComparison as Doc, },
-            { toolTip: "Tap or drag to create an audio recorder", title: "Audio", icon: "microphone",   dragFactory: doc.emptyAudio as Doc,       scripts: { onClick: 'openInOverlay(copyDragFactory(this.dragFactory))', onDragStart: '{ return copyDragFactory(this.dragFactory);}'}, },
-            { toolTip: "Tap or drag to create a map",             title: "Map", icon: "map-marker-alt", dragFactory: doc.emptyMap as Doc, },
-            { toolTip: "Tap or drag to create a screen grabber",  title: "Grab", icon: "photo-video",   dragFactory: doc.emptyScreengrab as Doc,  scripts: { onClick: 'openInOverlay(copyDragFactory(this.dragFactory))', onDragStart: '{ return copyDragFactory(this.dragFactory);}'},funcs: { hidden: 'IsNoviceMode()'} },
-            { toolTip: "Tap or drag to create a WebCam recorder", title: "WebCam", icon: "photo-video", dragFactory: doc.emptyWebCam as Doc,      scripts: { onClick: 'openInOverlay(copyDragFactory(this.dragFactory))', onDragStart: '{ return copyDragFactory(this.dragFactory);}'},funcs: { hidden: 'IsNoviceMode()'}},
-            { toolTip: "Tap or drag to create a button",          title: "Button", icon: "bolt",        dragFactory: doc.emptyButton as Doc,      funcs:   { hidden: 'IsNoviceMode()'} },
-            { toolTip: "Tap or drag to create a scripting box",   title: "Script", icon: "terminal",    dragFactory: doc.emptyScript as Doc,      funcs:   { hidden: 'IsNoviceMode()'}},
-            { toolTip: "Tap or drag to create a data viz node",   title: "DataViz", icon: "file",       dragFactory: doc.emptyDataViz as Doc, },
-            { toolTip: "Tap or drag to create a bullet slide",    title: "PPT Slide", icon: "file",     dragFactory: doc.emptySlide as Doc,       funcs:   { hidden: 'IsNoviceMode()'}},
-            { toolTip: "Tap or drag to create a data note",       title: "DataNote", icon: "window-maximize", dragFactory: doc.emptyHeader as Doc,scripts: { onClick: 'openOnRight(delegateDragFactory(this.dragFactory))', onDragStart: '{ return delegateDragFactory(this.dragFactory);}'},  },
-            { toolTip: "Toggle a Calculator REPL",                title: "repl", icon: "calculator",                                              scripts: { onClick: 'addOverlayWindow("ScriptingRepl", { x: 300, y: 100, width: 200, height: 200, title: "Scripting REPL" })' } },
-          ].map(tuple => ({scripts: {onClick: 'openOnRight(copyDragFactory(this.dragFactory))', onDragStart: '{ return copyDragFactory(this.dragFactory);}'}, ...tuple, }))
+            { toolTip: "Tap or drag to create a note",            title: "Note", icon: "sticky-note",   dragFactory: doc.emptyNote as Doc,      clickFactory: DocCast(doc.emptyNote)},
+            { toolTip: "Tap or drag to create a note board",      title: "Notes", icon: "folder",       dragFactory: doc.emptyNoteboard as Doc, clickFactory: DocCast(doc.emptyNoteboard)},
+            { toolTip: "Tap or drag to create a collection",      title: "Col", icon: "folder",         dragFactory: doc.emptyCollection as Doc,clickFactory: DocCast(doc.emptyTab)},
+            { toolTip: "Tap or drag to create an equation",       title: "Math", icon: "calculator",    dragFactory: doc.emptyEquation as Doc,  clickFactory: DocCast(doc.emptyEquation)},
+            { toolTip: "Tap or drag to create a webpage",         title: "Web", icon: "globe-asia",     dragFactory: doc.emptyWebpage as Doc,   clickFactory: DocCast(doc.emptyWebpage)},
+            { toolTip: "Tap or drag to create a comparison box",  title: "Compare", icon: "columns",    dragFactory: doc.emptyComparison as Doc,clickFactory: DocCast(doc.emptyComparison)},
+            { toolTip: "Tap or drag to create an audio recorder", title: "Audio", icon: "microphone",   dragFactory: doc.emptyAudio as Doc,     clickFactory: DocCast(doc.emptyAudio),      openFactoryLocation: OpenWhere.overlay},
+            { toolTip: "Tap or drag to create a map",             title: "Map", icon: "map-marker-alt", dragFactory: doc.emptyMap as Doc,       clickFactory: DocCast(doc.emptyMap)},
+            { toolTip: "Tap or drag to create a screen grabber",  title: "Grab", icon: "photo-video",   dragFactory: doc.emptyScreengrab as Doc,clickFactory: DocCast(doc.emptyScreengrab), openFactoryLocation: OpenWhere.overlay},
+            { toolTip: "Tap or drag to create a WebCam recorder", title: "WebCam", icon: "photo-video", dragFactory: doc.emptyWebCam as Doc,    clickFactory: DocCast(doc.emptyWebCam),     openFactoryLocation: OpenWhere.overlay},
+            { toolTip: "Tap or drag to create a button",          title: "Button", icon: "bolt",        dragFactory: doc.emptyButton as Doc,    clickFactory: DocCast(doc.emptyButton)},
+            { toolTip: "Tap or drag to create a scripting box",   title: "Script", icon: "terminal",    dragFactory: doc.emptyScript as Doc,    clickFactory: DocCast(doc.emptyScript)},
+            { toolTip: "Tap or drag to create a data viz node",   title: "DataViz", icon: "file",       dragFactory: doc.emptyDataViz as Doc,   clickFactory: DocCast(doc.emptyDataViz)},
+            { toolTip: "Tap or drag to create a bullet slide",    title: "PPT Slide", icon: "file",     dragFactory: doc.emptySlide as Doc,     clickFactory: DocCast(doc.emptySlide),      openFactoryLocation: OpenWhere.overlay},
+            { toolTip: "Tap or drag to create a data note", title: "DataNote", icon: "window-maximize", dragFactory: doc.emptyHeader as Doc,    clickFactory: DocCast(doc.emptyHeader),     openFactoryAsDelegate: true },
+            { toolTip: "Toggle a Calculator REPL",                title: "repl", icon: "calculator",                                            clickFactory: "repl" as any,                openFactoryLocation: OpenWhere.overlay},
+          ].map(tuple => (
+              { openFactoryLocation: OpenWhere.addRight, 
+                scripts: { onClick:     'openDoc(copyDragFactory(this.clickFactory,this.openFactoryAsDelegate), this.openFactoryLocation)', 
+                           onDragStart: '{ return copyDragFactory(this.dragFactory,this.openFactoryAsDelegate); }'}, 
+                ...tuple, }))
     }
 
     /// Initalizes the "creator" buttons for the sidebar-- eg. the default set of draggable document creation tools
     static setupCreatorButtons(doc: Doc, dragCreatorDoc?:Doc):Doc {
         const creatorBtns = CurrentUserUtils.creatorBtnDescriptors(doc).map((reqdOpts) => {
             const btn = dragCreatorDoc ? DocListCast(dragCreatorDoc.data).find(doc => doc.title === reqdOpts.title): undefined;
-            const opts:DocumentOptions = {...OmitKeys(reqdOpts, ["funcs", "scripts", "backgroundColor"]).omit,
+            const opts:DocumentOptions = {...OmitKeys(reqdOpts, ["funcs", "scripts", "backgroundColor"]).omit, 
                 _nativeWidth: 50, _nativeHeight: 50, _width: 35, _height: 35, _hideContextMenu: true, _stayInCollection: true, 
                 btnType: ButtonType.ToolButton, backgroundColor: reqdOpts.backgroundColor ?? Colors.DARK_GRAY, color: Colors.WHITE, system: true,
                 _removeDropProperties: new List<string>(["_stayInCollection"]),
@@ -603,53 +610,50 @@ export class CurrentUserUtils {
 
     static textTools():Button[] {
         return [
-            { title: "Font",    toolTip: "Font", width: 100,     btnType: ButtonType.DropdownList,               ignoreClick: true, scripts: {script: 'setFont(value, _readOnly_)'},
+            { title: "Font",    toolTip: "Font", width: 100,     btnType: ButtonType.DropdownList,                      toolType:"font",     ignoreClick: true,   scripts: {script:  '{ return setFontAttr(self.toolType, value, _readOnly_);}'},
               btnList: new List<string>(["Roboto", "Roboto Mono", "Nunito", "Times New Roman", "Arial", "Georgia", "Comic Sans MS", "Tahoma", "Impact", "Crimson Text"]) },
-            { title: "Size",    toolTip: "Font size", width: 75, btnType: ButtonType.NumberButton,               ignoreClick: true, scripts: {script:  '{ return setFontSize(value, _readOnly_);}'}, numBtnMax: 200, numBtnMin: 0, numBtnType: NumButtonType.DropdownOptions },
-            { title: "Color",   toolTip: "Font color",           btnType: ButtonType.ColorButton,  icon: "font", ignoreClick: true, scripts: {script:  '{ return setFontColor(value, _readOnly_);}'}},
-            { title: "Highlight",toolTip:"Font highlight",       btnType: ButtonType.ColorButton,  icon: "highlighter", ignoreClick: true, scripts: {script:  '{ return setFontHighlight(value, _readOnly_);}'},funcs: {hidden: "IsNoviceMode()"} },
-            { title: "Bold",    toolTip: "Bold (Ctrl+B)",        btnType: ButtonType.ToggleButton, icon: "bold",                    scripts: {onClick: '{ return toggleBold(_readOnly_); }'} },
-            { title: "Italic",  toolTip: "Italic (Ctrl+I)",      btnType: ButtonType.ToggleButton, icon: "italic",                  scripts: {onClick: '{ return toggleItalic(_readOnly_);}'} },
-            { title: "Under",   toolTip: "Underline (Ctrl+U)",   btnType: ButtonType.ToggleButton, icon: "underline",               scripts: {onClick: '{ return toggleUnderline(_readOnly_);}'} },
-            { title: "Bullets", toolTip: "Bullet List",          btnType: ButtonType.ToggleButton, icon: "list",                    scripts: {onClick: '{ return setBulletList("bullet", _readOnly_);}'} },
-            { title: "#",       toolTip: "Number List",          btnType: ButtonType.ToggleButton, icon: "list-ol",                 scripts: {onClick: '{ return setBulletList("decimal", _readOnly_);}'} },
-
+            { title: "Size",    toolTip: "Font size", width: 75, btnType: ButtonType.NumberButton,                      toolType:"fontSize", ignoreClick: true,   scripts: {script:  '{ return setFontAttr(self.toolType, value, _readOnly_);}'}, numBtnMax: 200, numBtnMin: 0, numBtnType: NumButtonType.DropdownOptions },
+            { title: "Color",   toolTip: "Font color",           btnType: ButtonType.ColorButton,  icon: "font",        toolType:"fontColor",ignoreClick: true,   scripts: {script:  '{ return setFontAttr(self.toolType, value, _readOnly_);}'}},
+            { title: "Highlight",toolTip:"Font highlight",       btnType: ButtonType.ColorButton,  icon: "highlighter", toolType:"highlight",ignoreClick: true,   scripts: {script:  '{ return setFontAttr(self.toolType, value, _readOnly_);}'},funcs: {hidden: "IsNoviceMode()"} },
+            { title: "Bold",    toolTip: "Bold (Ctrl+B)",        btnType: ButtonType.ToggleButton, icon: "bold",        toolType:"bold",                          scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'} },
+            { title: "Italic",  toolTip: "Italic (Ctrl+I)",      btnType: ButtonType.ToggleButton, icon: "italic",      toolType:"italics",                       scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'} },
+            { title: "Under",   toolTip: "Underline (Ctrl+U)",   btnType: ButtonType.ToggleButton, icon: "underline",   toolType:"underline",                     scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'} },
+            { title: "Bullets", toolTip: "Bullet List",          btnType: ButtonType.ToggleButton, icon: "list",        toolType:"bullet",                        scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'} },
+            { title: "#",       toolTip: "Number List",          btnType: ButtonType.ToggleButton, icon: "list-ol",     toolType:"decimal",                       scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'} },
+            { title: "Left",    toolTip: "Left align",           btnType: ButtonType.ToggleButton, icon: "align-left",  toolType:"left",                          scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}' }},
+            { title: "Center",  toolTip: "Center align",         btnType: ButtonType.ToggleButton, icon: "align-center",toolType:"center",                        scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'} },
+            { title: "Right",   toolTip: "Right align",          btnType: ButtonType.ToggleButton, icon: "align-right", toolType:"right",                         scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'} },
+            { title: "Dictate", toolTip: "Dictate",              btnType: ButtonType.ToggleButton, icon: "microphone",  toolType:"dictation",                     scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'}},
+            { title: "NoLink",  toolTip: "Auto Link",            btnType: ButtonType.ToggleButton, icon: "link",        toolType:"noAutoLink", expertMode:true,   scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'}, funcs: {hidden: 'IsNoviceMode()'}},
             // { title: "Strikethrough", tooltip: "Strikethrough", btnType: ButtonType.ToggleButton, icon: "strikethrough", scripts: {onClick:: 'toggleStrikethrough()'}},
             // { title: "Superscript", tooltip: "Superscript", btnType: ButtonType.ToggleButton, icon: "superscript", scripts: {onClick:: 'toggleSuperscript()'}},
             // { title: "Subscript", tooltip: "Subscript", btnType: ButtonType.ToggleButton, icon: "subscript", scripts: {onClick:: 'toggleSubscript()'}},
-            { title: "Left",   toolTip: "Left align",   btnType: ButtonType.ToggleButton, icon: "align-left",   scripts: {onClick:'{ return setAlignment("left", _readOnly_);}' }},
-            { title: "Center", toolTip: "Center align", btnType: ButtonType.ToggleButton, icon: "align-center", scripts: {onClick:'{ return setAlignment("center", _readOnly_);}'} },
-            { title: "Right",  toolTip: "Right align",  btnType: ButtonType.ToggleButton, icon: "align-right",  scripts: {onClick:'{ return setAlignment("right", _readOnly_);}'} },
-            { title: "NoLink", toolTip: "Auto Link",    btnType: ButtonType.ToggleButton, icon: "link",         scripts: {onClick:'{ return toggleNoAutoLinkAnchor(_readOnly_);}'}, funcs: {hidden: '!SelectionManager_selectedDocType(undefined, "freeform") || IsNoviceMode()'}},
-            { title: "Dictate",toolTip: "Dictate",      btnType: ButtonType.ToggleButton, icon: "microphone",   scripts: {onClick:'{ return toggleDictation(_readOnly_);}'}},
-        ];
+             ];
     }
 
     static inkTools():Button[] {
         return [
-            { title: "Pen",    toolTip: "Pen (Ctrl+P)",          btnType: ButtonType.ToggleButton, icon: "pen-nib",                     scripts: {onClick:'{ return setActiveTool("pen", false, _readOnly_);}' }},
-            { title: "Write",  toolTip: "Write (Ctrl+Shift+P)",  btnType: ButtonType.ToggleButton, icon: "pen",                         scripts: {onClick:'{ return setActiveTool("write", false, _readOnly_);}'} },
-            { title: "Eraser", toolTip: "Eraser (Ctrl+E)",       btnType: ButtonType.ToggleButton, icon: "eraser",                      scripts: {onClick:'{ return setActiveTool("eraser", false, _readOnly_);}' }},
-            // { title: "Highlighter", toolTip: "Highlighter (Ctrl+H)", btnType: ButtonType.ToggleButton, icon: "highlighter", scripts:{onClick: 'setActiveTool("highlighter")'} },
-            { title: "Circle", toolTip: "Circle (double tap to lock mode)", btnType: ButtonType.ToggleButton, icon: "circle",           scripts: {onClick:`{ return setActiveTool("${GestureUtils.Gestures.Circle}", false, _readOnly_);}`, onDoubleClick:`{ return setActiveTool("${GestureUtils.Gestures.Circle}", true, _readOnly_);}`} },
-            { title: "Square", toolTip: "Square (double tap to lock mode)", btnType: ButtonType.ToggleButton, icon: "square",           scripts: {onClick:`{ return setActiveTool("${GestureUtils.Gestures.Rectangle}", false, _readOnly_);}`, onDoubleClick:`{ return setActiveTool("${GestureUtils.Gestures.Rectangle}", true, _readOnly_);}`} },
-            { title: "Line",   toolTip: "Line (double tap to lock mode)",   btnType: ButtonType.ToggleButton, icon: "minus",            scripts: {onClick:`{ return setActiveTool("${GestureUtils.Gestures.Line}", false, _readOnly_);}`, onDoubleClick:`{ return setActiveTool("${GestureUtils.Gestures.Line}", true, _readOnly_);}`} },
-            { title: "Mask",   toolTip: "Mask",                  btnType: ButtonType.ToggleButton, icon: "user-circle",                 scripts: {onClick:'{ return setIsInkMask(_readOnly_);}'} },
-            { title: "Fill",   toolTip: "Fill color",            btnType: ButtonType.ColorButton,  icon: "fill-drip",ignoreClick: true, scripts: {script: '{ return setFillColor(value, _readOnly_);}'} },
-            { title: "Width",  toolTip: "Stroke width",          btnType: ButtonType.NumberButton,                   ignoreClick: true, scripts: {script: '{ return setStrokeWidth(value, _readOnly_);}'}, numBtnType: NumButtonType.Slider, numBtnMin: 1},
-            { title: "Color",  toolTip: "Stroke color",          btnType: ButtonType.ColorButton,  icon: "pen",      ignoreClick: true, scripts: {script: '{ return setStrokeColor(value, _readOnly_);}'} },
+            { title: "Pen",    toolTip: "Pen (Ctrl+P)",                     btnType: ButtonType.ToggleButton, icon: "pen-nib",    toolType: "pen",                            scripts: {onClick:'{ return setActiveTool(self.toolType, false, _readOnly_);}' }},
+            { title: "Write",  toolTip: "Write (Ctrl+Shift+P)",             btnType: ButtonType.ToggleButton, icon: "pen",        toolType: "write",                          scripts: {onClick:'{ return setActiveTool(self.toolType, false, _readOnly_);}'} },
+            { title: "Eraser", toolTip: "Eraser (Ctrl+E)",                  btnType: ButtonType.ToggleButton, icon: "eraser",     toolType: "eraser",                         scripts: {onClick:'{ return setActiveTool(self.toolType, false, _readOnly_);}' }},
+            { title: "Circle", toolTip: "Circle (double tap to lock mode)", btnType: ButtonType.ToggleButton, icon: "circle",     toolType:GestureUtils.Gestures.Circle,      scripts: {onClick:`{ return setActiveTool(self.toolType, false, _readOnly_);}`, onDoubleClick:`{ return setActiveTool(self.toolType, true, _readOnly_);}`} },
+            { title: "Square", toolTip: "Square (double tap to lock mode)", btnType: ButtonType.ToggleButton, icon: "square",     toolType:GestureUtils.Gestures.Rectangle,   scripts: {onClick:`{ return setActiveTool(self.toolType, false, _readOnly_);}`, onDoubleClick:`{ return setActiveTool(self.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(self.toolType, false, _readOnly_);}`, onDoubleClick:`{ return setActiveTool(self.toolType, true, _readOnly_);}`} },
+            { title: "Mask",   toolTip: "Mask",                             btnType: ButtonType.ToggleButton, icon: "user-circle",toolType: "inkMask",                        scripts: {onClick:'{ return setInkProperty(self.toolType, value, _readOnly_);}'} },
+            { title: "Fill",   toolTip: "Fill color",                       btnType: ButtonType.ColorButton,  icon: "fill-drip",  toolType: "fillColor",   ignoreClick: true, scripts: {script: '{ return setInkProperty(self.toolType, value, _readOnly_);}'} },
+            { title: "Width",  toolTip: "Stroke width",                     btnType: ButtonType.NumberButton,                     toolType: "strokeWidth", ignoreClick: true, scripts: {script: '{ return setInkProperty(self.toolType, value, _readOnly_);}'}, numBtnType: NumButtonType.Slider, numBtnMin: 1},
+            { title: "Color",  toolTip: "Stroke color",                     btnType: ButtonType.ColorButton,  icon: "pen",        toolType: "strokeColor", ignoreClick: true, scripts: {script: '{ return setInkProperty(self.toolType, value, _readOnly_);}'} },
         ];
     }
 
     static schemaTools():Button[] {
-        return [{ title: "Show preview", toolTip: "Show preview of selected document", btnType: ButtonType.ToggleButton, buttonText: "Show Preview", icon: "eye", scripts:{ onClick: '{return toggleSchemaPreview(_readOnly_);}'}, }];
+        return [{ title: "Show preview", toolTip: "Show selection preview", btnType: ButtonType.ToggleButton, buttonText: "Show Preview", icon: "eye", scripts:{ onClick: '{ return toggleSchemaPreview(_readOnly_); }'}, }];
     }
 
     static webTools() {
         return [
             { title: "Back",    toolTip: "Go back",         btnType: ButtonType.ClickButton,  icon: "arrow-left",              scripts: { onClick: '{ return webBack(_readOnly_); }' }},
             { title: "Forward", toolTip: "Go forward",      btnType: ButtonType.ClickButton,  icon: "arrow-right",             scripts: { onClick: '{ return webForward(_readOnly_); }'}},
-            //{ title: "Reload", toolTip: "Reload webpage", btnType: ButtonType.ClickButton, icon: "redo-alt", click: 'webReload()' },
             { title: "URL",     toolTip: "URL", width: 250, btnType: ButtonType.EditableText, icon: "lock", ignoreClick: true, scripts: { script: '{ return webSetURL(value, _readOnly_); }'} },
         ];
     }
@@ -661,18 +665,18 @@ export class CurrentUserUtils {
                     CollectionViewType.Multirow, CollectionViewType.Time, CollectionViewType.Carousel,
                     CollectionViewType.Carousel3D, CollectionViewType.Linear, CollectionViewType.Map,
                     CollectionViewType.Grid, CollectionViewType.NoteTaking]),
-              title: "Perspective",                    toolTip: "View",                 btnType: ButtonType.DropdownList,                                                                                    ignoreClick: true, width: 100, scripts: { script: 'setView(value, _readOnly_)'}},
-            { title: "Pin",     icon: "map-pin",       toolTip: "Pin View to Trail",    btnType: ButtonType.ClickButton,         funcs: {hidden: '!SelectionManager_selectedDocType(undefined, "tab")'},                        width: 20,  scripts: { onClick: 'pinWithView(_readOnly_, altKey)'}},
-            { title: "Back",    icon: "chevron-left",  toolTip: "Prev Animation Frame", btnType: ButtonType.ClickButton,         funcs: {hidden: '!SelectionManager_selectedDocType(undefined, "freeform") || IsNoviceMode()'}, width: 20,  scripts: { onClick: 'prevKeyFrame(_readOnly_)'}},
-            { title: "Num",icon: "",toolTip: "Frame Number (click to toggle edit mode)",btnType: ButtonType.TextButton,          funcs: {hidden: '!SelectionManager_selectedDocType(undefined, "freeform") || IsNoviceMode()', buttonText: 'selectedDocs()?.lastElement()?.currentFrame?.toString()'}, width: 20, scripts: { onClick: '{ return curKeyFrame(_readOnly_);}'}},
-            { title: "Fwd",     icon: "chevron-right", toolTip: "Next Animation Frame", btnType: ButtonType.ClickButton,         funcs: {hidden: '!SelectionManager_selectedDocType(undefined, "freeform") || IsNoviceMode()'}, width: 20,  scripts: { onClick: 'nextKeyFrame(_readOnly_)'}},
-            { title: "Fill",    icon: "fill-drip",     toolTip: "Background Fill Color",btnType: ButtonType.ColorButton,         funcs: {hidden: '!SelectionManager_selectedDocType()'},                     ignoreClick: true, width: 20,  scripts: { script:  'return setBackgroundColor(value, _readOnly_)'}}, // Only when a document is selected
-            { title: "Header",  icon: "heading",       toolTip: "Header Color",         btnType: ButtonType.ColorButton,         funcs: {hidden: '!SelectionManager_selectedDocType()'},                     ignoreClick: true,             scripts: { script:  'return setHeaderColor(value, _readOnly_)'}},
-            { title: "Overlay", icon: "layer-group",   toolTip: "Overlay",              btnType: ButtonType.ToggleButton,        funcs: {hidden: '!SelectionManager_selectedDocType(undefined, "freeform", true)'},                         scripts: { onClick: 'toggleOverlay(_readOnly_)'}}, // Only when floating document is selected in freeform
-            { title: "Text",    icon: "Text",          toolTip: "Text functions",       subMenu: CurrentUserUtils.textTools(),   funcs: {hidden: 'false',                                                                        linearViewIsExpanded: `SelectionManager_selectedDocType("${DocumentType.RTF}")`} }, // Always available
-            { title: "Ink",     icon: "Ink",           toolTip: "Ink functions",        subMenu: CurrentUserUtils.inkTools(),    funcs: {hidden: 'false',                                                                        linearViewIsExpanded: `SelectionManager_selectedDocType("${DocumentType.INK}")`}, scripts: { onClick: 'setInkToolDefaults()'} }, // Always available
-            { title: "Web",     icon: "Web",           toolTip: "Web functions",        subMenu: CurrentUserUtils.webTools(),    funcs: {hidden: `!SelectionManager_selectedDocType("${DocumentType.WEB}")`,                     linearViewIsExpanded: `SelectionManager_selectedDocType("${DocumentType.WEB}")`,                     } }, // Only when Web is selected
-            { title: "Schema",  icon: "Schema",        toolTip: "Schema functions",     subMenu: CurrentUserUtils.schemaTools(), funcs: {hidden: `!SelectionManager_selectedDocType(undefined, "${CollectionViewType.Schema}")`, linearViewIsExpanded: `SelectionManager_selectedDocType(undefined, "${CollectionViewType.Schema}")`} } // Only when Schema is selected
+              title: "Perspective",                    toolTip: "View",                 btnType: ButtonType.DropdownList,                                                                                                                                    ignoreClick: true, width: 100, scripts: { script: 'setView(value, _readOnly_)'}},
+            { title: "Pin",     icon: "map-pin",       toolTip: "Pin View to Trail",    btnType: ButtonType.ClickButton,         expertMode: false, toolType:"tab",                       funcs: {hidden: `!SelectionManager_selectedDocType(self.toolType, self.expertMode)`}, width: 20,  scripts: { onClick: 'pinWithView(_readOnly_, altKey)'}},
+            { title: "Fill",    icon: "fill-drip",     toolTip: "Background Fill Color",btnType: ButtonType.ColorButton,         expertMode: false, toolType:"tab",                                                                                          ignoreClick: true, width: 20,  scripts: { script:  'return setBackgroundColor(value, _readOnly_)'}}, // Only when a document is selected
+            { title: "Header",  icon: "heading",       toolTip: "Header Color",         btnType: ButtonType.ColorButton,         expertMode: false, toolType:"tab",                                                                                          ignoreClick: true,             scripts: { script:  'return setHeaderColor(value, _readOnly_)'}},
+            { title: "Overlay", icon: "layer-group",   toolTip: "Overlay",              btnType: ButtonType.ToggleButton,        expertMode: false, toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectionManager_selectedDocType(self.toolType, self.expertMode, true)'},       scripts: { onClick: 'toggleOverlay(_readOnly_)'}}, // Only when floating document is selected in freeform
+            { title: "Back",    icon: "chevron-left",  toolTip: "Prev Animation Frame", btnType: ButtonType.ClickButton,         expertMode: true,  toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectionManager_selectedDocType(self.toolType, self.expertMode)'}, width: 20,  scripts: { onClick: 'prevKeyFrame(_readOnly_)'}},
+            { title: "Num", icon:"",toolTip: "Frame Number (click to toggle edit mode)",btnType: ButtonType.TextButton,          expertMode: true,  toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectionManager_selectedDocType(self.toolType, self.expertMode)', buttonText: 'selectedDocs()?.lastElement()?.currentFrame?.toString()'}, width: 20, scripts: { onClick: '{ return curKeyFrame(_readOnly_);}'}},
+            { title: "Fwd",     icon: "chevron-right", toolTip: "Next Animation Frame", btnType: ButtonType.ClickButton,         expertMode: true,  toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectionManager_selectedDocType(self.toolType, self.expertMode)'}, width: 20,  scripts: { onClick: 'nextKeyFrame(_readOnly_)'}},
+            { title: "Text",    icon: "Text",          toolTip: "Text functions",       subMenu: CurrentUserUtils.textTools(),   expertMode: false, toolType:DocumentType.RTF,            funcs: {                                                                             linearViewIsExpanded: `SelectionManager_selectedDocType(self.toolType, self.expertMode)`} }, // Always available
+            { title: "Ink",     icon: "Ink",           toolTip: "Ink functions",        subMenu: CurrentUserUtils.inkTools(),    expertMode: false, toolType:DocumentType.INK,            funcs: {                                                                             linearViewIsExpanded: `SelectionManager_selectedDocType(self.toolType, self.expertMode)`}, scripts: { onClick: 'setInkToolDefaults()'} }, // Always available
+            { title: "Web",     icon: "Web",           toolTip: "Web functions",        subMenu: CurrentUserUtils.webTools(),    expertMode: false, toolType:DocumentType.WEB,            funcs: {hidden: `!SelectionManager_selectedDocType(self.toolType, self.expertMode)`, linearViewIsExpanded: `SelectionManager_selectedDocType(self.toolType, self.expertMode)`} }, // Only when Web is selected
+            { title: "Schema",  icon: "Schema",        toolTip: "Schema functions",     subMenu: CurrentUserUtils.schemaTools(), expertMode: false, toolType:CollectionViewType.Schema,   funcs: {hidden: `!SelectionManager_selectedDocType(self.toolType, self.expertMode)`, linearViewIsExpanded: `SelectionManager_selectedDocType(self.toolType, self.expertMode)`} } // Only when Schema is selected
         ];
     }
 
@@ -684,6 +688,7 @@ export class CurrentUserUtils {
             color: Colors.WHITE, system: true, dontUndo: true, 
             _nativeWidth: params.width ?? 30, _width: params.width ?? 30,
             _height: 30, _nativeHeight: 30, 
+            toolType: params.toolType, expertMode: params.expertMode,
             _stayInCollection: true, _hideContextMenu: true, _lockedPosition: true,
             _removeDropProperties: new List<string>([ "_stayInCollection"]),
         };
diff --git a/src/client/util/Scripting.ts b/src/client/util/Scripting.ts
index 6dcdcb71b..d32298c83 100644
--- a/src/client/util/Scripting.ts
+++ b/src/client/util/Scripting.ts
@@ -248,8 +248,10 @@ export function CompileScript(script: string, options: ScriptOptions = {}): Comp
         ScriptingGlobals.resetScriptingGlobals();
     }
     !signature.includes('XXX') && ScriptField._scriptFieldCache.set(script + ':' + signature, result as CompiledScript);
-    //console.log('COMPILED: ' + script + ':' + signature);
     return result;
 }
 
 ScriptingGlobals.add(CompileScript);
+ScriptingGlobals.add(function runScript(self: Doc, script: ScriptField) {
+    return script?.script.run({ this: self, self: self }).result;
+});
diff --git a/src/client/util/SelectionManager.ts b/src/client/util/SelectionManager.ts
index c0fc25376..0f4f77588 100644
--- a/src/client/util/SelectionManager.ts
+++ b/src/client/util/SelectionManager.ts
@@ -104,10 +104,11 @@ export namespace SelectionManager {
         return Array.from(manager.SelectedViews.values()).filter(doc => doc?._viewType !== CollectionViewType.Docking);
     }
 }
-ScriptingGlobals.add(function SelectionManager_selectedDocType(docType?: DocumentType, colType?: CollectionViewType, checkContext?: boolean) {
-    if (colType === ('tab' as any)) {
+ScriptingGlobals.add(function SelectionManager_selectedDocType(type: string, expertMode: boolean, checkContext?: boolean) {
+    if (Doc.noviceMode && expertMode) return false;
+    if (type === 'tab') {
         return SelectionManager.Views().lastElement()?.props.renderDepth === 0;
     }
     let selected = (sel => (checkContext ? DocCast(sel?.context) : sel))(SelectionManager.SelectedSchemaDoc() ?? SelectionManager.Docs().lastElement());
-    return docType ? selected?.type === docType : colType ? selected?.viewType === colType : true;
+    return selected?.type === type || selected?.viewType === type || !type;
 });
diff --git a/src/client/util/SerializationHelper.ts b/src/client/util/SerializationHelper.ts
index 2d1f61cfb..76037a7e9 100644
--- a/src/client/util/SerializationHelper.ts
+++ b/src/client/util/SerializationHelper.ts
@@ -44,12 +44,8 @@ export namespace SerializationHelper {
         }
 
         if (!obj.__type) {
-            if (true || ClientUtils.RELEASE) {
-                console.warn("No property 'type' found in JSON.");
-                return undefined;
-            } else {
-                throw Error("No property 'type' found in JSON.");
-            }
+            console.warn("No property 'type' found in JSON.");
+            return undefined;
         }
 
         if (!(obj.__type in serializationTypes)) {
@@ -58,9 +54,8 @@ export namespace SerializationHelper {
 
         const type = serializationTypes[obj.__type];
         const value = await new Promise(res => deserialize(type.ctor, obj, (err, result) => res(result)));
-        if (type.afterDeserialize) {
-            type.afterDeserialize(value);
-        }
+        type.afterDeserialize?.(value);
+
         return value;
     }
 }
@@ -68,75 +63,20 @@ export namespace SerializationHelper {
 const serializationTypes: { [name: string]: { ctor: { new (): any }; afterDeserialize?: (obj: any) => void | Promise<any> } } = {};
 const reverseMap: { [ctor: string]: string } = {};
 
-export interface DeserializableOpts {
-    (constructor: { new (...args: any[]): any }): void;
-    withFields(fields: string[]): Function;
-}
-
-export function Deserializable(name: string, afterDeserialize?: (obj: any) => void | Promise<any>): DeserializableOpts;
-export function Deserializable(constructor: { new (...args: any[]): any }): void;
-export function Deserializable(constructor: { new (...args: any[]): any } | string, afterDeserialize?: (obj: any) => void): DeserializableOpts | void {
-    function addToMap(name: string, ctor: { new (...args: any[]): any }) {
+export function Deserializable(className: string, afterDeserialize?: (obj: any) => void | Promise<any>, constructorArgs?: [string]): (constructor: { new (...args: any[]): any }) => void {
+    function addToMap(className: string, ctor: { new (...args: any[]): any }) {
         const schema = getDefaultModelSchema(ctor) as any;
-        if (schema.targetClass !== ctor) {
-            const newSchema = { ...schema, factory: () => new ctor() };
-            setDefaultModelSchema(ctor, newSchema);
+        if (schema.targetClass !== ctor || constructorArgs) {
+            setDefaultModelSchema(ctor, { ...schema, factory: (context: any) => new ctor(...(constructorArgs ?? [])?.map(arg => context.json[arg])) });
         }
-        if (!(name in serializationTypes)) {
-            serializationTypes[name] = { ctor, afterDeserialize };
-            reverseMap[ctor.name] = name;
+        if (!(className in serializationTypes)) {
+            serializationTypes[className] = { ctor, afterDeserialize };
+            reverseMap[ctor.name] = className;
         } else {
-            throw new Error(`Name ${name} has already been registered as deserializable`);
+            throw new Error(`Name ${className} has already been registered as deserializable`);
         }
     }
-    if (typeof constructor === 'string') {
-        return Object.assign(
-            (ctor: { new (...args: any[]): any }) => {
-                addToMap(constructor, ctor);
-            },
-            { withFields: (fields: string[]) => Deserializable.withFields(fields, constructor, afterDeserialize) }
-        );
-    }
-    addToMap(constructor.name, constructor);
-}
-
-export namespace Deserializable {
-    export function withFields(fields: string[], name?: string, afterDeserialize?: (obj: any) => void | Promise<any>) {
-        return function (constructor: { new (...fields: any[]): any }) {
-            Deserializable(name || constructor.name, afterDeserialize)(constructor);
-            let schema = getDefaultModelSchema(constructor);
-            if (schema) {
-                schema.factory = context => {
-                    const args = fields.map(key => context.json[key]);
-                    return new constructor(...args);
-                };
-                // TODO A modified version of this would let us not reassign fields that we're passing into the constructor later on in deserializing
-                // fields.forEach(field => {
-                //     if (field in schema.props) {
-                //         let propSchema = schema.props[field];
-                //         if (propSchema === false) {
-                //             return;
-                //         } else if (propSchema === true) {
-                //             propSchema = primitive();
-                //         }
-                //         schema.props[field] = custom(propSchema.serializer,
-                //             () => {
-                //                 return SKIP;
-                //             });
-                //     }
-                // });
-            } else {
-                schema = {
-                    props: {},
-                    factory: context => {
-                        const args = fields.map(key => context.json[key]);
-                        return new constructor(...args);
-                    },
-                };
-                setDefaultModelSchema(constructor, schema);
-            }
-        };
-    }
+    return (ctor: { new (...args: any[]): any }) => addToMap(className, ctor);
 }
 
 export function autoObject(): PropSchema {
diff --git a/src/client/views/DashboardView.tsx b/src/client/views/DashboardView.tsx
index 7ebe8d0e3..2b586b0e2 100644
--- a/src/client/views/DashboardView.tsx
+++ b/src/client/views/DashboardView.tsx
@@ -233,7 +233,6 @@ export class DashboardView extends React.Component {
     /// this also sets the readonly state of the dashboard based on the current mode of dash (from its url)
     public static openDashboard = (doc: Doc | undefined, fromHistory = false) => {
         if (!doc) return false;
-        Doc.MainDocId = doc[Id];
         Doc.AddDocToList(Doc.MyDashboards, 'data', doc);
 
         // this has the side-effect of setting the main container since we're assigning the active/guest dashboard
diff --git a/src/client/views/GestureOverlay.tsx b/src/client/views/GestureOverlay.tsx
index 6058eaaf9..0feccb742 100644
--- a/src/client/views/GestureOverlay.tsx
+++ b/src/client/views/GestureOverlay.tsx
@@ -94,7 +94,7 @@ export class GestureOverlay extends Touchable<GestureOverlayProps> {
     }
 
     static setupThumbButtons(doc: Doc) {
-        const docProtoData: { title: string; icon: string; drag?: string; ignoreClick?: boolean; pointerDown?: string; pointerUp?: string; clipboard?: Doc; backgroundColor?: string; dragFactory?: Doc }[] = [
+        const docProtoData: { title: string; icon: string; drag?: string; toolType?: string; ignoreClick?: boolean; pointerDown?: string; pointerUp?: string; clipboard?: Doc; backgroundColor?: string; dragFactory?: Doc }[] = [
             { title: 'use pen', icon: 'pen-nib', pointerUp: 'resetPen()', pointerDown: 'setPen(2, this.backgroundColor)', backgroundColor: 'blue' },
             { title: 'use highlighter', icon: 'highlighter', pointerUp: 'resetPen()', pointerDown: 'setPen(20, this.backgroundColor)', backgroundColor: 'yellow' },
             {
@@ -105,8 +105,8 @@ export class GestureOverlay extends Touchable<GestureOverlayProps> {
                 clipboard: Docs.Create.FreeformDocument([], { _width: 300, _height: 300, system: true }),
                 backgroundColor: 'orange',
             },
-            { title: 'interpret text', icon: 'font', pointerUp: "setToolglass('none')", pointerDown: "setToolglass('inktotext')", backgroundColor: 'orange' },
-            { title: 'ignore gestures', icon: 'signature', pointerUp: "setToolglass('none')", pointerDown: "setToolglass('ignoregesture')", backgroundColor: 'green' },
+            { title: 'interpret text', icon: 'font', toolType: 'inktotext', pointerUp: "setToolglass('none')", pointerDown: 'setToolglass(self.toolType)', backgroundColor: 'orange' },
+            { title: 'ignore gestures', icon: 'signature', toolType: 'ignoregesture', pointerUp: "setToolglass('none')", pointerDown: 'setToolglass(self.toolType)', backgroundColor: 'green' },
         ];
         return docProtoData.map(data =>
             Docs.Create.FontIconDocument({
@@ -116,6 +116,7 @@ export class GestureOverlay extends Touchable<GestureOverlayProps> {
                 _height: 10,
                 title: data.title,
                 icon: data.icon,
+                toolType: data.toolType,
                 _dropAction: data.pointerDown ? 'copy' : undefined,
                 ignoreClick: data.ignoreClick,
                 onDragStart: data.drag ? ScriptField.MakeFunction(data.drag) : undefined,
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 945cd61db..2e04ca3dd 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -230,8 +230,6 @@ export class MainView extends React.Component {
         if (window.location.pathname !== '/home') {
             const pathname = window.location.pathname.substr(1).split('/');
             if (pathname.length > 1 && pathname[0] === 'doc') {
-                Doc.MainDocId = pathname[1];
-                //!this.userDoc &&
                 DocServer.GetRefField(pathname[1]).then(
                     action(field => {
                         if (field instanceof Doc && field._viewType !== CollectionViewType.Docking) {
@@ -482,7 +480,6 @@ export class MainView extends React.Component {
                 fa.faHandPointUp,
             ]
         );
-        this.initAuthenticationRouters();
     }
 
     globalPointerDown = action((e: PointerEvent) => {
@@ -521,23 +518,6 @@ export class MainView extends React.Component {
         document.oncontextmenu = () => false;
     };
 
-    initAuthenticationRouters = async () => {
-        const received = Doc.MainDocId;
-        if (received && !this.userDoc) {
-            reaction(
-                () => Doc.GuestTarget,
-                target => target && DashboardView.createNewDashboard(),
-                { fireImmediately: true }
-            );
-        }
-        // else {
-        //     PromiseValue(this.userDoc.activeDashboard).then(dash => {
-        //         if (dash instanceof Doc) DashboardView.openDashboard(dash);
-        //         else Doc.createNewDashboard();
-        //     });
-        // }
-    };
-
     @action
     createNewPresentation = () => {
         const pres = Doc.MakeCopy(Doc.UserDoc().emptyTrail as Doc, true);
diff --git a/src/client/views/OverlayView.tsx b/src/client/views/OverlayView.tsx
index 08285ff0c..34e8cd6dd 100644
--- a/src/client/views/OverlayView.tsx
+++ b/src/client/views/OverlayView.tsx
@@ -255,4 +255,5 @@ export class OverlayView extends React.Component {
 // bcz: ugh ... want to be able to pass ScriptingRepl as tag argument, but that doesn't seem to work.. runtime error
 ScriptingGlobals.add(function addOverlayWindow(type: string, options: OverlayElementOptions) {
     OverlayView.Instance.addWindow(<ScriptingRepl />, options);
+    addOverlayWindow('ScriptingRepl', { x: 300, y: 100, width: 200, height: 200, title: 'Scripting REPL' });
 });
diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index 1ead80bd0..9b6554d67 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -26,6 +26,9 @@ import { CollectionSubView, SubCollectionViewProps } from './CollectionSubView';
 import { TabDocView } from './TabDocView';
 import React = require('react');
 import { OpenWhere, OpenWhereMod } from '../nodes/DocumentView';
+import { OverlayView } from '../OverlayView';
+import { ScriptingRepl } from '../ScriptingRepl';
+import { ScriptField } from '../../../fields/ScriptField';
 const _global = (window /* browser */ || global) /* node */ as any;
 
 @observer
@@ -580,15 +583,21 @@ ScriptingGlobals.add(
     '(doc: any)'
 );
 ScriptingGlobals.add(
-    function openOnRight(doc: any) {
-        return CollectionDockingView.AddSplit(doc, OpenWhereMod.right);
+    function openDoc(doc: any, where: OpenWhere) {
+        switch (where) {
+            case OpenWhere.addRight:
+                return CollectionDockingView.AddSplit(doc, OpenWhereMod.right);
+            case OpenWhere.overlay:
+                if (doc === 'repl') OverlayView.Instance.addWindow(<ScriptingRepl />, { x: 300, y: 100, width: 200, height: 200, title: 'Scripting REPL' });
+                else Doc.AddDocToList(Doc.MyOverlayDocs, undefined, doc);
+        }
     },
-    'opens up document in tab on right side of the screen',
+    'opens up document in location specified',
     '(doc: any)'
 );
 ScriptingGlobals.add(
-    function openInOverlay(doc: any) {
-        return Doc.AddDocToList(Doc.MyOverlayDocs, undefined, doc);
+    function openRepl() {
+        return 'openRepl';
     },
     'opens up document in screen overlay layer',
     '(doc: any)'
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index 48e5748a0..aed88aa1a 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -169,7 +169,7 @@ export class CollectionView extends ViewBoxAnnotatableComponent<ViewBoxAnnotatab
 
     onContextMenu = (e: React.MouseEvent): void => {
         const cm = ContextMenu.Instance;
-        if (cm && !e.isPropagationStopped() && this.rootDoc[Id] !== Doc.MainDocId) {
+        if (cm && !e.isPropagationStopped()) {
             // need to test this because GoldenLayout causes a parallel hierarchy in the React DOM for its children and the main document view7
             this.setupViewTypes(
                 'UI Controls...',
diff --git a/src/client/views/collections/TreeView.tsx b/src/client/views/collections/TreeView.tsx
index 2bdcf472f..af2d148e0 100644
--- a/src/client/views/collections/TreeView.tsx
+++ b/src/client/views/collections/TreeView.tsx
@@ -2,7 +2,7 @@ import { IconProp } from '@fortawesome/fontawesome-svg-core';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { action, computed, IReactionDisposer, observable, reaction } from 'mobx';
 import { observer } from 'mobx-react';
-import { DataSym, Doc, DocListCast, DocListCastOrNull, Field, HeightSym, Opt, StrListCast, WidthSym } from '../../../fields/Doc';
+import { DataSym, Doc, DocListCast, Field, HeightSym, Opt, StrListCast, WidthSym } from '../../../fields/Doc';
 import { Id } from '../../../fields/FieldSymbols';
 import { List } from '../../../fields/List';
 import { RichTextField } from '../../../fields/RichTextField';
@@ -172,11 +172,7 @@ export class TreeView extends React.Component<TreeViewProps> {
 
     childDocList(field: string) {
         const layout = Cast(Doc.LayoutField(this.doc), Doc, null);
-        return (
-            (this.props.dataDoc ? DocListCastOrNull(this.props.dataDoc[field]) : undefined) || // if there's a data doc for an expanded template, use it's data field
-            (layout ? DocListCastOrNull(layout[field]) : undefined) || // else if there's a layout doc, display it's fields
-            DocListCastOrNull(this.doc[field])
-        ); // otherwise use the document's data field
+        return DocListCast(this.props.dataDoc?.[field], DocListCast(layout?.[field], DocListCast(this.doc[field])));
     }
     moving: boolean = false;
     @undoBatch move = (doc: Doc | Doc[], target: Doc | undefined, addDoc: (doc: Doc | Doc[]) => boolean) => {
@@ -759,7 +755,7 @@ export class TreeView extends React.Component<TreeViewProps> {
         const makeFolder = { script: ScriptField.MakeFunction(`scriptContext.makeFolder()`, { scriptContext: 'any' })!, icon: 'folder-plus', label: 'New Folder' };
         const deleteItem = { script: ScriptField.MakeFunction(`scriptContext.deleteItem()`, { scriptContext: 'any' })!, icon: 'folder-plus', label: 'Delete' };
         const folderOp = this.childDocs?.length ? [makeFolder] : [];
-        const openAlias = { script: ScriptField.MakeFunction(`openOnRight(getAlias(self))`)!, icon: 'copy', label: 'Open Alias' };
+        const openAlias = { script: ScriptField.MakeFunction(`openDoc(getAlias(self), ${OpenWhere.addRight})`)!, icon: 'copy', label: 'Open Alias' };
         const focusDoc = { script: ScriptField.MakeFunction(`DocFocusOrOpen(self)`)!, icon: 'eye', label: 'Focus or Open' };
         return [
             ...(this.props.contextMenuItems ?? []).filter(mi => (!mi.filter ? true : mi.filter.script.run({ doc: this.doc })?.result)),
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 964231e9d..02af30d0c 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -80,6 +80,7 @@ export enum OpenWhere {
     replaceLeft = 'replace:left',
     inParent = 'inParent',
     inParentFromScreen = 'inParentFromScreen',
+    overlay = 'overlay',
 }
 export enum OpenWhereMod {
     none = '',
@@ -612,6 +613,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
                                 altKey,
                                 shiftKey,
                                 ctrlKey,
+                                value: undefined,
                             },
                             console.log
                         );
diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx
index b1acef44a..f7425b26a 100644
--- a/src/client/views/nodes/WebBox.tsx
+++ b/src/client/views/nodes/WebBox.tsx
@@ -145,6 +145,9 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
         const nativeWidth = NumCast(this.layoutDoc.nativeWidth);
         const nativeHeight = (nativeWidth * this.props.PanelHeight()) / this.props.PanelWidth();
         if (
+            !this.props.isSelected(true) &&
+            !Doc.IsBrushedDegree(this.rootDoc) &&
+            !this.isAnyChildContentActive() &&
             !this.rootDoc.thumbLockout &&
             !this.props.dontRegisterView &&
             this._iframe &&
@@ -181,6 +184,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
                 });
         }
     };
+    _thumbTimer: any;
     async componentDidMount() {
         this.props.setContentView?.(this); // this tells the DocumentView that this WebBox is the "content" of the document.  this allows the DocumentView to call WebBox relevant methods to configure the UI (eg, show back/forward buttons)
 
@@ -197,6 +201,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
             () => this.props.isSelected(true) || this.isAnyChildContentActive() || Doc.isBrushedHighlightedDegree(this.props.Document),
             async selected => {
                 if (selected) {
+                    this._thumbTimer && clearTimeout(this._thumbTimer);
                     this._webPageHasBeenRendered = true;
                 } else if (
                     (!this.props.isContentActive(true) || SnappingManager.GetIsDragging()) && // update thumnail when unselected AND (no child annotation is active OR we've started dragging the document in which case no additional deselect will occur so this is the only chance to update the thumbnail)
@@ -204,7 +209,8 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
                     LightboxView.LightboxDoc !== this.rootDoc
                 ) {
                     // don't create a thumbnail if entering Lightbox from maximize either, since thumb will be empty.
-                    this.updateThumb();
+                    this._thumbTimer && clearTimeout(this._thumbTimer);
+                    this._thumbTimer = setTimeout(this.updateThumb, 2000);
                 }
             },
             { fireImmediately: this.props.isSelected(true) || this.isAnyChildContentActive() || (Doc.isBrushedHighlightedDegreeUnmemoized(this.props.Document) ? true : false) }
diff --git a/src/client/views/nodes/button/FontIconBox.tsx b/src/client/views/nodes/button/FontIconBox.tsx
index 0468d1c47..b3a3c3ae4 100644
--- a/src/client/views/nodes/button/FontIconBox.tsx
+++ b/src/client/views/nodes/button/FontIconBox.tsx
@@ -1,4 +1,5 @@
 import { IconProp } from '@fortawesome/fontawesome-svg-core';
+import { faAlignRight } from '@fortawesome/free-solid-svg-icons';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { Tooltip } from '@material-ui/core';
 import { action, computed, observable, runInAction } from 'mobx';
@@ -11,7 +12,7 @@ import { ScriptField } from '../../../../fields/ScriptField';
 import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from '../../../../fields/Types';
 import { WebField } from '../../../../fields/URLField';
 import { GestureUtils } from '../../../../pen-gestures/GestureUtils';
-import { aggregateBounds, StopEvent, Utils } from '../../../../Utils';
+import { aggregateBounds, Utils } from '../../../../Utils';
 import { CollectionViewType, DocumentType } from '../../../documents/DocumentTypes';
 import { LinkManager } from '../../../util/LinkManager';
 import { ScriptingGlobals } from '../../../util/ScriptingGlobals';
@@ -134,10 +135,10 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
     @computed get numberButton() {
         const numBtnType: string = StrCast(this.rootDoc.numBtnType);
         const numScript = ScriptCast(this.rootDoc.script);
-        const setValue = (value: number) => UndoManager.RunInBatch(() => numScript?.script.run({ value, _readOnly_: false }), 'set num value');
+        const setValue = (value: number) => UndoManager.RunInBatch(() => numScript?.script.run({ self: this.rootDoc, value, _readOnly_: false }), 'set num value');
 
         // Script for checking the outcome of the toggle
-        const checkResult = Number(numScript?.script.run({ value: 0, _readOnly_: true }).result ?? 0).toPrecision(NumCast(this.dataDoc.numPrecision, 3));
+        const checkResult = Number(numScript?.script.run({ self: this.rootDoc, value: 0, _readOnly_: true }).result ?? 0).toPrecision(NumCast(this.dataDoc.numPrecision, 3));
 
         const label = !FontIconBox.GetShowLabels() ? null : <div className="fontIconBox-label">{this.label}</div>;
 
@@ -150,7 +151,7 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
                         min={NumCast(this.rootDoc.numBtnMin, 0)}
                         max={NumCast(this.rootDoc.numBtnMax, 100)}
                         value={checkResult}
-                        className={'menu-slider'}
+                        className="menu-slider"
                         onPointerDown={() => (this._batch = UndoManager.StartBatch('presDuration'))}
                         onPointerUp={() => this._batch?.end()}
                         onChange={e => {
@@ -284,27 +285,23 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
                     text = 'User Default';
                 }
                 noviceList = [CollectionViewType.Freeform, CollectionViewType.Schema, CollectionViewType.Stacking, CollectionViewType.NoteTaking];
-            } else if (script?.script.originalScript.startsWith('setFont')) {
-                const editorView = RichTextMenu.Instance?.TextView?.EditorView;
-                text = StrCast((editorView ? RichTextMenu.Instance : Doc.UserDoc()).fontFamily);
-                noviceList = ['Roboto', 'Times New Roman', 'Arial', 'Georgia', 'Comic Sans MS', 'Tahoma', 'Impact', 'Crimson Text'];
-            }
+            } else text = StrCast((RichTextMenu.Instance?.TextView?.EditorView ? RichTextMenu.Instance : Doc.UserDoc()).fontFamily);
         } catch (e) {
             console.log(e);
         }
 
         // Get items to place into the list
         const list = this.buttonList
-            .filter(value => !Doc.noviceMode || noviceList.includes(value))
+            .filter(value => !Doc.noviceMode || !noviceList.length || noviceList.includes(value))
             .map(value => (
                 <div
                     className="list-item"
                     key={`${value}`}
                     style={{
-                        fontFamily: script.script.originalScript.startsWith('setFont') ? value : undefined,
+                        fontFamily: script.script.originalScript.startsWith('{ return setFont') ? value : undefined,
                         backgroundColor: value === text ? Colors.LIGHT_BLUE : undefined,
                     }}
-                    onClick={undoBatch(() => script.script.run({ value }))}>
+                    onClick={undoBatch(() => script.script.run({ self: this.rootDoc, value }))}>
                     {value[0].toUpperCase() + value.slice(1)}
                 </div>
             ));
@@ -357,7 +354,7 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
             ev.preventDefault();
             ev.stopPropagation();
             const s = this.colorScript;
-            s && undoBatch(() => s.script.run({ value: Utils.colorString(value), _readOnly_: false }).result)();
+            s && undoBatch(() => s.script.run({ self: this.rootDoc, value: Utils.colorString(value), _readOnly_: false }).result)();
         };
         const presets = ['#D0021B', '#F5A623', '#F8E71C', '#8B572A', '#7ED321', '#417505', '#9013FE', '#4A90E2', '#50E3C2', '#B8E986', '#000000', '#4A4A4A', '#9B9B9B', '#FFFFFF', '#f1efeb', 'transparent'];
         return <SketchPicker onChange={change as any /* SketchPicker passes the mouse event to the callback, but the type system doesn't know that */} color={curColor} presetColors={presets} />;
@@ -368,7 +365,7 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
     @computed get colorButton() {
         const color = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.Color);
         const backgroundColor = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor);
-        const curColor = this.colorScript?.script.run({ value: undefined, _readOnly_: true }).result ?? 'transparent';
+        const curColor = this.colorScript?.script.run({ self: this.rootDoc, value: undefined, _readOnly_: true }).result ?? 'transparent';
 
         const label =
             !this.label || !FontIconBox.GetShowLabels() ? null : (
@@ -377,13 +374,6 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
                 </div>
             );
 
-        // dropdown caret seems superfluous since clicking the color button does the same thing
-        // const dropdownCaret = <div
-        //     className="menuButton-dropDown"
-        //     style={{ borderBottomRightRadius: this.dropdown ? 0 : undefined }}>
-        //     <FontAwesomeIcon icon={'caret-down'} color={color} size="sm" />
-        // </div>;
-        //setTimeout(() => this.colorPicker(curColor)); // cause an update to the color picker rendered in MainView
         return (
             <div
                 className={`menuButton ${this.type + (FontIconBox.GetShowLabels() ? 'Label' : '')} ${this.colorPickerClosed}`}
@@ -598,121 +588,70 @@ ScriptingGlobals.add(function toggleOverlay(checkResult?: boolean) {
     selected ? selected.props.CollectionFreeFormDocumentView?.().float() : console.log('[FontIconBox.tsx] toggleOverlay failed');
 });
 
-/** TEXT
- *      setFont
- *      setFontSize
- *      toggleBold
- *      toggleUnderline
- *      toggleItalic
- *      setAlignment
- *      toggleBold
- *      toggleItalic
- *      toggleUnderline
- **/
-
-// toggle: Set overlay status of selected document
-ScriptingGlobals.add(function setFont(font: string, checkResult?: boolean) {
-    if (checkResult) return RichTextMenu.Instance?.fontFamily;
-    font && RichTextMenu.Instance.setFontFamily(font);
-});
-
-ScriptingGlobals.add(function getActiveTextInfo(info: 'family' | 'size' | 'color' | 'highlight') {
-    const editorView = RichTextMenu.Instance.TextView?.EditorView;
-    const style = editorView?.state && RichTextMenu.Instance.getActiveFontStylesOnSelection();
-    // prettier-ignore
-    switch (info) {
-        case 'family':    return style?.activeFamilies[0];
-        case 'size':      return style?.activeSizes[0];
-        case 'color':     return style?.activeColors[0];
-        case 'highlight': return style?.activeHighlights[0];
-    }
-});
-
-ScriptingGlobals.add(function setAlignment(align: 'left' | 'right' | 'center', checkResult?: boolean) {
-    const editorView = RichTextMenu.Instance?.TextView?.EditorView;
-    if (checkResult) {
-        return (editorView ? RichTextMenu.Instance.textAlign : Doc.UserDoc().textAlign) === align ? Colors.MEDIUM_BLUE : 'transparent';
-    }
-    if (editorView?.state) RichTextMenu.Instance.align(editorView, editorView.dispatch, align);
-    else Doc.UserDoc().textAlign = align;
-});
-
-ScriptingGlobals.add(function setBulletList(mapStyle: 'bullet' | 'decimal', checkResult?: boolean) {
+ScriptingGlobals.add(function setFontAttr(attr: 'font' | 'fontColor' | 'highlight' | 'fontSize', value: any, checkResult?: boolean) {
     const editorView = RichTextMenu.Instance?.TextView?.EditorView;
-    if (checkResult) {
-        const active = editorView?.state && RichTextMenu.Instance.getActiveListStyle();
-        if (active === mapStyle) return Colors.MEDIUM_BLUE;
-        return 'transparent';
-    }
-    editorView?.state && RichTextMenu.Instance.changeListType(mapStyle);
-});
-
-// toggle: Set overlay status of selected document
-ScriptingGlobals.add(function setFontColor(color?: string, checkResult?: boolean) {
-    if (checkResult) return RichTextMenu.Instance.fontColor;
-    color && RichTextMenu.Instance.setColor(color);
-});
-
-// toggle: Set overlay status of selected document
-ScriptingGlobals.add(function setFontHighlight(color?: string, checkResult?: boolean) {
     const selected = SelectionManager.Docs().lastElement();
-    const editorView = RichTextMenu.Instance.TextView?.EditorView;
+    // prettier-ignore
+    const map: Map<'font'|'fontColor'|'highlight'|'fontSize', { checkResult: () => any; setDoc: () => void; setMode?: () => void }> = new Map([
+        ['font', {
+            checkResult: () => RichTextMenu.Instance?.fontFamily,
+            setDoc: () => value && RichTextMenu.Instance.setFontFamily(value),
+            setMode: () => Doc.UserDoc().textAlign = value,
+        }],
+        ['highlight', {
+            checkResult: () =>(selected ?? Doc.UserDoc())._fontHighlight,
+            setDoc: () => value  && RichTextMenu.Instance.setHighlight(value),
+        }],
+        ['fontColor', {
+            checkResult: () => RichTextMenu.Instance?.fontColor,
+            setDoc: () => value && RichTextMenu.Instance.setColor(value),
+        }],
+        ['fontSize', {
+            checkResult: () => RichTextMenu.Instance?.fontSize.replace('px', ''),
+            setDoc: () => {
+                if (typeof value === 'number') value = value.toString();
+                if (value && Number(value).toString() === value) value += 'px';
+                RichTextMenu.Instance.setFontSize(value);
+            },
+        }],
+    ]);
 
     if (checkResult) {
-        return (selected ?? Doc.UserDoc())._fontHighlight;
+        return map.get(attr)?.checkResult();
     }
-    color && RichTextMenu.Instance.setHighlight(color);
+    if (editorView?.state) map.get(attr)?.setDoc();
+    else map.get(attr)?.setMode?.();
 });
 
-// toggle: Set overlay status of selected document
-ScriptingGlobals.add(function setFontSize(size: string | number, checkResult?: boolean) {
-    if (checkResult) {
-        return RichTextMenu.Instance?.fontSize.replace('px', '');
-    }
-    if (typeof size === 'number') size = size.toString();
-    if (size && Number(size).toString() === size) size += 'px';
-    RichTextMenu.Instance.setFontSize(size);
-});
-ScriptingGlobals.add(function toggleNoAutoLinkAnchor(checkResult?: boolean) {
-    const editorView = RichTextMenu.Instance?.TextView?.EditorView;
-    if (checkResult) {
-        return (editorView ? RichTextMenu.Instance.noAutoLink : false) ? Colors.MEDIUM_BLUE : 'transparent';
-    }
-    if (editorView) RichTextMenu.Instance?.toggleNoAutoLinkAnchor();
-});
-ScriptingGlobals.add(function toggleDictation(checkResult?: boolean) {
+type attrname = 'noAutoLink' | 'dictation' | 'bold' | 'italics' | 'underline' | 'left' | 'center' | 'right' | 'bullet' | 'decimal';
+type attrfuncs = [attrname, { checkResult: () => boolean; toggle: () => any }];
+ScriptingGlobals.add(function toggleCharStyle(charStyle: attrname, checkResult?: boolean) {
     const textView = RichTextMenu.Instance?.TextView;
-    if (checkResult) {
-        return textView?._recording ? Colors.MEDIUM_BLUE : 'transparent';
-    }
-    if (textView) runInAction(() => (textView._recording = !textView._recording));
-});
-
-ScriptingGlobals.add(function toggleBold(checkResult?: boolean) {
-    const editorView = RichTextMenu.Instance?.TextView?.EditorView;
-    if (checkResult) {
-        return (editorView ? RichTextMenu.Instance.bold : Doc.UserDoc().fontWeight === 'bold') ? Colors.MEDIUM_BLUE : 'transparent';
-    }
-    if (editorView) RichTextMenu.Instance?.toggleBold();
-    else Doc.UserDoc().fontWeight = Doc.UserDoc().fontWeight === 'bold' ? undefined : 'bold';
-});
-
-ScriptingGlobals.add(function toggleUnderline(checkResult?: boolean) {
-    const editorView = RichTextMenu.Instance?.TextView?.EditorView;
-    if (checkResult) {
-        return (editorView ? RichTextMenu.Instance.underline : Doc.UserDoc().textDecoration === 'underline') ? Colors.MEDIUM_BLUE : 'transparent';
-    }
-    if (editorView) RichTextMenu.Instance?.toggleUnderline();
-    else Doc.UserDoc().textDecoration = Doc.UserDoc().textDecoration === 'underline' ? undefined : 'underline';
-});
-
-ScriptingGlobals.add(function toggleItalic(checkResult?: boolean) {
-    const editorView = RichTextMenu.Instance?.TextView?.EditorView;
-    if (checkResult) {
-        return (editorView ? RichTextMenu.Instance.italics : Doc.UserDoc().fontStyle === 'italics') ? Colors.MEDIUM_BLUE : 'transparent';
-    }
-    if (editorView) RichTextMenu.Instance?.toggleItalics();
-    else Doc.UserDoc().fontStyle = Doc.UserDoc().fontStyle === 'italics' ? undefined : 'italics';
+    const editorView = textView?.EditorView;
+    // prettier-ignore
+    const alignments:attrfuncs[] = (['left','right','center'] as ("left"|"center"|"right")[]).map((where) =>
+        [ where, {      checkResult: () =>(editorView ? (RichTextMenu.Instance.textAlign ===where): (Doc.UserDoc().textAlign ===where) ? true:false),
+                        toggle: () => (editorView?.state ? RichTextMenu.Instance.align(editorView, editorView.dispatch, where):(Doc.UserDoc().textAlign = where))}]);
+    // prettier-ignore
+    const listings:attrfuncs[] = (['bullet','decimal'] as attrname[]).map(list =>
+        [ list, {       checkResult: () => (editorView ? RichTextMenu.Instance.getActiveListStyle() === list:false),
+                        toggle: () => editorView?.state && RichTextMenu.Instance.changeListType(list) }]);
+    // prettier-ignore
+    const attrs:attrfuncs[] = [
+        ['dictation', { checkResult: () => textView?._recording ? true:false,
+                        toggle: () => textView && runInAction(() => (textView._recording = !textView._recording)) }],
+        ['noAutoLink',{ checkResult: () => (editorView ? RichTextMenu.Instance.noAutoLink : false),
+                        toggle: () => editorView && RichTextMenu.Instance?.toggleNoAutoLinkAnchor()}],
+        ['bold',      { checkResult: () => (editorView ? RichTextMenu.Instance.bold : (Doc.UserDoc().fontWeight === 'bold') ? true:false),
+                        toggle: editorView ? RichTextMenu.Instance.toggleBold : () => (Doc.UserDoc().fontWeight = Doc.UserDoc().fontWeight === 'bold' ? undefined : 'bold')}],
+        ['italics',   { checkResult: () => (editorView ? RichTextMenu.Instance.italics : (Doc.UserDoc().fontStyle === 'italics') ? true:false),
+                        toggle: editorView ? RichTextMenu.Instance.toggleItalics : () => (Doc.UserDoc().fontStyle = Doc.UserDoc().fontStyle === 'italics' ? undefined : 'italics')}],
+        ['underline', { checkResult: () => (editorView ? RichTextMenu.Instance.underline : (Doc.UserDoc().textDecoration === 'underline') ? true:false),
+                        toggle: editorView ? RichTextMenu.Instance.toggleUnderline : () => (Doc.UserDoc().textDecoration = Doc.UserDoc().textDecoration === 'underline' ? undefined : 'underline') }]]
+
+    const map = new Map(attrs.concat(alignments).concat(listings));
+    if (checkResult) return map.get(charStyle)?.checkResult() ? Colors.MEDIUM_BLUE : 'transparent';
+    map.get(charStyle)?.toggle();
 });
 
 export function checkInksToGroup() {
@@ -832,62 +771,39 @@ 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 setIsInkMask(checkResult?: boolean) {
-    const selected = SelectionManager.Docs().lastElement();
-    if (checkResult) {
-        if (selected?.type === DocumentType.INK) {
-            return BoolCast(selected.isInkMask) ? Colors.MEDIUM_BLUE : 'transparent';
-        }
-        return ActiveIsInkMask() ? Colors.MEDIUM_BLUE : 'transparent';
-    }
-    SetActiveIsInkMask(!ActiveIsInkMask());
-    SelectionManager.Docs()
-        .filter(doc => doc.type === DocumentType.INK)
-        .map(doc => (doc.isInkMask = !doc.isInkMask));
-});
-
-// toggle: Set overlay status of selected document
-ScriptingGlobals.add(function setFillColor(color?: string, checkResult?: boolean) {
+ScriptingGlobals.add(function setInkProperty(option: 'inkMask' | 'fillColor' | 'strokeWidth' | 'strokeColor', value: any, checkResult?: boolean) {
     const selected = SelectionManager.Docs().lastElement();
-    if (checkResult) {
-        if (selected?.type === DocumentType.INK) {
-            return StrCast(selected.fillColor);
-        }
-        return ActiveFillColor();
-    }
-    SetActiveFillColor(StrCast(color));
-    SelectionManager.Docs()
-        .filter(doc => doc.type === DocumentType.INK)
-        .map(doc => (doc.fillColor = color));
-});
-
-ScriptingGlobals.add(function setStrokeWidth(width: number, checkResult?: boolean) {
-    if (checkResult) {
-        const selected = SelectionManager.Docs().lastElement();
-        if (selected?.type === DocumentType.INK) {
-            return NumCast(selected.strokeWidth);
-        }
-        return ActiveInkWidth();
-    }
-    SetActiveInkWidth(width.toString());
-    SelectionManager.Docs()
-        .filter(doc => doc.type === DocumentType.INK)
-        .map(doc => (doc.strokeWidth = Number(width)));
-});
+    // prettier-ignore
+    const map: Map<'inkMask' | 'fillColor' | 'strokeWidth' | 'strokeColor', { checkResult: () => any; setInk: (doc: Doc) => void; setMode: () => void }> = new Map([
+        ['inkMask', {
+            checkResult: () => ((selected?.type === DocumentType.INK ? BoolCast(selected.isInkMask) : ActiveIsInkMask()) ? Colors.MEDIUM_BLUE : 'transparent'),
+            setInk: (doc: Doc) => (doc.isInkMask = !doc.isInkMask),
+            setMode: () => selected?.type !== DocumentType.INK && SetActiveIsInkMask(!ActiveIsInkMask()),
+        }],
+        ['fillColor', {
+            checkResult: () => (selected?.type === DocumentType.INK ? StrCast(selected.fillColor) : ActiveFillColor() ? Colors.MEDIUM_BLUE : 'transparent'),
+            setInk: (doc: Doc) => (doc.fillColor = StrCast(value)),
+            setMode: () => SetActiveFillColor(StrCast(value)),
+        }],
+        [ 'strokeWidth', {
+            checkResult: () => (selected?.type === DocumentType.INK ? NumCast(selected.strokeWidth) : ActiveInkWidth()),
+            setInk: (doc: Doc) => (doc.strokeWidth = NumCast(value)),
+            setMode: () => SetActiveInkWidth(value.toString()),
+        }],
+        ['strokeColor', {
+            checkResult: () => (selected?.type === DocumentType.INK ? NumCast(selected.color) : ActiveInkColor()),
+            setInk: (doc: Doc) => (doc.color = String(value)),
+            setMode: () => SetActiveInkColor(StrCast(value)),
+        }],
+    ]);
 
-// toggle: Set overlay status of selected document
-ScriptingGlobals.add(function setStrokeColor(color?: string, checkResult?: boolean) {
     if (checkResult) {
-        const selected = SelectionManager.Docs().lastElement();
-        if (selected?.type === DocumentType.INK) {
-            return StrCast(selected.color);
-        }
-        return ActiveInkColor();
+        return map.get(option)?.checkResult();
     }
-    SetActiveInkColor(StrCast(color));
+    map.get(option)?.setMode();
     SelectionManager.Docs()
         .filter(doc => doc.type === DocumentType.INK)
-        .map(doc => (doc.color = String(color)));
+        .map(doc => map.get(option)?.setInk(doc));
 });
 
 /** WEB
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index ee164ab31..de94ed5db 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -33,20 +33,19 @@ export namespace Field {
         return !Field.IsField(field) ? '' : (onDelegate ? '=' : '') + (field instanceof ComputedField ? `:=${field.script.originalScript}` : Field.toScriptString(field));
     }
     export function toScriptString(field: Field): string {
-        if (typeof field === 'string') {
-            if (field.startsWith('{"')) return `'${field}'`; // bcz: hack ... want to quote the string the right way. if there are nested "'s, then use ' instead of ".  In this case, test for the start of a JSON string of the format {"property": ... } and use outer 's instead of "s
-            return `"${field}"`;
+        switch (typeof field) {
+            case 'string':
+                if (field.startsWith('{"')) return `'${field}'`; // bcz: hack ... want to quote the string the right way. if there are nested "'s, then use ' instead of ".  In this case, test for the start of a JSON string of the format {"property": ... } and use outer 's instead of "s
+                return `"${field}"`;
+            case 'number':
+            case 'boolean':
+                return String(field);
         }
-        if (typeof field === 'number' || typeof field === 'boolean') return String(field);
-        if (field === undefined || field === null) return 'null';
-        return field[ToScriptString]();
+        return field?.[ToScriptString]?.() ?? 'null';
     }
     export function toString(field: Field): string {
-        if (typeof field === 'string') return field;
-        if (typeof field === 'number' || typeof field === 'boolean') return String(field);
-        if (field instanceof ObjectField) return field[ToString]();
-        if (field instanceof RefField) return field[ToString]();
-        return '';
+        if (typeof field === 'string' || typeof field === 'number' || typeof field === 'boolean') return String(field);
+        return field?.[ToString]?.() || '';
     }
     export function IsField(field: any): field is Field;
     export function IsField(field: any, includeUndefined: true): field is Field | undefined;
@@ -79,17 +78,14 @@ export async function DocCastAsync(field: FieldResult): Promise<Opt<Doc>> {
     return Cast(field, Doc);
 }
 
-export function NumListCast(field: FieldResult) {
-    return Cast(field, listSpec('number'), []);
+export function NumListCast(field: FieldResult, defaultVal: number[] = []) {
+    return Cast(field, listSpec('number'), defaultVal);
 }
-export function StrListCast(field: FieldResult) {
-    return Cast(field, listSpec('string'), []);
+export function StrListCast(field: FieldResult, defaultVal: string[] = []) {
+    return Cast(field, listSpec('string'), defaultVal);
 }
-export function DocListCast(field: FieldResult) {
-    return Cast(field, listSpec(Doc), []).filter(d => d instanceof Doc) as Doc[];
-}
-export function DocListCastOrNull(field: FieldResult) {
-    return Cast(field, listSpec(Doc), null)?.filter(d => d instanceof Doc) as Doc[] | undefined;
+export function DocListCast(field: FieldResult, defaultVal: Doc[] = []) {
+    return Cast(field, listSpec(Doc), defaultVal).filter(d => d instanceof Doc) as Doc[];
 }
 
 export const WidthSym = Symbol('Width');
@@ -153,17 +149,8 @@ export function updateCachedAcls(doc: Doc) {
 }
 
 @scriptingGlobal
-@Deserializable('Doc', updateCachedAcls).withFields(['id'])
+@Deserializable('Doc', updateCachedAcls, ['id'])
 export class Doc extends RefField {
-    //TODO tfs: these should be temporary...
-    private static mainDocId: string | undefined;
-    public static get MainDocId() {
-        return this.mainDocId;
-    }
-    public static set MainDocId(id: string | undefined) {
-        this.mainDocId = id;
-    }
-
     @observable public static CurrentlyLoading: Doc[];
     // removes from currently loading display
     @action
@@ -582,21 +569,13 @@ export namespace Doc {
 
     // compare whether documents or their protos match
     export function AreProtosEqual(doc?: Doc, other?: Doc) {
-        if (!doc || !other) return false;
-        const r = doc === other;
-        const r2 = Doc.GetProto(doc) === other;
-        const r3 = Doc.GetProto(other) === doc;
-        const r4 = Doc.GetProto(doc) === Doc.GetProto(other) && Doc.GetProto(other) !== undefined;
-        return r || r2 || r3 || r4;
+        return doc && other && Doc.GetProto(doc) === Doc.GetProto(other);
     }
 
     // Gets the data document for the document.  Note: this is mis-named -- it does not specifically
     // return the doc's proto, but rather recursively searches through the proto inheritance chain
     // and returns the document who's proto is undefined or whose proto is marked as a base prototype ('isPrototype').
     export function GetProto(doc: Doc): Doc {
-        if (doc instanceof Promise) {
-            // console.log("GetProto: warning: got Promise insead of Doc");
-        }
         const proto = doc && (Doc.GetT(doc, 'isPrototype', 'boolean', true) ? doc : doc.proto || doc);
         return proto === doc ? proto : Doc.GetProto(proto);
     }
diff --git a/src/fields/ScriptField.ts b/src/fields/ScriptField.ts
index b5eca78dd..feb419597 100644
--- a/src/fields/ScriptField.ts
+++ b/src/fields/ScriptField.ts
@@ -103,30 +103,9 @@ export class ScriptField extends ObjectField {
         }
         this.rawscript = rawscript;
         this.setterscript = setterscript;
-        this.script = script ?? (CompileScript('false', { addReturn: true }) as CompiledScript);
+        this.script = script ?? ScriptField.GetScriptFieldCache('false:') ?? (CompileScript('false', { addReturn: true }) as CompiledScript);
     }
 
-    //     init(callback: (res: Field) => any) {
-    //         const options = this.options!;
-    //         const keys = Object.keys(options.options.capturedIds);
-    //         Server.GetFields(keys).then(fields => {
-    //             let captured: { [name: string]: Field } = {};
-    //             keys.forEach(key => captured[options.options.capturedIds[key]] = fields[key]);
-    //             const opts: ScriptOptions = {
-    //                 addReturn: options.options.addReturn,
-    //                 params: options.options.params,
-    //                 requiredType: options.options.requiredType,
-    //                 capturedVariables: captured
-    //             };
-    //             const script = CompileScript(options.script, opts);
-    //             if (!script.compiled) {
-    //                 throw new Error("Can't compile script");
-    //             }
-    //             this._script = script;
-    //             callback(this);
-    //         });
-    //     }
-
     [Copy](): ObjectField {
         return new ScriptField(this.script, this.setterscript, this.rawscript);
     }
@@ -172,7 +151,7 @@ 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.run({ this: doc, self: Cast(doc.rootDocument, Doc, null) || doc, _last_: this._lastComputedResult, _readOnly_: true }, console.log).result);
+    _valueOutsideReaction = (doc: Doc) => (this._lastComputedResult = this.script.run({ this: doc, self: Cast(doc.rootDocument, Doc, null) ?? doc, value: '', _last_: this._lastComputedResult, _readOnly_: true }, console.log).result);
 
     [ToValue](doc: Doc) {
         return ComputedField.toValue(doc, this);
@@ -181,12 +160,8 @@ export class ComputedField extends ScriptField {
         return new ComputedField(this.script, this.setterscript, this.rawscript);
     }
 
-    public static MakeScript(script: string, params: object = {}) {
-        const compiled = ScriptField.CompileScript(script, params, false);
-        return compiled.compiled ? new ComputedField(compiled) : undefined;
-    }
     public static MakeFunction(script: string, params: object = {}, capturedVariables?: { [name: string]: Doc | string | number | boolean }, setterscript?: string) {
-        const compiled = ScriptField.CompileScript(script, params, true, capturedVariables);
+        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;
diff --git a/src/fields/util.ts b/src/fields/util.ts
index 6ff13d5d3..70d9ed61f 100644
--- a/src/fields/util.ts
+++ b/src/fields/util.ts
@@ -48,15 +48,11 @@ export function TraceMobx() {
 }
 
 const _setterImpl = action(function (target: any, prop: string | symbol | number, value: any, receiver: any): boolean {
-    if (SerializationHelper.IsSerializing()) {
+    if (SerializationHelper.IsSerializing() || typeof prop === 'symbol') {
         target[prop] = value;
         return true;
     }
 
-    if (typeof prop === 'symbol') {
-        target[prop] = value;
-        return true;
-    }
     if (value !== undefined) {
         value = value[SelfProxy] || value;
     }
@@ -95,12 +91,8 @@ const _setterImpl = action(function (target: any, prop: string | symbol | number
             delete target.__fields[prop];
         } else {
             target.__fieldKeys && (target.__fieldKeys[prop] = true);
-            // if (target.__fields[prop] !== value) {
-            //     console.log("ASSIGN " + prop + " " + value);
-            // }
             target.__fields[prop] = value;
         }
-        //if (typeof value === "object" && !(value instanceof ObjectField)) debugger;
 
         if (writeToServer) {
             if (value === undefined) target[Update]({ $unset: { ['fields.' + prop]: '' } });
@@ -143,13 +135,6 @@ export function denormalizeEmail(email: string) {
     return email.replace(/__/g, '.');
 }
 
-// playground mode allows the user to add/delete documents or make layout changes without them saving to the server
-// let playgroundMode = false;
-
-// export function togglePlaygroundMode() {
-//     playgroundMode = !playgroundMode;
-// }
-
 /**
  * Copies parent's acl fields to the child
  */
@@ -317,7 +302,6 @@ export function setter(target: any, in_prop: string | symbol | number, value: an
     if (effectiveAcl !== AclEdit && effectiveAcl !== AclAdmin && !(effectiveAcl === AclSelfEdit && value instanceof RichTextField)) return true;
     // if you're trying to change an acl but don't have Admin access / you're trying to change it to something that isn't an acceptable acl, you can't
     if (typeof prop === 'string' && prop.startsWith('acl') && (effectiveAcl !== AclAdmin || ![...Object.values(SharingPermissions), undefined].includes(value))) return true;
-    // if (typeof prop === "string" && prop.startsWith("acl") && !["Can Edit", "Can Augment", "Can View", "Not Shared", undefined].includes(value)) return true;
 
     if (typeof prop === 'string' && prop !== '__id' && prop !== '__fields' && prop.startsWith('_')) {
         if (!prop.startsWith('__')) prop = prop.substring(1);
@@ -377,9 +361,9 @@ export function getField(target: any, prop: string | number, ignoreProto: boolea
 export function deleteProperty(target: any, prop: string | number | symbol) {
     if (typeof prop === 'symbol') {
         delete target[prop];
-        return true;
+    } else {
+        target[SelfProxy][prop] = undefined;
     }
-    target[SelfProxy][prop] = undefined;
     return true;
 }
 
-- 
cgit v1.2.3-70-g09d2


From 8aeab8c3f25c556e39f3e9e58f9c321e79459df8 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Fri, 17 Mar 2023 12:22:03 -0400
Subject: fixed scriptingbox to remove script if text is empty.  fixed
 scripting with capturedvariables not to cache scripts with lists of captured
 documents.   fixed runtime warnings with stackedTimelined and AudioBox

---
 src/client/util/Scripting.ts                             |  8 +++++++-
 src/client/views/DocumentButtonBar.tsx                   |  2 +-
 src/client/views/MainView.tsx                            |  3 +--
 src/client/views/StyleProvider.tsx                       |  2 +-
 src/client/views/TemplateMenu.tsx                        |  1 -
 .../views/collections/CollectionStackedTimeline.tsx      |  2 +-
 src/client/views/nodes/AudioBox.tsx                      | 10 +++-------
 src/client/views/nodes/DocumentView.tsx                  |  4 ----
 src/client/views/nodes/ScriptingBox.tsx                  | 16 +++++++++-------
 src/fields/Doc.ts                                        |  9 +--------
 10 files changed, 24 insertions(+), 33 deletions(-)

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

diff --git a/src/client/util/Scripting.ts b/src/client/util/Scripting.ts
index d32298c83..f17a98616 100644
--- a/src/client/util/Scripting.ts
+++ b/src/client/util/Scripting.ts
@@ -7,7 +7,9 @@
 import * as typescriptlib from '!!raw-loader!./type_decls.d';
 import * as ts from 'typescript';
 import { Doc, Field } from '../../fields/Doc';
+import { ToScriptString } from '../../fields/FieldSymbols';
 import { ObjectField } from '../../fields/ObjectField';
+import { RefField } from '../../fields/RefField';
 import { ScriptField } from '../../fields/ScriptField';
 import { scriptingGlobals, ScriptingGlobals } from './ScriptingGlobals';
 export { ts };
@@ -180,7 +182,11 @@ function forEachNode(node: ts.Node, onEnter: Traverser, onExit?: Traverser, inde
 
 export function CompileScript(script: string, options: ScriptOptions = {}): CompileResult {
     const captured = options.capturedVariables ?? {};
-    const signature = Object.keys(captured).reduce((p, v) => p + `${v}=${captured[v] instanceof ObjectField ? 'XXX' : captured[v].toString()}`, '');
+    const signature = Object.keys(captured).reduce((p, v) => {
+        const formatCapture = (obj: any) => `${v}=${obj instanceof RefField ? 'XXX' : obj.toString()}`;
+        if (captured[v] instanceof Array) return p + (captured[v] as any).map(formatCapture);
+        return p + formatCapture(captured[v]);
+    }, '');
     const found = ScriptField.GetScriptFieldCache(script + ':' + signature);
     if (found) return found as CompiledScript;
     const { requiredType = '', addReturn = false, params = {}, capturedVariables = {}, typecheck = true } = options;
diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx
index af868ba9c..9389fed01 100644
--- a/src/client/views/DocumentButtonBar.tsx
+++ b/src/client/views/DocumentButtonBar.tsx
@@ -518,7 +518,7 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV
                             )
                         }>
                         <div className={'documentButtonBar-linkButton-empty'} ref={this._dragRef} onPointerDown={this.onTemplateButton}>
-                            {<FontAwesomeIcon className="documentdecorations-icon" icon="edit" size="sm" />}
+                            <FontAwesomeIcon className="documentdecorations-icon" icon="edit" size="sm" />
                         </div>
                     </Flyout>
                 </div>
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 2e04ca3dd..118635a38 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -10,7 +10,7 @@ import 'normalize.css';
 import * as React from 'react';
 import { Doc, DocListCast, Opt } from '../../fields/Doc';
 import { ScriptField } from '../../fields/ScriptField';
-import { DocCast, StrCast } from '../../fields/Types';
+import { StrCast } from '../../fields/Types';
 import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue, returnZero, setupMoveUpEvents, Utils } from '../../Utils';
 import { GoogleAuthenticationManager } from '../apis/GoogleAuthenticationManager';
 import { DocServer } from '../DocServer';
@@ -57,7 +57,6 @@ import { LinkDescriptionPopup } from './nodes/LinkDescriptionPopup';
 import { LinkDocPreview } from './nodes/LinkDocPreview';
 import { RadialMenu } from './nodes/RadialMenu';
 import { TaskCompletionBox } from './nodes/TaskCompletedBox';
-import { PresBox } from './nodes/trails';
 import { OverlayView } from './OverlayView';
 import { AnchorMenu } from './pdf/AnchorMenu';
 import { PreviewCursor } from './PreviewCursor';
diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx
index dba5d703f..b11117b11 100644
--- a/src/client/views/StyleProvider.tsx
+++ b/src/client/views/StyleProvider.tsx
@@ -5,7 +5,7 @@ import { action, runInAction } from 'mobx';
 import { extname } from 'path';
 import { Doc, Opt } from '../../fields/Doc';
 import { BoolCast, Cast, ImageCast, NumCast, StrCast } from '../../fields/Types';
-import { DashColor, emptyFunction, lightOrDark } from '../../Utils';
+import { DashColor, lightOrDark } from '../../Utils';
 import { CollectionViewType, DocumentType } from '../documents/DocumentTypes';
 import { DocFocusOrOpen } from '../util/DocumentManager';
 import { LinkManager } from '../util/LinkManager';
diff --git a/src/client/views/TemplateMenu.tsx b/src/client/views/TemplateMenu.tsx
index 863829a51..681ff66e0 100644
--- a/src/client/views/TemplateMenu.tsx
+++ b/src/client/views/TemplateMenu.tsx
@@ -1,7 +1,6 @@
 import { action, computed, observable, ObservableSet, runInAction } from 'mobx';
 import { observer } from 'mobx-react';
 import { Doc, DocListCast } from '../../fields/Doc';
-import { List } from '../../fields/List';
 import { ScriptField } from '../../fields/ScriptField';
 import { Cast, StrCast } from '../../fields/Types';
 import { TraceMobx } from '../../fields/util';
diff --git a/src/client/views/collections/CollectionStackedTimeline.tsx b/src/client/views/collections/CollectionStackedTimeline.tsx
index f9249e7d5..302d4a464 100644
--- a/src/client/views/collections/CollectionStackedTimeline.tsx
+++ b/src/client/views/collections/CollectionStackedTimeline.tsx
@@ -565,7 +565,7 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack
             anchor,
         }));
         const maxLevel = overlaps.reduce((m, o) => Math.max(m, o.level), 0) + 2;
-        return (
+        return this.clipDuration === 0 ? null : (
             <div ref={this.createDashEventsTarget} style={{ pointerEvents: SnappingManager.GetIsDragging() ? 'all' : undefined }}>
                 <div
                     className="collectionStackedTimeline-timelineContainer"
diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx
index 0b95d712c..610c10be6 100644
--- a/src/client/views/nodes/AudioBox.tsx
+++ b/src/client/views/nodes/AudioBox.tsx
@@ -634,15 +634,11 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
                                 step="0.1"
                                 min="1"
                                 max="5"
-                                value={this.timeline?._zoomFactor}
+                                value={this.timeline?._zoomFactor ?? 1}
                                 className="toolbar-slider"
                                 id="zoom-slider"
-                                onPointerDown={(e: React.PointerEvent) => {
-                                    e.stopPropagation();
-                                }}
-                                onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
-                                    this.zoom(Number(e.target.value));
-                                }}
+                                onPointerDown={(e: React.PointerEvent) => e.stopPropagation()}
+                                onChange={(e: React.ChangeEvent<HTMLInputElement>) => this.zoom(Number(e.target.value))}
                             />
                         </div>
                     )}
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 6ae102a0c..b7a760c1e 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -1489,10 +1489,6 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
             clipPath: borderPath.path ? `path('${borderPath.path}')` : undefined,
         });
 
-        if (this.rootDoc.title === 'Italic') {
-            console.log(highlighting, this.borderRounding, highlighting?.highlightStroke);
-        }
-
         const animRenderDoc = DocumentViewInternal.AnimationEffect(renderDoc, this.rootDoc[AnimationSym], this.rootDoc);
         return (
             <div
diff --git a/src/client/views/nodes/ScriptingBox.tsx b/src/client/views/nodes/ScriptingBox.tsx
index fa2021642..a6e2c3e90 100644
--- a/src/client/views/nodes/ScriptingBox.tsx
+++ b/src/client/views/nodes/ScriptingBox.tsx
@@ -180,13 +180,15 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatable
         const params: ScriptParam = {};
         this.compileParams.forEach(p => (params[p.split(':')[0].trim()] = p.split(':')[1].trim()));
 
-        const result = CompileScript(this.rawText, {
-            editable: true,
-            transformer: DocumentIconContainer.getTransformer(),
-            params,
-            typecheck: false,
-        });
-        Doc.SetInPlace(this.rootDoc, this.fieldKey, result.compiled ? new ScriptField(result, undefined, this.rawText) : new ScriptField(undefined, undefined, this.rawText), true);
+        const result = !this.rawText.trim()
+            ? ({ compiled: false, errors: undefined } as any)
+            : CompileScript(this.rawText, {
+                  editable: true,
+                  transformer: DocumentIconContainer.getTransformer(),
+                  params,
+                  typecheck: false,
+              });
+        Doc.SetInPlace(this.rootDoc, this.fieldKey, result.compiled ? new ScriptField(result, undefined, this.rawText) : undefined, true);
         this.onError(result.compiled ? undefined : result.errors);
         return result.compiled;
     };
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index 3169031b4..44314dca2 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -518,20 +518,13 @@ export namespace Doc {
     }
     export async function SetInPlace(doc: Doc, key: string, value: Field | undefined, defaultProto: boolean) {
         if (key.startsWith('_')) key = key.substring(1);
-        const hasProto = doc.proto instanceof Doc;
+        const hasProto = Doc.GetProto(doc) !== doc ? Doc.GetProto(doc) : undefined;
         const onDeleg = Object.getOwnPropertyNames(doc).indexOf(key) !== -1;
         const onProto = hasProto && Object.getOwnPropertyNames(doc.proto).indexOf(key) !== -1;
         if (onDeleg || !hasProto || (!onProto && !defaultProto)) {
             doc[key] = value;
         } else doc.proto![key] = value;
     }
-    export async function SetOnPrototype(doc: Doc, key: string, value: Field) {
-        const proto = Object.getOwnPropertyNames(doc).indexOf('isPrototype') === -1 ? doc.proto : doc;
-
-        if (proto) {
-            proto[key] = value;
-        }
-    }
     export function GetAllPrototypes(doc: Doc): Doc[] {
         const protos: Doc[] = [];
         let d: Opt<Doc> = doc;
-- 
cgit v1.2.3-70-g09d2


From 2a4e86be98ad0f8d4aa4cb09b982d448b542d916 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Wed, 22 Mar 2023 14:20:09 -0400
Subject: removed gitlike and branch stuff.  updated fortawesome.  added Z
 ordering buttons.  moved ctrl-t/alt-t to edit text title into
 formattedTextBox

---
 package-lock.json                                  |  66 +++---
 package.json                                       |  10 +-
 src/client/documents/Gitlike.ts                    | 226 ++++++++++-----------
 src/client/util/CurrentUserUtils.ts                |   8 +
 src/client/util/DragManager.ts                     |   7 +
 src/client/views/MainView.tsx                      |   4 +
 .../views/collections/CollectionDockingView.tsx    |   3 +-
 src/client/views/collections/CollectionView.tsx    |  38 ++--
 .../collectionFreeForm/CollectionFreeFormView.tsx  |   6 +
 src/client/views/nodes/DocumentView.tsx            |  32 +--
 src/client/views/nodes/button/ButtonScripts.ts     |  16 --
 src/client/views/nodes/button/FontIconBox.tsx      |   1 -
 .../views/nodes/formattedText/FormattedTextBox.tsx |   4 +-
 src/fields/Doc.ts                                  |  26 ++-
 14 files changed, 211 insertions(+), 236 deletions(-)
 delete mode 100644 src/client/views/nodes/button/ButtonScripts.ts

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

diff --git a/package-lock.json b/package-lock.json
index cc51ad9e0..18a435a41 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -698,67 +698,53 @@
             }
         },
         "@fortawesome/fontawesome-common-types": {
-            "version": "0.3.0",
-            "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.3.0.tgz",
-            "integrity": "sha512-CA3MAZBTxVsF6SkfkHXDerkhcQs0QPofy43eFdbWJJkZiq3SfiaH1msOkac59rQaqto5EqWnASboY1dBuKen5w=="
+            "version": "6.3.0",
+            "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.3.0.tgz",
+            "integrity": "sha512-4BC1NMoacEBzSXRwKjZ/X/gmnbp/HU5Qqat7E8xqorUtBFZS+bwfGH5/wqOC2K6GV0rgEobp3OjGRMa5fK9pFg=="
         },
         "@fortawesome/fontawesome-svg-core": {
-            "version": "1.3.0",
-            "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-1.3.0.tgz",
-            "integrity": "sha512-UIL6crBWhjTNQcONt96ExjUnKt1D68foe3xjEensLDclqQ6YagwCRYVQdrp/hW0ALRp/5Fv/VKw+MqTUWYYvPg==",
+            "version": "6.3.0",
+            "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.3.0.tgz",
+            "integrity": "sha512-uz9YifyKlixV6AcKlOX8WNdtF7l6nakGyLYxYaCa823bEBqyj/U2ssqtctO38itNEwXb8/lMzjdoJ+aaJuOdrw==",
             "requires": {
-                "@fortawesome/fontawesome-common-types": "^0.3.0"
+                "@fortawesome/fontawesome-common-types": "6.3.0"
             }
         },
         "@fortawesome/free-brands-svg-icons": {
-            "version": "5.15.4",
-            "resolved": "https://registry.npmjs.org/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-5.15.4.tgz",
-            "integrity": "sha512-f1witbwycL9cTENJegcmcZRYyawAFbm8+c6IirLmwbbpqz46wyjbQYLuxOc7weXFXfB7QR8/Vd2u5R3q6JYD9g==",
+            "version": "6.3.0",
+            "resolved": "https://registry.npmjs.org/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-6.3.0.tgz",
+            "integrity": "sha512-xI0c+a8xnKItAXCN8rZgCNCJQiVAd2Y7p9e2ND6zN3J3ekneu96qrePieJ7yA7073C1JxxoM3vH1RU7rYsaj8w==",
             "requires": {
-                "@fortawesome/fontawesome-common-types": "^0.2.36"
-            },
-            "dependencies": {
-                "@fortawesome/fontawesome-common-types": {
-                    "version": "0.2.36",
-                    "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.2.36.tgz",
-                    "integrity": "sha512-a/7BiSgobHAgBWeN7N0w+lAhInrGxksn13uK7231n2m8EDPE3BMCl9NZLTGrj9ZXfCmC6LM0QLqXidIizVQ6yg=="
-                }
+                "@fortawesome/fontawesome-common-types": "6.3.0"
             }
         },
         "@fortawesome/free-regular-svg-icons": {
-            "version": "5.15.4",
-            "resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-5.15.4.tgz",
-            "integrity": "sha512-9VNNnU3CXHy9XednJ3wzQp6SwNwT3XaM26oS4Rp391GsxVYA+0oDR2J194YCIWf7jNRCYKjUCOduxdceLrx+xw==",
+            "version": "6.3.0",
+            "resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.3.0.tgz",
+            "integrity": "sha512-cZnwiVHZ51SVzWHOaNCIA+u9wevZjCuAGSvSYpNlm6A4H4Vhwh8481Bf/5rwheIC3fFKlgXxLKaw8Xeroz8Ntg==",
             "requires": {
-                "@fortawesome/fontawesome-common-types": "^0.2.36"
+                "@fortawesome/fontawesome-common-types": "6.3.0"
             },
             "dependencies": {
                 "@fortawesome/fontawesome-common-types": {
-                    "version": "0.2.36",
-                    "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.2.36.tgz",
-                    "integrity": "sha512-a/7BiSgobHAgBWeN7N0w+lAhInrGxksn13uK7231n2m8EDPE3BMCl9NZLTGrj9ZXfCmC6LM0QLqXidIizVQ6yg=="
+                    "version": "6.3.0",
+                    "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.3.0.tgz",
+                    "integrity": "sha512-4BC1NMoacEBzSXRwKjZ/X/gmnbp/HU5Qqat7E8xqorUtBFZS+bwfGH5/wqOC2K6GV0rgEobp3OjGRMa5fK9pFg=="
                 }
             }
         },
         "@fortawesome/free-solid-svg-icons": {
-            "version": "5.15.4",
-            "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-5.15.4.tgz",
-            "integrity": "sha512-JLmQfz6tdtwxoihXLg6lT78BorrFyCf59SAwBM6qV/0zXyVeDygJVb3fk+j5Qat+Yvcxp1buLTY5iDh1ZSAQ8w==",
+            "version": "6.3.0",
+            "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.3.0.tgz",
+            "integrity": "sha512-x5tMwzF2lTH8pyv8yeZRodItP2IVlzzmBuD1M7BjawWgg9XAvktqJJ91Qjgoaf8qJpHQ8FEU9VxRfOkLhh86QA==",
             "requires": {
-                "@fortawesome/fontawesome-common-types": "^0.2.36"
-            },
-            "dependencies": {
-                "@fortawesome/fontawesome-common-types": {
-                    "version": "0.2.36",
-                    "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.2.36.tgz",
-                    "integrity": "sha512-a/7BiSgobHAgBWeN7N0w+lAhInrGxksn13uK7231n2m8EDPE3BMCl9NZLTGrj9ZXfCmC6LM0QLqXidIizVQ6yg=="
-                }
+                "@fortawesome/fontawesome-common-types": "6.3.0"
             }
         },
         "@fortawesome/react-fontawesome": {
-            "version": "0.1.19",
-            "resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-0.1.19.tgz",
-            "integrity": "sha512-Hyb+lB8T18cvLNX0S3llz7PcSOAJMLwiVKBuuzwM/nI5uoBw+gQjnf9il0fR1C3DKOI5Kc79pkJ4/xB0Uw9aFQ==",
+            "version": "0.2.0",
+            "resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-0.2.0.tgz",
+            "integrity": "sha512-uHg75Rb/XORTtVt7OS9WoK8uM276Ufi7gCzshVWkUJbHhh3svsUUeqXerrM96Wm7fRiDzfKRwSoahhMIkGAYHw==",
             "requires": {
                 "prop-types": "^15.8.1"
             }
@@ -20903,7 +20889,7 @@
         "sparse-bitfield": {
             "version": "3.0.3",
             "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz",
-            "integrity": "sha1-/0rm5oZWBWuks+eSqzM004JzyhE=",
+            "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==",
             "optional": true,
             "requires": {
                 "memory-pager": "^1.0.2"
diff --git a/package.json b/package.json
index 2c4c41917..41bdd5f8f 100644
--- a/package.json
+++ b/package.json
@@ -129,11 +129,11 @@
     "dependencies": {
         "@ffmpeg/core": "0.10.0",
         "@ffmpeg/ffmpeg": "0.10.0",
-        "@fortawesome/fontawesome-svg-core": "^1.3.0",
-        "@fortawesome/free-brands-svg-icons": "^5.15.4",
-        "@fortawesome/free-regular-svg-icons": "^5.15.4",
-        "@fortawesome/free-solid-svg-icons": "^5.15.4",
-        "@fortawesome/react-fontawesome": "^0.1.19",
+        "@fortawesome/fontawesome-svg-core": "^6.3.0",
+        "@fortawesome/free-brands-svg-icons": "^6.3.0",
+        "@fortawesome/free-regular-svg-icons": "^6.3.0",
+        "@fortawesome/free-solid-svg-icons": "^6.3.0",
+        "@fortawesome/react-fontawesome": "^0.2.0",
         "@hig/flyout": "^1.3.1",
         "@hig/theme-context": "^2.1.3",
         "@hig/theme-data": "^2.23.1",
diff --git a/src/client/documents/Gitlike.ts b/src/client/documents/Gitlike.ts
index 575c984f5..5e2baf924 100644
--- a/src/client/documents/Gitlike.ts
+++ b/src/client/documents/Gitlike.ts
@@ -1,118 +1,118 @@
-import { Doc, DocListCast, DocListCastAsync, Field } from "../../fields/Doc";
-import { List } from "../../fields/List";
-import { Cast, DateCast } from "../../fields/Types";
-import { DateField } from "../../fields/DateField";
-import { Id } from "../../fields/FieldSymbols";
+// import { Doc, DocListCast, DocListCastAsync, Field } from "../../fields/Doc";
+// import { List } from "../../fields/List";
+// import { Cast, DateCast } from "../../fields/Types";
+// import { DateField } from "../../fields/DateField";
+// import { Id } from "../../fields/FieldSymbols";
 
-// synchs matching documents on the two branches that are being merged/pulled
-// currently this just synchs the main 'fieldKey' component of the data since 
-// we don't have individual timestamps for all fields -- this is a problematic design issue.
-function GitlikeSynchDocs(bd: Doc, md: Doc) {
-    const fieldKey = Doc.LayoutFieldKey(md);
-    const bdate = DateCast(bd[`${fieldKey}-lastModified`])?.date;
-    const mdate = DateCast(md[`${fieldKey}-lastModified`])?.date;
-    const bdproto = bd && Doc.GetProto(bd);
-    if (bdate !== mdate && bdate <= mdate) {
-        if (bdproto && md) {
-            bdproto[fieldKey] = Field.Copy(md[fieldKey]);
-            bdproto[`${fieldKey}-lastModified`] = new DateField();
-        }
-    }
-    const bldate = DateCast(bd._lastModified)?.date;
-    const mldate = DateCast(md._lastModified)?.date;
-    if (bldate === mldate || bldate > mldate) return;
-    if (bdproto && md) {
-        bd.x = Field.Copy(md.x);
-        bd.y = Field.Copy(md.y);
-        bd.width = Field.Copy(md.width);
-        bd.height = Field.Copy(md.height);
-        bdproto._lastModified = new DateField();
-    }
-}
+// // synchs matching documents on the two branches that are being merged/pulled
+// // currently this just synchs the main 'fieldKey' component of the data since
+// // we don't have individual timestamps for all fields -- this is a problematic design issue.
+// function GitlikeSynchDocs(bd: Doc, md: Doc) {
+//     const fieldKey = Doc.LayoutFieldKey(md);
+//     const bdate = DateCast(bd[`${fieldKey}-lastModified`])?.date;
+//     const mdate = DateCast(md[`${fieldKey}-lastModified`])?.date;
+//     const bdproto = bd && Doc.GetProto(bd);
+//     if (bdate !== mdate && bdate <= mdate) {
+//         if (bdproto && md) {
+//             bdproto[fieldKey] = Field.Copy(md[fieldKey]);
+//             bdproto[`${fieldKey}-lastModified`] = new DateField();
+//         }
+//     }
+//     const bldate = DateCast(bd._lastModified)?.date;
+//     const mldate = DateCast(md._lastModified)?.date;
+//     if (bldate === mldate || bldate > mldate) return;
+//     if (bdproto && md) {
+//         bd.x = Field.Copy(md.x);
+//         bd.y = Field.Copy(md.y);
+//         bd.width = Field.Copy(md.width);
+//         bd.height = Field.Copy(md.height);
+//         bdproto._lastModified = new DateField();
+//     }
+// }
 
-// pulls documents onto a branch from the branch's master
-// if a document exists on master but not on the branch, it is branched and added
-// NOTE: need to set a timestamp on the branch that is equal to the master's last merge timestamp.
-async function GitlikePullFromMaster(branch: Doc, suffix = "") {
-    const masterMain = Cast(branch.branchOf, Doc, null);
-    // get the set of documents on both the branch and master
-    const masterMainDocs = masterMain && await DocListCastAsync(masterMain[Doc.LayoutFieldKey(masterMain) + suffix]);
-    const branchMainDocs = await DocListCastAsync(branch[Doc.LayoutFieldKey(branch) + suffix]);
-    // get the master documents that correspond to the branch documents
-    const branchMasterMainDocs = branchMainDocs?.map(bd => Cast(bd.branchOf, Doc, null) || bd);
-    const branchMasterMainDocProtos = branchMasterMainDocs?.map(doc => Doc.GetProto(doc));
-    // get documents on master that don't have a corresponding master doc (form a branch doc), and ...
-    const newDocsFromMaster = masterMainDocs?.filter(md => !branchMasterMainDocProtos?.includes(Doc.GetProto(md)));
-    const oldDocsFromMaster = masterMainDocs?.filter(md => branchMasterMainDocProtos?.includes(Doc.GetProto(md)));
-    oldDocsFromMaster?.forEach(md => {
-        const bd = branchMainDocs?.find(bd => (Cast(bd.branchOf, Doc, null) || bd) === md);
-        bd && GitlikeSynchDocs(bd, md);
-    });
-    const cloneMap = new Map<string, Doc>(); cloneMap.set(masterMain[Id], branch);
-    // make branch clones of them, then add them to the branch
-    const newlyBranchedDocs = await Promise.all(newDocsFromMaster?.map(async md => (await Doc.MakeClone(md, false, true, cloneMap)).clone) || []);
-    newlyBranchedDocs.forEach(nd => {
-        Doc.AddDocToList(branch, Doc.LayoutFieldKey(branch) + suffix, nd);
-        nd.context = branch;
-    });
-    // if a branch doc's corresponding main branch doc doesn't have a context, then it was deleted.  
-    const remDocsFromMaster = branchMainDocs?.filter(bd => Cast(bd.branchOf, Doc, null) && !Cast(bd.branchOf, Doc, null)?.context);
-    // so then remove all the deleted main docs from this branch.
-    remDocsFromMaster?.forEach(rd => Doc.RemoveDocFromList(branch, Doc.LayoutFieldKey(branch) + suffix, rd));
-}
+// // pulls documents onto a branch from the branch's master
+// // if a document exists on master but not on the branch, it is branched and added
+// // NOTE: need to set a timestamp on the branch that is equal to the master's last merge timestamp.
+// async function GitlikePullFromMaster(branch: Doc, suffix = "") {
+//     const masterMain = Cast(branch.branchOf, Doc, null);
+//     // get the set of documents on both the branch and master
+//     const masterMainDocs = masterMain && await DocListCastAsync(masterMain[Doc.LayoutFieldKey(masterMain) + suffix]);
+//     const branchMainDocs = await DocListCastAsync(branch[Doc.LayoutFieldKey(branch) + suffix]);
+//     // get the master documents that correspond to the branch documents
+//     const branchMasterMainDocs = branchMainDocs?.map(bd => Cast(bd.branchOf, Doc, null) || bd);
+//     const branchMasterMainDocProtos = branchMasterMainDocs?.map(doc => Doc.GetProto(doc));
+//     // get documents on master that don't have a corresponding master doc (form a branch doc), and ...
+//     const newDocsFromMaster = masterMainDocs?.filter(md => !branchMasterMainDocProtos?.includes(Doc.GetProto(md)));
+//     const oldDocsFromMaster = masterMainDocs?.filter(md => branchMasterMainDocProtos?.includes(Doc.GetProto(md)));
+//     oldDocsFromMaster?.forEach(md => {
+//         const bd = branchMainDocs?.find(bd => (Cast(bd.branchOf, Doc, null) || bd) === md);
+//         bd && GitlikeSynchDocs(bd, md);
+//     });
+//     const cloneMap = new Map<string, Doc>(); cloneMap.set(masterMain[Id], branch);
+//     // make branch clones of them, then add them to the branch
+//     const newlyBranchedDocs = await Promise.all(newDocsFromMaster?.map(async md => (await Doc.MakeClone(md, false, true, cloneMap)).clone) || []);
+//     newlyBranchedDocs.forEach(nd => {
+//         Doc.AddDocToList(branch, Doc.LayoutFieldKey(branch) + suffix, nd);
+//         nd.context = branch;
+//     });
+//     // if a branch doc's corresponding main branch doc doesn't have a context, then it was deleted.
+//     const remDocsFromMaster = branchMainDocs?.filter(bd => Cast(bd.branchOf, Doc, null) && !Cast(bd.branchOf, Doc, null)?.context);
+//     // so then remove all the deleted main docs from this branch.
+//     remDocsFromMaster?.forEach(rd => Doc.RemoveDocFromList(branch, Doc.LayoutFieldKey(branch) + suffix, rd));
+// }
 
-// merges all branches from the master branch by first merging the top-level collection of documents, 
-// and then merging all the annotations on those documents.
-// TODO: need to add an incrementing timestamp whenever anything merges.  don't allow a branch to merge if it's last pull timestamp isn't equal to the last merge timestamp.
-async function GitlikeMergeWithMaster(master: Doc, suffix = "") {
-    const branches = await DocListCastAsync(master.branches);
-    branches?.map(async branch => {
-        const branchChildren = await DocListCastAsync(branch[Doc.LayoutFieldKey(branch) + suffix]);
-        branchChildren && await Promise.all(branchChildren.map(async bd => {
-            const cloneMap = new Map<string, Doc>(); cloneMap.set(master[Id], branch);
-            // see if the branch's child exists on master.  
-            const masterChild = Cast(bd.branchOf, Doc, null) || (await Doc.MakeClone(bd, false, true, cloneMap)).clone;
-            // if the branch's child didn't exist on master, we make a branch clone of the child to add to master.  
-            // however, since master is supposed to have the "main" clone, and branches, the "branch" clones, we have to reverse the fields
-            // on the branch child and master clone.
-            if (masterChild.branchOf) {
-                const branchDocProto = Doc.GetProto(bd);
-                const masterChildProto = Doc.GetProto(masterChild);
-                const branchTitle = bd.title;
-                branchDocProto.title = masterChildProto.title;
-                masterChildProto.title = branchTitle;
-                masterChildProto.branchOf = masterChild.branchOf = undefined; // the master child should not be a branch of the branch child, so unset 'branchOf'
-                masterChildProto.branches = new List<Doc>([bd]); // the master child's branches needs to include the branch child
-                Doc.RemoveDocFromList(branchDocProto, "branches", masterChildProto);   // the branch child should not have the master child in its branch list.
-                branchDocProto.branchOf = masterChild;                                 // the branch child is now a branch of the master child
-            }
-            Doc.AddDocToList(master, Doc.LayoutFieldKey(master) + suffix, masterChild); // add the masterChild to master (if it's already there, this is a no-op)
-            masterChild.context = master;
-            GitlikeSynchDocs(masterChild, bd);//Doc.GetProto(masterChild), bd);
-        }));
-        const masterChildren = await DocListCastAsync(master[Doc.LayoutFieldKey(master) + suffix]);
-        masterChildren?.forEach(mc => {                      // see if any master children
-            if (!branchChildren?.find(bc => bc.branchOf === mc)) { //    are not in the list of children for this branch. 
-                Doc.RemoveDocFromList(master, Doc.LayoutFieldKey(master) + suffix, mc); // if so, delete the master child since the branch has deleted it.
-                mc.context = undefined;      // NOTE if we merge a branch that didn't do a pull, it will look like the branch deleted documents -- need edit timestamps that prevent merging if branch isn't up-to-date with last edit timestamp
-            }
-        });
-    });
-}
+// // merges all branches from the master branch by first merging the top-level collection of documents,
+// // and then merging all the annotations on those documents.
+// // TODO: need to add an incrementing timestamp whenever anything merges.  don't allow a branch to merge if it's last pull timestamp isn't equal to the last merge timestamp.
+// async function GitlikeMergeWithMaster(master: Doc, suffix = "") {
+//     const branches = await DocListCastAsync(master.branches);
+//     branches?.map(async branch => {
+//         const branchChildren = await DocListCastAsync(branch[Doc.LayoutFieldKey(branch) + suffix]);
+//         branchChildren && await Promise.all(branchChildren.map(async bd => {
+//             const cloneMap = new Map<string, Doc>(); cloneMap.set(master[Id], branch);
+//             // see if the branch's child exists on master.
+//             const masterChild = Cast(bd.branchOf, Doc, null) || (await Doc.MakeClone(bd, false, true, cloneMap)).clone;
+//             // if the branch's child didn't exist on master, we make a branch clone of the child to add to master.
+//             // however, since master is supposed to have the "main" clone, and branches, the "branch" clones, we have to reverse the fields
+//             // on the branch child and master clone.
+//             if (masterChild.branchOf) {
+//                 const branchDocProto = Doc.GetProto(bd);
+//                 const masterChildProto = Doc.GetProto(masterChild);
+//                 const branchTitle = bd.title;
+//                 branchDocProto.title = masterChildProto.title;
+//                 masterChildProto.title = branchTitle;
+//                 masterChildProto.branchOf = masterChild.branchOf = undefined; // the master child should not be a branch of the branch child, so unset 'branchOf'
+//                 masterChildProto.branches = new List<Doc>([bd]); // the master child's branches needs to include the branch child
+//                 Doc.RemoveDocFromList(branchDocProto, "branches", masterChildProto);   // the branch child should not have the master child in its branch list.
+//                 branchDocProto.branchOf = masterChild;                                 // the branch child is now a branch of the master child
+//             }
+//             Doc.AddDocToList(master, Doc.LayoutFieldKey(master) + suffix, masterChild); // add the masterChild to master (if it's already there, this is a no-op)
+//             masterChild.context = master;
+//             GitlikeSynchDocs(masterChild, bd);//Doc.GetProto(masterChild), bd);
+//         }));
+//         const masterChildren = await DocListCastAsync(master[Doc.LayoutFieldKey(master) + suffix]);
+//         masterChildren?.forEach(mc => {                      // see if any master children
+//             if (!branchChildren?.find(bc => bc.branchOf === mc)) { //    are not in the list of children for this branch.
+//                 Doc.RemoveDocFromList(master, Doc.LayoutFieldKey(master) + suffix, mc); // if so, delete the master child since the branch has deleted it.
+//                 mc.context = undefined;      // NOTE if we merge a branch that didn't do a pull, it will look like the branch deleted documents -- need edit timestamps that prevent merging if branch isn't up-to-date with last edit timestamp
+//             }
+//         });
+//     });
+// }
 
-// performs a "git"-like task: pull or merge
-//    if pull, then target is a specific branch document that will be updated from its associated master
-//    if merge, then target is the master doc that will merge in all branches associated with it.
-// TODO: parameterize 'merge' to specify which branch(es) should be merged.   
-//       extend 'merge' to allow a specific branch to be merge target (not just master);
-//       make pull/merge be recursive (ie, this func currently just operates on the main doc and its children)
-export async function BranchTask(target: Doc, action: "pull" | "merge") {
-    const func = action === "pull" ? GitlikePullFromMaster : GitlikeMergeWithMaster;
-    await func(target, "");
-    await DocListCast(target[Doc.LayoutFieldKey(target)]).forEach(async targetChild => func(targetChild, "-annotations"));
-    await DocListCast(target[Doc.LayoutFieldKey(target)]).forEach(async targetChild => func(targetChild, "-sidebar"));
-}
+// // performs a "git"-like task: pull or merge
+// //    if pull, then target is a specific branch document that will be updated from its associated master
+// //    if merge, then target is the master doc that will merge in all branches associated with it.
+// // TODO: parameterize 'merge' to specify which branch(es) should be merged.
+// //       extend 'merge' to allow a specific branch to be merge target (not just master);
+// //       make pull/merge be recursive (ie, this func currently just operates on the main doc and its children)
+// export async function BranchTask(target: Doc, action: "pull" | "merge") {
+//     const func = action === "pull" ? GitlikePullFromMaster : GitlikeMergeWithMaster;
+//     await func(target, "");
+//     await DocListCast(target[Doc.LayoutFieldKey(target)]).forEach(async targetChild => func(targetChild, "-annotations"));
+//     await DocListCast(target[Doc.LayoutFieldKey(target)]).forEach(async targetChild => func(targetChild, "-sidebar"));
+// }
 
-export async function BranchCreate(target: Doc) {
-    return (await Doc.MakeClone(target, false, true)).clone;
-}
\ No newline at end of file
+// export async function BranchCreate(target: Doc) {
+//     return (await Doc.MakeClone(target, false, true)).clone;
+// }
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index c038fd6ab..f9766c3fb 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -608,6 +608,13 @@ export class CurrentUserUtils {
         return DocUtils.AssignDocField(doc, field, (opts, items) => this.linearButtonList(opts, items??[]), dockBtnsReqdOpts, btns);
     }
 
+    static zTools(): Button[] {
+        return [
+            { title: "Bottom",  icon: "arrows-down-to-line",  toolTip: "Make doc topmost",btnType: ButtonType.ClickButton,        expertMode: false, toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectionManager_selectedDocType(self.toolType, self.expertMode, true)'}, scripts: { onClick: 'sendToBack()'}}, // Only when floating document is selected in freeform
+            { title: "Top",     icon: "arrows-up-to-line",    toolTip: "Make doc bottommost",btnType: ButtonType.ClickButton,     expertMode: false, toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectionManager_selectedDocType(self.toolType, self.expertMode, true)'}, scripts: { onClick: 'bringToFront()'}}, // Only when floating document is selected in freeform
+            { title: "Z order", icon: "z",                    toolTip: "Bring Forward on Drag",btnType: ButtonType.ToggleButton,  expertMode: false, toolType:CollectionViewType.Freeform, funcs: {hidden: 'tab'},                                                                     scripts: { onClick: 'toggleRaiseOnDrag(_readOnly_)'}}, // Only when floating document is selected in freeform
+        ]
+    }
     static textTools():Button[] {
         return [
             { title: "Font",    toolTip: "Font", width: 100,     btnType: ButtonType.DropdownList,                      toolType:"font",     ignoreClick: true,   scripts: {script:  '{ return setFontAttr(self.toolType, value, _readOnly_);}'},
@@ -675,6 +682,7 @@ export class CurrentUserUtils {
             { title: "Fwd",     icon: "chevron-right", toolTip: "Next Animation Frame", btnType: ButtonType.ClickButton,         expertMode: true,  toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectionManager_selectedDocType(self.toolType, self.expertMode)'}, width: 20,  scripts: { onClick: 'nextKeyFrame(_readOnly_)'}},
             { title: "Text",    icon: "Text",          toolTip: "Text functions",       subMenu: CurrentUserUtils.textTools(),   expertMode: false, toolType:DocumentType.RTF,            funcs: {                                                                             linearViewIsExpanded: `SelectionManager_selectedDocType(self.toolType, self.expertMode)`} }, // Always available
             { title: "Ink",     icon: "Ink",           toolTip: "Ink functions",        subMenu: CurrentUserUtils.inkTools(),    expertMode: false, toolType:DocumentType.INK,            funcs: {                                                                             linearViewIsExpanded: `SelectionManager_selectedDocType(self.toolType, self.expertMode)`}, scripts: { onClick: 'setInkToolDefaults()'} }, // Always available
+            { title: "Z",       icon: "z",             toolTip: "Z order functions",    subMenu: CurrentUserUtils.zTools(),      expertMode: false, toolType:"tab",                       funcs: {                                                                             linearViewIsExpanded: `SelectionManager_selectedDocType(self.toolType, self.expertMode)`} }, // Always available
             { title: "Web",     icon: "Web",           toolTip: "Web functions",        subMenu: CurrentUserUtils.webTools(),    expertMode: false, toolType:DocumentType.WEB,            funcs: {hidden: `!SelectionManager_selectedDocType(self.toolType, self.expertMode)`, linearViewIsExpanded: `SelectionManager_selectedDocType(self.toolType, self.expertMode)`} }, // Only when Web is selected
             { title: "Schema",  icon: "Schema",        toolTip: "Schema functions",     subMenu: CurrentUserUtils.schemaTools(), expertMode: false, toolType:CollectionViewType.Schema,   funcs: {hidden: `!SelectionManager_selectedDocType(self.toolType, self.expertMode)`, linearViewIsExpanded: `SelectionManager_selectedDocType(self.toolType, self.expertMode)`} } // Only when Schema is selected
         ];
diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts
index a56f87075..5a35fcd53 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -10,7 +10,9 @@ import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from '../../fields/Types
 import { emptyFunction, Utils } from '../../Utils';
 import { Docs, DocUtils } from '../documents/Documents';
 import * as globalCssVariables from '../views/global/globalCssVariables.scss';
+import { Colors } from '../views/global/globalEnums';
 import { DocumentView } from '../views/nodes/DocumentView';
+import { ScriptingGlobals } from './ScriptingGlobals';
 import { SnappingManager } from './SnappingManager';
 import { UndoManager } from './UndoManager';
 
@@ -590,3 +592,8 @@ export namespace DragManager {
         endDrag?.();
     }
 }
+
+ScriptingGlobals.add(function toggleRaiseOnDrag(readOnly?: boolean) {
+    if (readOnly) return DragManager.GetRaiseWhenDragged() ? Colors.MEDIUM_BLUE : 'transparent';
+    DragManager.SetRaiseWhenDragged(!DragManager.GetRaiseWhenDragged());
+});
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 118635a38..e755f204b 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -477,6 +477,10 @@ export class MainView extends React.Component {
                 fa.faHighlighter,
                 fa.faRemoveFormat,
                 fa.faHandPointUp,
+                fa.faXRay,
+                fa.faZ,
+                fa.faArrowsUpToLine,
+                fa.faArrowsDownToLine,
             ]
         );
     }
diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index 9b6554d67..057c1e30f 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -410,11 +410,10 @@ export class CollectionDockingView extends CollectionSubView() {
             const _width = Number(getComputedStyle(content).width.replace('px', ''));
             const _height = Number(getComputedStyle(content).height.replace('px', ''));
             return CollectionFreeFormView.UpdateIcon(this.layoutDoc[Id] + '-icon' + new Date().getTime(), content, _width, _height, _width, _height, 0, 1, true, this.layoutDoc[Id] + '-icon', (iconFile, _nativeWidth, _nativeHeight) => {
-                const img = Docs.Create.ImageDocument(new ImageField(iconFile), { title: this.rootDoc.title + '-icon', _width, _height, _nativeWidth, _nativeHeight });
                 const proto = this.dataDoc; // Cast(img.proto, Doc, null)!;
                 proto['thumb-nativeWidth'] = _width;
                 proto['thumb-nativeHeight'] = _height;
-                this.dataDoc.thumb = new ImageField(iconFile);
+                proto.thumb = new ImageField(iconFile);
             });
         }
     }
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index aed88aa1a..eafa50d27 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -2,7 +2,6 @@ import { computed, observable, runInAction } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
 import { Doc, DocListCast } from '../../../fields/Doc';
-import { Id } from '../../../fields/FieldSymbols';
 import { ObjectField } from '../../../fields/ObjectField';
 import { ScriptField } from '../../../fields/ScriptField';
 import { BoolCast, Cast, ScriptCast, StrCast } from '../../../fields/Types';
@@ -10,13 +9,12 @@ import { TraceMobx } from '../../../fields/util';
 import { returnEmptyString } from '../../../Utils';
 import { DocUtils } from '../../documents/Documents';
 import { CollectionViewType } from '../../documents/DocumentTypes';
-import { BranchCreate, BranchTask } from '../../documents/Gitlike';
 import { ImageUtils } from '../../util/Import & Export/ImageUtils';
 import { InteractionUtils } from '../../util/InteractionUtils';
 import { ContextMenu } from '../ContextMenu';
 import { ContextMenuProps } from '../ContextMenuItem';
 import { ViewBoxAnnotatableComponent, ViewBoxAnnotatableProps } from '../DocComponent';
-import { OpenWhere, OpenWhereMod } from '../nodes/DocumentView';
+import { OpenWhere } from '../nodes/DocumentView';
 import { FieldView, FieldViewProps } from '../nodes/FieldView';
 import { CollectionCarousel3DView } from './CollectionCarousel3DView';
 import { CollectionCarouselView } from './CollectionCarouselView';
@@ -193,23 +191,23 @@ export class CollectionView extends ViewBoxAnnotatableComponent<ViewBoxAnnotatab
             }
             !Doc.noviceMode && optionItems.push({ description: `${this.rootDoc._isLightbox ? 'Unset' : 'Set'} is Lightbox`, event: () => (this.rootDoc._isLightbox = !this.rootDoc._isLightbox), icon: 'project-diagram' });
 
-            if (!Doc.noviceMode && false) {
-                optionItems.push({
-                    description: 'Create Branch',
-                    event: async () => this.props.addDocTab(await BranchCreate(this.rootDoc), OpenWhere.addRight),
-                    icon: 'project-diagram',
-                });
-                optionItems.push({
-                    description: 'Pull Master',
-                    event: () => BranchTask(this.rootDoc, 'pull'),
-                    icon: 'project-diagram',
-                });
-                optionItems.push({
-                    description: 'Merge Branches',
-                    event: () => BranchTask(this.rootDoc, 'merge'),
-                    icon: 'project-diagram',
-                });
-            }
+            // if (!Doc.noviceMode && false) {
+            //     optionItems.push({
+            //         description: 'Create Branch',
+            //         event: async () => this.props.addDocTab(await BranchCreate(this.rootDoc), OpenWhere.addRight),
+            //         icon: 'project-diagram',
+            //     });
+            //     optionItems.push({
+            //         description: 'Pull Master',
+            //         event: () => BranchTask(this.rootDoc, 'pull'),
+            //         icon: 'project-diagram',
+            //     });
+            //     optionItems.push({
+            //         description: 'Merge Branches',
+            //         event: () => BranchTask(this.rootDoc, 'merge'),
+            //         icon: 'project-diagram',
+            //     });
+            // }
 
             !options && cm.addItem({ description: 'Options...', subitems: optionItems, icon: 'hand-point-right' });
 
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index ac90c67a5..0ea614472 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -2280,3 +2280,9 @@ ScriptingGlobals.add(function pinWithView(readOnly: boolean, pinDocContent: bool
             TabDocView.PinDoc(view.rootDoc, { currentFrame: Cast(view.rootDoc.currentFrame, 'number', null), pinDocContent, pinViewport: MarqueeView.CurViewBounds(view.rootDoc, view.props.PanelWidth(), view.props.PanelHeight()) })
         );
 });
+ScriptingGlobals.add(function bringToFront() {
+    SelectionManager.Views().forEach(view => view.props.CollectionFreeFormDocumentView?.().props.CollectionFreeFormView.bringToFront(view.rootDoc));
+});
+ScriptingGlobals.add(function sendToBack(doc: Doc) {
+    SelectionManager.Views().forEach(view => view.props.CollectionFreeFormDocumentView?.().props.CollectionFreeFormView.bringToFront(view.rootDoc, true));
+});
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index edf508c95..c1665ea3d 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -162,6 +162,7 @@ export interface DocumentViewSharedProps {
     childHideResizeHandles?: () => boolean;
     dataTransition?: string; // specifies animation transition - used by collectionPile and potentially other layout engines when changing the size of documents so that the change won't be abrupt
     styleProvider: Opt<StyleProviderFunc>;
+    setTitleFocus?: () => void;
     focus: DocFocusFunc;
     fitWidth?: (doc: Doc) => boolean | undefined;
     docFilters: () => string[];
@@ -539,18 +540,11 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
                 .ScreenToLocalTransform()
                 .scale(this.NativeDimScaling)
                 .transformDirection(x - left, y - top);
-            // dragData.offset[0] = Math.min(this.rootDoc[WidthSym](), dragData.offset[0]);  // bcz: this was breaking dragging rotated objects since the offset may be out of bounds with regard to the unrotated document
-            // dragData.offset[1] = Math.min(this.rootDoc[HeightSym](), dragData.offset[1]);
             dragData.dropAction = dropAction;
             dragData.treeViewDoc = this.props.treeViewDoc;
             dragData.removeDocument = this.props.removeDocument;
             dragData.moveDocument = this.props.moveDocument;
             dragData.canEmbed = this.props.canEmbedOnDrag;
-            //dragData.dimSource :
-            // dragEffects field, set dim
-            // add kv pairs to a doc, swap properties with the node while dragging, and then swap when dropping
-            // add a dragEffects prop to DocumentView as a function that sets up. Each view has its own prop, when you start dragging:
-            // in Draganager, figure out which doc(s) you're dragging and change what opacity function returns
             const ffview = this.props.CollectionFreeFormDocumentView?.().props.CollectionFreeFormView;
             ffview && runInAction(() => (ffview.ChildDrag = this.props.DocumentView()));
             DragManager.StartDocumentDrag([this._mainCont.current], dragData, x, y, { hideSource: hideSource || (!dropAction && !this.layoutDoc.onDragStart && !this.props.dontHideOnDrag) }, () =>
@@ -560,22 +554,6 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
         }
     }
 
-    onKeyDown = (e: React.KeyboardEvent) => {
-        if (e.altKey) {
-            e.stopPropagation();
-            e.preventDefault();
-            if (e.key === '†' || e.key === 't') {
-                if (!StrCast(this.layoutDoc._showTitle)) this.layoutDoc._showTitle = 'title';
-                if (!this._titleRef.current) setTimeout(() => this._titleRef.current?.setIsFocused(true));
-                else if (!this._titleRef.current.setIsFocused(true)) {
-                    // if focus didn't change, focus on interior text...
-                    this._titleRef.current?.setIsFocused(false);
-                    this._componentView?.setFocus?.();
-                }
-            }
-        }
-    };
-
     defaultRestoreTargetView = (docView: DocumentView, anchor: Doc, focusSpeed: number, options: DocFocusOptions) => {
         const targetMatch =
             Doc.AreProtosEqual(anchor, this.rootDoc) || // anchor is this document, so anchor's properties apply to this document
@@ -585,6 +563,12 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
         return targetMatch && PresBox.restoreTargetDocView(docView, anchor, focusSpeed) ? focusSpeed : undefined;
     };
 
+    // switches text input focus to the title bar of the document (and displays the title bar if it hadn't been)
+    setTitleFocus = () => {
+        if (!StrCast(this.layoutDoc._showTitle)) this.layoutDoc._showTitle = 'title';
+        setTimeout(() => this._titleRef.current?.setIsFocused(true)); // use timeout in case title wasn't shown to allow re-render so that titleref will be defined
+    };
+
     onClick = action((e: React.MouseEvent | React.PointerEvent) => {
         if (!this.Document.ignoreClick && this.props.renderDepth >= 0 && Math.abs(e.clientX - this._downX) < Utils.DRAG_THRESHOLD && Math.abs(e.clientY - this._downY) < Utils.DRAG_THRESHOLD) {
             let stopPropagate = true;
@@ -1176,6 +1160,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
                     rootSelected={this.rootSelected}
                     onClick={this.onClickFunc}
                     focus={this.props.focus}
+                    setTitleFocus={this.setTitleFocus}
                     layoutKey={this.finalLayoutKey}
                 />
                 {this.layoutDoc.hideAllLinks ? null : this.allLinkEndpoints}
@@ -1492,7 +1477,6 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
                 className={`${DocumentView.ROOT_DIV} docView-hack`}
                 ref={this._mainCont}
                 onContextMenu={this.onContextMenu}
-                onKeyDown={this.onKeyDown}
                 onPointerDown={this.onPointerDown}
                 onClick={this.onClick}
                 onPointerEnter={e => (!SnappingManager.GetIsDragging() || DragManager.CanEmbed) && Doc.BrushDoc(this.props.Document)}
diff --git a/src/client/views/nodes/button/ButtonScripts.ts b/src/client/views/nodes/button/ButtonScripts.ts
deleted file mode 100644
index b4a382faf..000000000
--- a/src/client/views/nodes/button/ButtonScripts.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import { ScriptingGlobals } from "../../../util/ScriptingGlobals";
-import { SelectionManager } from "../../../util/SelectionManager";
-import { Colors } from "../../global/globalEnums";
-
-// toggle: Set overlay status of selected document
-ScriptingGlobals.add(function changeView(view: string) {
-    const selected = SelectionManager.Views().length ? SelectionManager.Views()[0] : undefined;
-    selected ? selected.Document._viewType = view : console.log("[FontIconBox.tsx] changeView failed");
-});
-
-// toggle: Set overlay status of selected document
-ScriptingGlobals.add(function toggleOverlay(readOnly?: boolean) {
-    const selected = SelectionManager.Views().length ? SelectionManager.Views()[0] : undefined;
-    if (readOnly) return selected?.Document.z ? Colors.MEDIUM_BLUE : "transparent";
-    selected ? selected.props.CollectionFreeFormDocumentView?.().float() : console.log("failed");
-});
\ No newline at end of file
diff --git a/src/client/views/nodes/button/FontIconBox.tsx b/src/client/views/nodes/button/FontIconBox.tsx
index 339887757..cb962cad3 100644
--- a/src/client/views/nodes/button/FontIconBox.tsx
+++ b/src/client/views/nodes/button/FontIconBox.tsx
@@ -1,5 +1,4 @@
 import { IconProp } from '@fortawesome/fontawesome-svg-core';
-import { faAlignRight } from '@fortawesome/free-solid-svg-icons';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { Tooltip } from '@material-ui/core';
 import { action, computed, observable, runInAction } from 'mobx';
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index da10bb0dc..a2c1195cb 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -1669,8 +1669,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
     };
 
     onKeyDown = (e: React.KeyboardEvent) => {
-        if (e.altKey) {
+        if ((e.altKey || e.ctrlKey) && e.key === 't') {
             e.preventDefault();
+            e.stopPropagation();
+            this.props.setTitleFocus?.();
             return;
         }
         const state = this._editorView!.state;
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index 40da482d2..4d82551db 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -698,7 +698,7 @@ export namespace Doc {
     // this lists out all the tag ids that can be  in a RichTextField that might contain document ids.
     // if a document is cloned, we need to make sure to clone all of these referenced documents as well;
     export const DocsInTextFieldIds = ['audioId', 'textId', 'anchorId', 'docId'];
-    export async function makeClone(doc: Doc, cloneMap: Map<string, Doc>, linkMap: Map<string, Doc>, rtfs: { copy: Doc; key: string; field: RichTextField }[], exclusions: string[], cloneLinks: boolean, asBranch: boolean): Promise<Doc> {
+    export async function makeClone(doc: Doc, cloneMap: Map<string, Doc>, linkMap: Map<string, Doc>, rtfs: { copy: Doc; key: string; field: RichTextField }[], exclusions: string[], cloneLinks: boolean): Promise<Doc> {
         if (Doc.IsBaseProto(doc)) return doc;
         if (cloneMap.get(doc[Id])) return cloneMap.get(doc[Id])!;
         const copy = new Doc(undefined, true);
@@ -714,7 +714,7 @@ export namespace Doc {
                     const list = await Cast(doc[key], listSpec(Doc));
                     const docs = list && (await DocListCastAsync(list))?.filter(d => d instanceof Doc);
                     if (docs !== undefined && docs.length) {
-                        const clones = await Promise.all(docs.map(async d => Doc.makeClone(d, cloneMap, linkMap, rtfs, exclusions, cloneLinks, asBranch)));
+                        const clones = await Promise.all(docs.map(async d => Doc.makeClone(d, cloneMap, linkMap, rtfs, exclusions, cloneLinks)));
                         assignKey(new List<Doc>(clones));
                     } else {
                         assignKey(ObjectField.MakeCopy(field));
@@ -731,7 +731,7 @@ export namespace Doc {
                                 );
                                 const results = docids && (await DocServer.GetRefFields(docids));
                                 const docs = results && Array.from(Object.keys(results)).map(key => DocCast(results[key]));
-                                docs && docs.map(doc => Doc.makeClone(doc, cloneMap, linkMap, rtfs, exclusions, cloneLinks, asBranch));
+                                docs && docs.map(doc => Doc.makeClone(doc, cloneMap, linkMap, rtfs, exclusions, cloneLinks));
                                 rtfs.push({ copy, key, field });
                             }
                         }
@@ -740,7 +740,7 @@ export namespace Doc {
                 const docAtKey = doc[key];
                 if (docAtKey instanceof Doc) {
                     if (!Doc.IsSystem(docAtKey) && (key.startsWith('layout') || key === 'annotationOn' || key === 'proto' || ((key === 'anchor1' || key === 'anchor2') && doc.author === Doc.CurrentUserEmail))) {
-                        assignKey(await Doc.makeClone(docAtKey, cloneMap, linkMap, rtfs, exclusions, cloneLinks, asBranch));
+                        assignKey(await Doc.makeClone(docAtKey, cloneMap, linkMap, rtfs, exclusions, cloneLinks));
                     } else {
                         assignKey(docAtKey);
                     }
@@ -763,14 +763,11 @@ export namespace Doc {
                 cloneLinks ||
                 ((cloneMap.has(DocCast(link.anchor1)?.[Id]) || cloneMap.has(DocCast(DocCast(link.anchor1)?.annotationOn)?.[Id])) && (cloneMap.has(DocCast(link.anchor2)?.[Id]) || cloneMap.has(DocCast(DocCast(link.anchor2)?.annotationOn)?.[Id])))
             ) {
-                linkMap.set(link[Id], await Doc.makeClone(link, cloneMap, linkMap, rtfs, exclusions, cloneLinks, asBranch));
+                linkMap.set(link[Id], await Doc.makeClone(link, cloneMap, linkMap, rtfs, exclusions, cloneLinks));
             }
         });
-        Doc.SetInPlace(copy, 'title', (asBranch ? 'BRANCH: ' : 'CLONE: ') + doc.title, true);
-        asBranch ? (copy.branchOf = doc) : (copy.cloneOf = doc);
-        if (!Doc.IsPrototype(copy)) {
-            Doc.AddDocToList(doc, 'branches', Doc.GetProto(copy));
-        }
+        Doc.SetInPlace(copy, 'title', 'CLONE: ' + doc.title, true);
+        copy.cloneOf = doc;
         cloneMap.set(doc[Id], copy);
 
         Doc.AddFileOrphan(copy);
@@ -794,14 +791,15 @@ export namespace Doc {
                 }
             });
     }
-    export function MakeClones(docs: Doc[], cloneLinks: boolean, asBranch = false, cloneMap: Map<string, Doc> = new Map()) {
-        return docs.map(doc => Doc.MakeClone(doc, cloneLinks, asBranch, cloneMap));
+    export function MakeClones(docs: Doc[], cloneLinks: boolean) {
+        const cloneMap = new Map<string, Doc>();
+        return docs.map(doc => Doc.MakeClone(doc, cloneLinks, cloneMap));
     }
 
-    export async function MakeClone(doc: Doc, cloneLinks = true, asBranch = false, cloneMap: Map<string, Doc> = new Map()) {
+    export async function MakeClone(doc: Doc, cloneLinks = true, cloneMap: Map<string, Doc> = new Map()) {
         const linkMap = new Map<string, Doc>();
         const rtfMap: { copy: Doc; key: string; field: RichTextField }[] = [];
-        const copy = await Doc.makeClone(doc, cloneMap, linkMap, rtfMap, ['cloneOf', 'branches', 'branchOf'], cloneLinks, asBranch);
+        const copy = await Doc.makeClone(doc, cloneMap, linkMap, rtfMap, ['cloneOf'], cloneLinks);
         const repaired = new Set<Doc>();
         const linkedDocs = Array.from(linkMap.values());
         const clonedDocs = [...Array.from(cloneMap.values()), ...linkedDocs];
-- 
cgit v1.2.3-70-g09d2

