From af380979349308077e13fc12a2d09255b7f05f28 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Tue, 23 Jan 2024 16:11:42 -0500
Subject: reorganization of DocumentView, DocumentViewInternal and FieldView
 methods and props. fix for selection bug after following a link.  migrating
 to use [DocData] instad of GetProto()

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

(limited to 'src/client/views/nodes/FontIconBox/FontIconBox.tsx')

diff --git a/src/client/views/nodes/FontIconBox/FontIconBox.tsx b/src/client/views/nodes/FontIconBox/FontIconBox.tsx
index 5a8665aaf..cf07d98be 100644
--- a/src/client/views/nodes/FontIconBox/FontIconBox.tsx
+++ b/src/client/views/nodes/FontIconBox/FontIconBox.tsx
@@ -46,7 +46,7 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() {
         return FieldView.LayoutString(FontIconBox, fieldKey);
     }
 
-    constructor(props: any) {
+    constructor(props: ButtonProps) {
         super(props);
         makeObservable(this);
     }
-- 
cgit v1.2.3-70-g09d2


From e3709b4445732791f696cdf26274ab09294ce208 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Sat, 27 Jan 2024 04:21:08 -0500
Subject: made dataViz nodes linked to schema nodes update automatically as
 cahnges are made.  fixed user created templates from disappearing from menu,
 and made them work.  added toJavascriptString and made DashField views
 convert to text.  added support for turning text into javascript rendering
 (paint) code.

---
 src/client/documents/Documents.ts                  |  2 +-
 src/client/util/CurrentUserUtils.ts                | 12 +++--
 src/client/util/DropConverter.ts                   |  1 +
 src/client/util/UndoManager.ts                     |  2 +-
 src/client/views/ContextMenuItem.tsx               | 38 ++++++-------
 src/client/views/DocComponent.tsx                  |  6 +--
 src/client/views/DocumentDecorations.tsx           | 16 ++++--
 src/client/views/EditableView.tsx                  |  9 ++--
 .../collectionFreeForm/CollectionFreeFormView.scss | 48 +++++++++--------
 .../collectionFreeForm/CollectionFreeFormView.tsx  | 21 +++++++-
 .../collectionSchema/CollectionSchemaView.tsx      |  3 +-
 .../views/global/globalCssVariables.module.scss    |  2 +
 .../global/globalCssVariables.module.scss.d.ts     |  1 +
 src/client/views/linking/LinkMenuItem.tsx          |  2 +-
 src/client/views/nodes/DataVizBox/DataVizBox.tsx   | 63 +++++++++++-----------
 .../views/nodes/DataVizBox/components/Chart.scss   |  1 -
 src/client/views/nodes/DocumentView.tsx            |  6 +--
 src/client/views/nodes/FontIconBox/FontIconBox.tsx |  8 ++-
 .../views/nodes/formattedText/DashFieldView.tsx    | 13 +++--
 .../views/nodes/formattedText/FormattedTextBox.tsx | 34 ++++++++++--
 src/client/views/nodes/formattedText/nodes_rts.ts  |  3 ++
 src/fields/CursorField.ts                          |  5 +-
 src/fields/DateField.ts                            |  5 +-
 src/fields/Doc.ts                                  | 39 +++++++++-----
 src/fields/FieldSymbols.ts                         |  1 +
 src/fields/HtmlField.ts                            | 15 +++---
 src/fields/IconField.ts                            | 17 +++---
 src/fields/InkField.ts                             |  5 +-
 src/fields/List.ts                                 |  5 +-
 src/fields/ObjectField.ts                          |  3 +-
 src/fields/Proxy.ts                                |  5 +-
 src/fields/RefField.ts                             | 11 ++--
 src/fields/RichTextField.ts                        |  5 +-
 src/fields/SchemaHeaderField.ts                    |  5 +-
 src/fields/ScriptField.ts                          |  5 +-
 src/fields/URLField.ts                             |  8 ++-
 36 files changed, 273 insertions(+), 152 deletions(-)

(limited to 'src/client/views/nodes/FontIconBox/FontIconBox.tsx')

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index ee3bf0d82..f1a0b37b3 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -211,7 +211,7 @@ export class DocumentOptions {
     author?: string; // STRt = new StrInfo('creator of document'); // bcz: don't change this. Otherwise, the userDoc's field Infos will have a FieldInfo assigned to its author field which will render it unreadable
     author_date?: DATEt = new DateInfo('date the document was created', true);
     annotationOn?: DOCt = new DocInfo('document annotated by this document', false);
-    _embedContainer?: DOCt = new DocInfo('document that displays (contains) this discument', false);
+    _embedContainer?: DOCt = new DocInfo('document that displays (contains) this document', false);
     rootDocument?: DOCt = new DocInfo('document that supplies the information needed for a rendering template (eg, pres slide for PresElement)');
     color?: STRt = new StrInfo('foreground color data doc', false);
     hidden?: BOOLt = new BoolInfo('whether the document is not rendered by its collection', false);
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index 6ddb65941..8f46f844c 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -61,7 +61,7 @@ export let resolvedPorts: { server: number, socket: number };
 export class CurrentUserUtils {
 
     // initializes experimental advanced template views - slideView,  headerView
-    static setupExperimentalTemplateButtons(doc: Doc, tempDocs?:Doc) {
+    static setupExperimentalTemplateButtons(doc: Doc, tempDocs:Opt<Doc>, userBtns:Doc[]) {
         const requiredTypeNameFields:{btnOpts:DocumentOptions, templateOpts:DocumentOptions, template:(opts:DocumentOptions) => Doc}[] = [
             {
                 btnOpts: { title: "slide", icon: "address-card" }, 
@@ -83,7 +83,7 @@ export class CurrentUserUtils {
                 )
             },
         ];
-        const requiredTypes = requiredTypeNameFields.map(({ btnOpts, template, templateOpts }) => {
+        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,this.openFactoryAsDelegate); }' };
             const assignBtnAndTempOpts = (templateBtn:Opt<Doc>, btnOpts:DocumentOptions, templateOptions:DocumentOptions) => {
@@ -94,7 +94,7 @@ export class CurrentUserUtils {
                 return templateBtn;
             };
             return DocUtils.AssignScripts(assignBtnAndTempOpts(tempBtn, btnOpts, templateOpts) ?? this.createToolButton( {...btnOpts, dragFactory: MakeTemplate(template(templateOpts))}), reqdScripts);
-        });
+        }), ...userBtns];
 
         const reqdOpts:DocumentOptions = {
             title: "Experimental Tools", _xMargin: 0, _layout_showTitle: "title", _chromeHidden: true,
@@ -201,7 +201,7 @@ export class CurrentUserUtils {
             makeIconTemplate(DocumentType.AUDIO, "title", { iconTemplate:DocumentType.LABEL, backgroundColor: "lightgreen"}),
             makeIconTemplate(DocumentType.PDF,   "title", { iconTemplate:DocumentType.LABEL, backgroundColor: "pink"}),
             makeIconTemplate(DocumentType.WEB,   "title", { iconTemplate:DocumentType.LABEL, backgroundColor: "brown"}),
-            makeIconTemplate(DocumentType.RTF,   "text",  { iconTemplate:DocumentType.LABEL, _layout_showTitle: "author_date"}),
+            makeIconTemplate(DocumentType.RTF,   "text",  { iconTemplate:DocumentType.LABEL, _layout_showTitle: "title"}),
             makeIconTemplate(DocumentType.IMG,   "data",  { iconTemplate:DocumentType.IMG,   _height: undefined}),
             makeIconTemplate(DocumentType.COL,   "icon",  { iconTemplate:DocumentType.IMG}),
             makeIconTemplate(DocumentType.COL,   "icon",  { iconTemplate:DocumentType.IMG}),
@@ -465,7 +465,9 @@ export class CurrentUserUtils {
     static setupToolsBtnPanel(doc: Doc, field:string) {
         const myTools = DocCast(doc[field]);
         const creatorBtns = CurrentUserUtils.setupCreatorButtons(doc, DocListCast(myTools?.data)?.length ? DocListCast(myTools.data)[0]:undefined);
-        const templateBtns = CurrentUserUtils.setupExperimentalTemplateButtons(doc,DocListCast(myTools?.data)?.length > 1 ? DocListCast(myTools.data)[1]:undefined);
+        const tempBtns = DocListCast(myTools?.data)?.length > 1 ? DocListCast(myTools.data)[1]:undefined;
+        const userTemplateBtns = DocListCast(tempBtns?.data).filter(btn => !btn.isSystem);
+        const templateBtns = CurrentUserUtils.setupExperimentalTemplateButtons(doc, tempBtns, userTemplateBtns);
         const reqdToolOps:DocumentOptions = {
             title: "My Tools", isSystem: true, ignoreClick: true, layout_boxShadow: "0 0",
             layout_explainer: "This is a palette of documents that can be created.",
diff --git a/src/client/util/DropConverter.ts b/src/client/util/DropConverter.ts
index f62ec8f83..8c3b56452 100644
--- a/src/client/util/DropConverter.ts
+++ b/src/client/util/DropConverter.ts
@@ -80,6 +80,7 @@ export function convertDropDataToButtons(data: DragManager.DocumentDragData) {
                 title: StrCast(layoutDoc.title),
                 btnType: ButtonType.ClickButton,
                 icon: 'bolt',
+                isSystem: false,
             });
             dbox.dragFactory = layoutDoc;
             dbox.dropPropertiesToRemove = doc.dropPropertiesToRemove instanceof ObjectField ? ObjectField.MakeCopy(doc.dropPropertiesToRemove) : undefined;
diff --git a/src/client/util/UndoManager.ts b/src/client/util/UndoManager.ts
index 9a6719ea5..421855bf3 100644
--- a/src/client/util/UndoManager.ts
+++ b/src/client/util/UndoManager.ts
@@ -102,7 +102,7 @@ export namespace UndoManager {
                     'UndoEvent : ' +
                     event.prop +
                     ' = ' +
-                    (value instanceof RichTextField ? value.Text : value instanceof Array ? value.map(val => Field.toScriptString(val)).join(',') : Field.toScriptString(value))
+                    (value instanceof RichTextField ? value.Text : value instanceof Array ? value.map(val => Field.toJavascriptString(val)).join(',') : Field.toJavascriptString(value))
             );
             currentBatch.push(event);
             tempEvents?.push(event);
diff --git a/src/client/views/ContextMenuItem.tsx b/src/client/views/ContextMenuItem.tsx
index 3c9d821a9..b2076e1a5 100644
--- a/src/client/views/ContextMenuItem.tsx
+++ b/src/client/views/ContextMenuItem.tsx
@@ -38,16 +38,16 @@ export class ContextMenuItem extends ObservableReactComponent<ContextMenuProps &
 
     componentDidMount() {
         runInAction(() => (this._items.length = 0));
-        if ((this.props as SubmenuProps)?.subitems) {
-            (this.props as SubmenuProps).subitems?.forEach(i => runInAction(() => this._items.push(i)));
+        if ((this._props as SubmenuProps)?.subitems) {
+            (this._props as SubmenuProps).subitems?.forEach(i => runInAction(() => this._items.push(i)));
         }
     }
 
     handleEvent = async (e: React.MouseEvent<HTMLDivElement>) => {
-        if ('event' in this.props) {
-            this.props.closeMenu?.();
-            const batch = this.props.undoable !== false ? UndoManager.StartBatch(`Click Menu item: ${this.props.description}`) : undefined;
-            await this.props.event({ x: e.clientX, y: e.clientY });
+        if ('event' in this._props) {
+            this._props.closeMenu?.();
+            const batch = this._props.undoable !== false ? UndoManager.StartBatch(`Click Menu item: ${this._props.description}`) : undefined;
+            await this._props.event({ x: e.clientX, y: e.clientY });
             batch?.end();
         }
     };
@@ -91,11 +91,11 @@ export class ContextMenuItem extends ObservableReactComponent<ContextMenuProps &
     }
 
     render() {
-        if ('event' in this.props) {
+        if ('event' in this._props) {
             return (
-                <div className={'contextMenu-item' + (this.props.selected ? ' contextMenu-itemSelected' : '')} onPointerDown={this.handleEvent}>
-                    {this.props.icon ? <span className="contextMenu-item-icon-background">{this.isJSXElement(this.props.icon) ? this.props.icon : <FontAwesomeIcon icon={this.props.icon} size="sm" />}</span> : null}
-                    <div className="contextMenu-description">{this.props.description.replace(':', '')}</div>
+                <div className={'contextMenu-item' + (this._props.selected ? ' contextMenu-itemSelected' : '')} onPointerDown={this.handleEvent}>
+                    {this._props.icon ? <span className="contextMenu-item-icon-background">{this.isJSXElement(this._props.icon) ? this._props.icon : <FontAwesomeIcon icon={this._props.icon} size="sm" />}</span> : null}
+                    <div className="contextMenu-description">{this._props.description.replace(':', '')}</div>
                     <div
                         className={`contextMenu-item-background`}
                         style={{
@@ -104,7 +104,7 @@ export class ContextMenuItem extends ObservableReactComponent<ContextMenuProps &
                     />
                 </div>
             );
-        } else if ('subitems' in this.props) {
+        } else if ('subitems' in this._props) {
             const where = !this.overItem ? '' : this._overPosY < window.innerHeight / 3 ? 'flex-start' : this._overPosY > (window.innerHeight * 2) / 3 ? 'flex-end' : 'center';
             const marginTop = !this.overItem ? '' : this._overPosY < window.innerHeight / 3 ? '20px' : this._overPosY > (window.innerHeight * 2) / 3 ? '-20px' : '';
 
@@ -118,32 +118,32 @@ export class ContextMenuItem extends ObservableReactComponent<ContextMenuProps &
                         background: SettingsManager.userBackgroundColor,
                     }}>
                     {this._items.map(prop => (
-                        <ContextMenuItem {...prop} key={prop.description} closeMenu={this.props.closeMenu} />
+                        <ContextMenuItem {...prop} key={prop.description} closeMenu={this._props.closeMenu} />
                     ))}
                 </div>
             );
-            if (!(this.props as SubmenuProps).noexpand) {
+            if (!(this._props as SubmenuProps).noexpand) {
                 return (
                     <div className="contextMenu-inlineMenu">
                         {this._items.map(prop => (
-                            <ContextMenuItem {...prop} key={prop.description} closeMenu={this.props.closeMenu} />
+                            <ContextMenuItem {...prop} key={prop.description} closeMenu={this._props.closeMenu} />
                         ))}
                     </div>
                 );
             }
             return (
                 <div
-                    className={'contextMenu-item' + (this.props.selected ? ' contextMenu-itemSelected' : '')}
-                    style={{ alignItems: where, borderTop: this.props.addDivider ? 'solid 1px' : undefined }}
+                    className={'contextMenu-item' + (this._props.selected ? ' contextMenu-itemSelected' : '')}
+                    style={{ alignItems: where, borderTop: this._props.addDivider ? 'solid 1px' : undefined }}
                     onMouseLeave={this.onPointerLeave}
                     onMouseEnter={this.onPointerEnter}>
-                    {this.props.icon ? (
+                    {this._props.icon ? (
                         <span className="contextMenu-item-icon-background" onMouseEnter={this.onPointerLeave} style={{ alignItems: 'center', alignSelf: 'center' }}>
-                            <FontAwesomeIcon icon={this.props.icon} size="sm" />
+                            <FontAwesomeIcon icon={this._props.icon} size="sm" />
                         </span>
                     ) : null}
                     <div className="contextMenu-description" onMouseEnter={this.onPointerEnter} style={{ alignItems: 'center', alignSelf: 'center' }}>
-                        {this.props.description}
+                        {this._props.description}
                         <FontAwesomeIcon icon={'angle-right'} size="lg" style={{ position: 'absolute', right: '10px' }} />
                     </div>
                     <div
diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx
index b21b13e4c..73fa6709c 100644
--- a/src/client/views/DocComponent.tsx
+++ b/src/client/views/DocComponent.tsx
@@ -188,8 +188,8 @@ export function ViewBoxAnnotatableComponent<P extends FieldViewProps>() {
                 const recent = this.Document !== Doc.MyRecentlyClosed ? Doc.MyRecentlyClosed : undefined;
                 toRemove.forEach(doc => {
                     leavePushpin && DocUtils.LeavePushpin(doc, annotationKey ?? this.annotationKey);
-                    Doc.RemoveDocFromList(targetDataDoc, annotationKey ?? this.annotationKey, doc);
-                    Doc.RemoveDocFromList(doc[DocData], 'proto_embeddings', doc);
+                    Doc.RemoveDocFromList(targetDataDoc, annotationKey ?? this.annotationKey, doc, true);
+                    Doc.RemoveDocFromList(doc[DocData], 'proto_embeddings', doc, true);
                     doc.embedContainer = undefined;
                     if (recent && !dontAddToRemoved) {
                         doc.type !== DocumentType.LOADING && Doc.AddDocToList(recent, 'data', doc, undefined, true, true);
@@ -243,7 +243,7 @@ export function ViewBoxAnnotatableComponent<P extends FieldViewProps>() {
                         inheritParentAcls(targetDataDoc, doc, true);
                     });
 
-                    const annoDocs = targetDataDoc[annotationKey ?? this.annotationKey] as List<Doc>;
+                    const annoDocs = Doc.Get(targetDataDoc, annotationKey ?? this.annotationKey, true) as List<Doc>; // get the dataDoc directly ... when using templates there may be some default items already there, but we can't change them.  maybe we should copy them over, though...
                     if (annoDocs instanceof List) annoDocs.push(...added.filter(add => !annoDocs.includes(add)));
                     else targetDataDoc[annotationKey ?? this.annotationKey] = new List<Doc>(added);
                     targetDataDoc[(annotationKey ?? this.annotationKey) + '_modificationDate'] = new DateField();
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 5ef62b2c5..2193acf62 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -18,7 +18,6 @@ import { DocumentType } from '../documents/DocumentTypes';
 import { Docs } from '../documents/Documents';
 import { DocumentManager } from '../util/DocumentManager';
 import { DragManager } from '../util/DragManager';
-import { LinkFollower } from '../util/LinkFollower';
 import { SelectionManager } from '../util/SelectionManager';
 import { SettingsManager } from '../util/SettingsManager';
 import { SnappingManager } from '../util/SnappingManager';
@@ -112,7 +111,6 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
 
     @action
     titleBlur = () => {
-        this._editingTitle = false;
         if (this._accumulatedTitle.startsWith('#') || this._accumulatedTitle.startsWith('=')) {
             this._titleControlString = this._accumulatedTitle;
         } else if (this._titleControlString.startsWith('#')) {
@@ -616,7 +614,7 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
         if (SelectionManager.Views.length === 1) {
             const selected = SelectionManager.Views[0];
             if (this._titleControlString.startsWith('=')) {
-                return ScriptField.MakeFunction(this._titleControlString.substring(1), { doc: Doc.name })!.script.run({ self: selected.Document, this: selected.layoutDoc }, console.log).result?.toString() || '';
+                return ScriptField.MakeFunction(this._titleControlString.substring(1), { doc: Doc.name })?.script.run({ self: selected.Document, this: selected.layoutDoc }, console.log).result?.toString() || '';
             }
             if (this._titleControlString.startsWith('#')) {
                 return Field.toString(selected.Document[this._titleControlString.substring(1)] as Field) || '-unset-';
@@ -641,7 +639,12 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
         const { b, r, x, y } = this.Bounds;
         const seldocview = SelectionManager.Views.lastElement();
         if (SnappingManager.IsDragging || r - x < 1 || x === Number.MAX_VALUE || !seldocview || this._hidden || isNaN(r) || isNaN(b) || isNaN(x) || isNaN(y)) {
-            setTimeout(action(() => (this._showNothing = true)));
+            setTimeout(
+                action(() => {
+                    this._editingTitle = false;
+                    this._showNothing = true;
+                })
+            );
             return null;
         }
 
@@ -726,7 +729,10 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
                 name="dynbox"
                 autoComplete="on"
                 value={hideTitle ? '' : this._accumulatedTitle}
-                onBlur={e => !hideTitle && this.titleBlur()}
+                onBlur={action((e: React.FocusEvent) => {
+                    this._editingTitle = false;
+                    !hideTitle && this.titleBlur();
+                })}
                 onChange={action(e => !hideTitle && (this._accumulatedTitle = e.target.value))}
                 onKeyDown={hideTitle ? emptyFunction : this.titleEntered}
                 onPointerDown={e => e.stopPropagation()}
diff --git a/src/client/views/EditableView.tsx b/src/client/views/EditableView.tsx
index c6666d79d..4508d00a7 100644
--- a/src/client/views/EditableView.tsx
+++ b/src/client/views/EditableView.tsx
@@ -1,4 +1,4 @@
-import { action, IReactionDisposer, makeObservable, observable, reaction } from 'mobx';
+import { action, IReactionDisposer, makeObservable, observable, reaction, runInAction } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
 import * as Autosuggest from 'react-autosuggest';
@@ -97,9 +97,10 @@ export class EditableView extends ObservableReactComponent<EditableProps> {
         super.componentDidUpdate(prevProps);
         if (this._editing && this._props.editing === false) {
             this._inputref?.value && this.finalizeEdit(this._inputref.value, false, true, false);
-        } else if (this._props.editing !== undefined) {
-            this._editing = this._props.editing;
-        }
+        } else
+            runInAction(() => {
+                if (this._props.editing !== undefined) this._editing = this._props.editing;
+            });
     }
 
     componentWillUnmount() {
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
index 7d3acaea7..9e7d364ea 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
@@ -176,6 +176,11 @@
     // touch action none means that the browser will handle none of the touch actions. this allows us to implement our own actions.
     touch-action: none;
     transform-origin: top left;
+    > svg {
+        height: 100%;
+        width: 100%;
+        margin: auto;
+    }
 
     .collectionfreeformview-placeholder {
         background: gray;
@@ -270,34 +275,31 @@
     padding: 10px;
 
     .msg {
-      position: relative;
-      // display: block;
-      -webkit-user-select: none;
-      -khtml-user-select: none;
-      -moz-user-select: none;
-      -o-user-select: none;
-      user-select: none;
-      
+        position: relative;
+        // display: block;
+        -webkit-user-select: none;
+        -khtml-user-select: none;
+        -moz-user-select: none;
+        -o-user-select: none;
+        user-select: none;
     }
-    
+
     .gif-container {
-      position: relative;
-      margin-top: 5px;
-      // display: block;
-
-      justify-content: center;
-      align-items: center;
-      -webkit-user-select: none;
-      -khtml-user-select: none;
-      -moz-user-select: none;
-      -o-user-select: none;
-      user-select: none;
-
-      
+        position: relative;
+        margin-top: 5px;
+        // display: block;
+
+        justify-content: center;
+        align-items: center;
+        -webkit-user-select: none;
+        -khtml-user-select: none;
+        -moz-user-select: none;
+        -o-user-select: none;
+        user-select: none;
+
         .gif {
             background-color: transparent;
             height: 300px;
         }
     }
-
 }
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 53dc389b4..be19fc50f 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -39,7 +39,7 @@ import { LightboxView } from '../../LightboxView';
 import { CollectionFreeFormDocumentView, CollectionFreeFormDocumentViewWrapper } from '../../nodes/CollectionFreeFormDocumentView';
 import { SchemaCSVPopUp } from '../../nodes/DataVizBox/SchemaCSVPopUp';
 import { DocumentView, OpenWhere } from '../../nodes/DocumentView';
-import { FocusViewOptions, FieldViewProps } from '../../nodes/FieldView';
+import { FieldViewProps, FocusViewOptions } from '../../nodes/FieldView';
 import { FormattedTextBox } from '../../nodes/formattedText/FormattedTextBox';
 import { PinProps, PresBox } from '../../nodes/trails/PresBox';
 import { CreateImage } from '../../nodes/WebBoxRenderer';
@@ -73,6 +73,14 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         return 'CollectionFreeFormView(' + this.Document.title?.toString() + ')';
     } // this makes mobx trace() statements more descriptive
 
+    @observable _paintedId = 'id' + Utils.GenerateGuid().replace(/-/g, '');
+    @computed get paintFunc() {
+        const paintFunc = StrCast(this.Document[DocData].paintFunc).trim();
+        return !paintFunc
+            ? ''
+            : `const dashDiv = document.querySelector('#${this._paintedId}'); 
+        (async () => {  ${paintFunc} })()`;
+    }
     constructor(props: any) {
         super(props);
         makeObservable(this);
@@ -1227,6 +1235,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                 onKey={this.onKeyDown}
                 onDoubleClickScript={this.onChildDoubleClickHandler}
                 onBrowseClickScript={this.onBrowseClickHandler}
+                bringToFront={this.bringToFront}
                 ScreenToLocalTransform={childLayout.z ? this.ScreenToLocalBoxXf : this.ScreenToContentsXf}
                 PanelWidth={childLayout[Width]}
                 PanelHeight={childLayout[Height]}
@@ -1482,6 +1491,13 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                 );
             })
         );
+
+        this._disposers.paintFunc = reaction(
+            () => ({ code: this.paintFunc, first: this._firstRender, width: this.Document._width, height: this.Document._height }),
+            ({ code, first }) => code && !first && eval(code),
+            { fireImmediately: true }
+        );
+
         this._disposers.layoutElements = reaction(
             // layoutElements can't be a computed value because doLayoutComputation() is an action that has side effect of updating clusters
             () => this.doInternalLayoutComputation,
@@ -1847,6 +1863,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         return (
             <div
                 className="collectionfreeformview-container"
+                id={this._paintedId}
                 ref={r => {
                     this.createDashEventsTarget(r);
                     this._oldWheel?.removeEventListener('wheel', this.onPassiveWheel);
@@ -1868,7 +1885,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                     width: `${100 / (this.nativeDimScaling || 1)}%`,
                     height: this._props.getScrollHeight?.() ?? `${100 / (this.nativeDimScaling || 1)}%`,
                 }}>
-                {this._lightboxDoc ? (
+                {this.paintFunc ? null : this._lightboxDoc ? (
                     <div style={{ padding: 15, width: '100%', height: '100%' }}>
                         <DocumentView
                             {...this._props}
diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx
index 581425d77..31b4a2dd4 100644
--- a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx
+++ b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx
@@ -25,6 +25,7 @@ import { CollectionSubView } from '../CollectionSubView';
 import './CollectionSchemaView.scss';
 import { SchemaColumnHeader } from './SchemaColumnHeader';
 import { SchemaRowBox } from './SchemaRowBox';
+const { default: { SCHEMA_NEW_NODE_HEIGHT } } = require('../../global/globalCssVariables.module.scss'); // prettier-ignore
 
 export enum ColumnType {
     Number,
@@ -69,7 +70,7 @@ export class CollectionSchemaView extends CollectionSubView() {
     public static _minColWidth: number = 25;
     public static _rowMenuWidth: number = 60;
     public static _previewDividerWidth: number = 4;
-    public static _newNodeInputHeight: number = 20;
+    public static _newNodeInputHeight: number = Number(SCHEMA_NEW_NODE_HEIGHT);
     public fieldInfos = new ObservableMap<string, FInfo>();
 
     @observable _menuKeys: string[] = [];
diff --git a/src/client/views/global/globalCssVariables.module.scss b/src/client/views/global/globalCssVariables.module.scss
index 44e8efe23..ad0c5c21d 100644
--- a/src/client/views/global/globalCssVariables.module.scss
+++ b/src/client/views/global/globalCssVariables.module.scss
@@ -64,6 +64,7 @@ $mainTextInput-zindex: 999; // then text input overlay so that it's context menu
 $docDecorations-zindex: 998; // then doc decorations appear over everything else
 $remoteCursors-zindex: 997; // ... not sure what level the remote cursors should go -- is this right?
 $SCHEMA_DIVIDER_WIDTH: 4;
+$SCHEMA_NEW_NODE_HEIGHT: 30;
 $MINIMIZED_ICON_SIZE: 24;
 $MAX_ROW_HEIGHT: 44px;
 $DFLT_IMAGE_NATIVE_DIM: 900px;
@@ -78,6 +79,7 @@ $DATA_VIZ_TABLE_ROW_HEIGHT: 30;
 
 :export {
     contextMenuZindex: $contextMenu-zindex;
+    SCHEMA_NEW_NODE_HEIGHT: $SCHEMA_NEW_NODE_HEIGHT;
     SCHEMA_DIVIDER_WIDTH: $SCHEMA_DIVIDER_WIDTH;
     MINIMIZED_ICON_SIZE: $MINIMIZED_ICON_SIZE;
     MAX_ROW_HEIGHT: $MAX_ROW_HEIGHT;
diff --git a/src/client/views/global/globalCssVariables.module.scss.d.ts b/src/client/views/global/globalCssVariables.module.scss.d.ts
index bcbb1f068..12cc3fc89 100644
--- a/src/client/views/global/globalCssVariables.module.scss.d.ts
+++ b/src/client/views/global/globalCssVariables.module.scss.d.ts
@@ -1,5 +1,6 @@
 interface IGlobalScss {
     contextMenuZindex: string; // context menu shows up over everything
+    SCHEMA_NEW_NODE_HEIGHT: string;
     SCHEMA_DIVIDER_WIDTH: string;
     MINIMIZED_ICON_SIZE: string;
     MAX_ROW_HEIGHT: string;
diff --git a/src/client/views/linking/LinkMenuItem.tsx b/src/client/views/linking/LinkMenuItem.tsx
index dc4aee1ca..ad6deeefb 100644
--- a/src/client/views/linking/LinkMenuItem.tsx
+++ b/src/client/views/linking/LinkMenuItem.tsx
@@ -136,7 +136,7 @@ export class LinkMenuItem extends ObservableReactComponent<LinkMenuItemProps> {
 
     deleteLink = (e: React.PointerEvent): void => setupMoveUpEvents(this, e, returnFalse, emptyFunction, undoBatch(action(() => LinkManager.Instance.deleteLink(this._props.linkDoc))));
     @observable _hover = false;
-    docView = () => this.props.docView;
+    docView = () => this._props.docView;
     render() {
         const destinationIcon = Doc.toIcon(this._props.destinationDoc) as any as IconProp;
 
diff --git a/src/client/views/nodes/DataVizBox/DataVizBox.tsx b/src/client/views/nodes/DataVizBox/DataVizBox.tsx
index 8365f4770..e453bcee0 100644
--- a/src/client/views/nodes/DataVizBox/DataVizBox.tsx
+++ b/src/client/views/nodes/DataVizBox/DataVizBox.tsx
@@ -1,6 +1,6 @@
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { Colors, Toggle, ToggleType, Type } from 'browndash-components';
-import { ObservableMap, action, computed, makeObservable, observable, runInAction } from 'mobx';
+import { IReactionDisposer, ObservableMap, action, computed, makeObservable, observable, reaction, runInAction } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
 import { emptyFunction, returnEmptyString, returnFalse, returnOne, setupMoveUpEvents } from '../../../../Utils';
@@ -40,9 +40,9 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() im
     private _mainCont: React.RefObject<HTMLDivElement> = React.createRef();
     private _marqueeref = React.createRef<MarqueeAnnotator>();
     private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef();
+    private _disposers: { [name: string]: IReactionDisposer } = {};
     anchorMenuClick?: () => undefined | ((anchor: Doc) => void);
     crop: ((region: Doc | undefined, addCrop?: boolean) => Doc | undefined) | undefined;
-    @observable _schemaDataVizChildren: any = undefined;
     @observable _marqueeing: number[] | undefined = undefined;
     @observable _savedAnnotations = new ObservableMap<number, HTMLDivElement[]>();
 
@@ -256,12 +256,39 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() im
     componentDidMount() {
         this._props.setContentViewBox?.(this);
         if (!DataVizBox.dataset.has(CsvCast(this.dataDoc[this.fieldKey]).url.href)) this.fetchData();
+        this._disposers.datavis = reaction(
+            () => {
+                const getFrom = DocCast(this.layoutDoc.dataViz_asSchema);
+                const keys = Cast(getFrom?.schema_columnKeys, listSpec('string'))?.filter(key => key != 'text');
+                if (!keys) return;
+                const children = DocListCast(getFrom[Doc.LayoutFieldKey(getFrom)]);
+                var current: { [key: string]: string }[] = [];
+                children
+                    .filter(child => child)
+                    .forEach(child => {
+                        const row: { [key: string]: string } = {};
+                        keys.forEach(key => {
+                            var cell = child[key];
+                            if (cell && (cell as string)) cell = cell.toString().replace(/\,/g, '');
+                            row[key] = StrCast(cell);
+                        });
+                        current.push(row);
+                    });
+                return current;
+            },
+            current => {
+                current && DataVizBox.dataset.set(CsvCast(this.Document[this.fieldKey]).url.href, current);
+            },
+            { fireImmediately: true }
+        );
     }
 
     fetchData = () => {
-        DataVizBox.dataset.set(CsvCast(this.dataDoc[this.fieldKey]).url.href, []); // assign temporary dataset as a lock to prevent duplicate server requests
-        fetch('/csvData?uri=' + this.dataUrl?.url.href) //
-            .then(res => res.json().then(action(res => !res.errno && DataVizBox.dataset.set(CsvCast(this.dataDoc[this.fieldKey]).url.href, res))));
+        if (!this.Document.dataViz_asSchema) {
+            DataVizBox.dataset.set(CsvCast(this.dataDoc[this.fieldKey]).url.href, []); // assign temporary dataset as a lock to prevent duplicate server requests
+            fetch('/csvData?uri=' + this.dataUrl?.url.href) //
+                .then(res => res.json().then(action(res => !res.errno && DataVizBox.dataset.set(CsvCast(this.dataDoc[this.fieldKey]).url.href, res))));
+        }
     };
 
     // toggles for user to decide which chart type to view the data in
@@ -327,33 +354,7 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() im
         GPTPopup.Instance.addDoc = this.sidebarAddDocument;
     };
 
-    @action
-    updateSchemaViz = () => {
-        const getFrom = DocCast(this.layoutDoc.dataViz_asSchema);
-        const keys = Cast(getFrom.schema_columnKeys, listSpec('string'))?.filter(key => key != 'text');
-        if (!keys) return;
-        const children = DocListCast(getFrom[Doc.LayoutFieldKey(getFrom)]);
-        var current: { [key: string]: string }[] = [];
-        for (let i = 0; i < children.length; i++) {
-            var row: { [key: string]: string } = {};
-            if (children[i]) {
-                for (let j = 0; j < keys.length; j++) {
-                    var cell = children[i][keys[j]];
-                    if (cell && (cell as string)) cell = cell.toString().replace(/\,/g, '');
-                    row[keys[j]] = StrCast(cell);
-                }
-            }
-            current.push(row);
-        }
-        DataVizBox.dataset.set(CsvCast(this.Document[this.fieldKey]).url.href, current);
-    };
-
     render() {
-        if (this.layoutDoc && this.layoutDoc.dataViz_asSchema) {
-            this._schemaDataVizChildren = DocListCast(DocCast(this.layoutDoc.dataViz_asSchema)[Doc.LayoutFieldKey(DocCast(this.layoutDoc.dataViz_asSchema))]).length;
-            this.updateSchemaViz();
-        }
-
         const scale = this._props.NativeDimScaling?.() || 1;
         return !this.records.length ? (
             // displays how to get data into the DataVizBox if its empty
diff --git a/src/client/views/nodes/DataVizBox/components/Chart.scss b/src/client/views/nodes/DataVizBox/components/Chart.scss
index 2f7dd0487..116a45623 100644
--- a/src/client/views/nodes/DataVizBox/components/Chart.scss
+++ b/src/client/views/nodes/DataVizBox/components/Chart.scss
@@ -93,7 +93,6 @@
     display: flex;
     flex-direction: column;
     cursor: default;
-    margin-top: 30px;
     height: calc(100% - 40px); // bcz: hack 40px is the size of the button rows
     .tableBox-container {
         overflow: scroll;
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index a82580ddb..2e1ba2a7e 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -592,13 +592,9 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
                 !Doc.noviceMode && onClicks.push({ description: 'Toggle Detail', event: this.setToggleDetail, icon: 'concierge-bell' });
 
                 if (!this.Document.annotationOn) {
-                    const options = cm.findByDescription('Options...');
-                    const optionItems: ContextMenuProps[] = options && 'subitems' in options ? options.subitems : [];
-                    !options && cm.addItem({ description: 'Options...', subitems: optionItems, icon: 'compass' });
-
                     onClicks.push({ description: this.onClickHandler ? 'Remove Click Behavior' : 'Follow Link', event: () => this.toggleFollowLink(false, false), icon: 'link' });
                     !Doc.noviceMode && onClicks.push({ description: 'Edit onClick Script', event: () => UndoManager.RunInBatch(() => DocUtils.makeCustomViewClicked(this.Document, undefined, 'onClick'), 'edit onClick'), icon: 'terminal' });
-                    !existingOnClick && cm.addItem({ description: 'OnClick...', noexpand: true, subitems: onClicks, icon: 'mouse-pointer' });
+                    cm.addItem({ description: 'OnClick...', noexpand: true, subitems: onClicks, icon: 'mouse-pointer' });
                 } else if (LinkManager.Links(this.Document).length) {
                     onClicks.push({ description: 'Restore On Click default', event: () => this.noOnClick(), icon: 'link' });
                     onClicks.push({ description: 'Follow Link on Click', event: () => this.followLinkOnClick(), icon: 'link' });
diff --git a/src/client/views/nodes/FontIconBox/FontIconBox.tsx b/src/client/views/nodes/FontIconBox/FontIconBox.tsx
index cf07d98be..8290e102c 100644
--- a/src/client/views/nodes/FontIconBox/FontIconBox.tsx
+++ b/src/client/views/nodes/FontIconBox/FontIconBox.tsx
@@ -73,7 +73,7 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() {
     };
 
     specificContextMenu = (): void => {
-        if (!Doc.noviceMode) {
+        if (!Doc.noviceMode && Cast(this.layoutDoc.dragFactory, Doc, null)) {
             const cm = ContextMenu.Instance;
             cm.addItem({ description: 'Show Template', event: this.showTemplate, icon: 'tag' });
             cm.addItem({ description: 'Use as Render Template', event: this.dragAsTemplate, icon: 'tag' });
@@ -366,7 +366,7 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() {
         );
     }
 
-    render() {
+    renderButton = () => {
         const color = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Color);
         const tooltip = StrCast(this.Document.toolTip);
         const scriptFunc = () => ScriptCast(this.Document.onClick)?.script.run({ this: this.Document, self: this.Document, _readOnly_: false });
@@ -389,5 +389,9 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() {
                         background={SettingsManager.userBackgroundColor} size={Size.LARGE} tooltipPlacement='right' onPointerDown={scriptFunc} />; 
         }
         return this.defaultButton;
+    };
+
+    render() {
+        return <div onContextMenu={this.specificContextMenu}>{this.renderButton()}</div>;
     }
 }
diff --git a/src/client/views/nodes/formattedText/DashFieldView.tsx b/src/client/views/nodes/formattedText/DashFieldView.tsx
index dc9914637..18286267a 100644
--- a/src/client/views/nodes/formattedText/DashFieldView.tsx
+++ b/src/client/views/nodes/formattedText/DashFieldView.tsx
@@ -1,10 +1,10 @@
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { Tooltip } from '@mui/material';
-import { action, computed, IReactionDisposer, makeObservable, observable } from 'mobx';
+import { action, computed, IReactionDisposer, makeObservable, observable, reaction } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
 import * as ReactDOM from 'react-dom/client';
-import { Doc } from '../../../../fields/Doc';
+import { Doc, Field } from '../../../../fields/Doc';
 import { List } from '../../../../fields/List';
 import { listSpec } from '../../../../fields/Schema';
 import { SchemaHeaderField } from '../../../../fields/SchemaHeaderField';
@@ -114,6 +114,13 @@ export class DashFieldViewInternal extends ObservableReactComponent<IDashFieldVi
         }
     }
 
+    componentDidMount() {
+        this._reactionDisposer = reaction(
+            () => (this._dashDoc ? Field.toKeyValueString(this._dashDoc, this._props.fieldKey) : undefined),
+            keyvalue => keyvalue && this._props.tbox.tryUpdateDoc(true)
+        );
+    }
+
     componentWillUnmount() {
         this._reactionDisposer?.();
     }
@@ -184,7 +191,7 @@ export class DashFieldViewInternal extends ObservableReactComponent<IDashFieldVi
                 }}>
                 {this._props.hideKey ? null : (
                     <span className="dashFieldView-labelSpan" title="click to see related tags" onPointerDown={this.onPointerDownLabelSpan}>
-                        {this._fieldKey}
+                        {(this._textBoxDoc === this._dashDoc ? '' : this._dashDoc?.title + ':') + this._fieldKey}
                     </span>
                 )}
 
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 731ab1d53..6f9c2893e 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -67,6 +67,7 @@ import { RichTextMenu, RichTextMenuPlugin } from './RichTextMenu';
 import { RichTextRules } from './RichTextRules';
 import { schema } from './schema_rts';
 import { SummaryView } from './SummaryView';
+import { CollectionView } from '../../collections/CollectionView';
 // import * as applyDevTools from 'prosemirror-dev-tools';
 @observer
 export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implements ViewBoxInterface {
@@ -325,13 +326,26 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
         } catch (e) {}
     };
 
+    leafText = (node: Node) => {
+        if (node.type === this._editorView?.state.schema.nodes.dashField) {
+            const refDoc = !node.attrs.docId ? this.Document : (DocServer.GetCachedRefField(node.attrs.docId as string) as Doc);
+            return Field.toJavascriptString(refDoc[node.attrs.fieldKey as string] as Field);
+        }
+        return '';
+    };
     dispatchTransaction = (tx: Transaction) => {
         if (this._editorView && (this._editorView as any).docView) {
             const state = this._editorView.state.apply(tx);
             this._editorView.updateState(state);
+            this.tryUpdateDoc(false);
+        }
+    };
 
+    tryUpdateDoc = (force: boolean) => {
+        if (this._editorView && (this._editorView as any).docView) {
+            const state = this._editorView.state;
             const dataDoc = Doc.IsDelegateField(DocCast(this.layoutDoc.proto), this.fieldKey) ? DocCast(this.layoutDoc.proto) : this.dataDoc;
-            const newText = state.doc.textBetween(0, state.doc.content.size, ' \n');
+            const newText = state.doc.textBetween(0, state.doc.content.size, ' \n', this.leafText);
             const newJson = JSON.stringify(state.toJSON());
             const prevData = Cast(this.layoutDoc[this.fieldKey], RichTextField, null); // the actual text in the text box
             const templateData = this.Document !== this.layoutDoc ? prevData : undefined; // the default text stored in a layout template
@@ -350,13 +364,13 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
                 dataDoc.tags = accumTags.length ? new List<string>(Array.from(new Set<string>(accumTags))) : undefined;
 
                 let unchanged = true;
-                if (this._applyingChange !== this.fieldKey && removeSelection(newJson) !== removeSelection(prevData?.Data)) {
+                if (this._applyingChange !== this.fieldKey && (force || removeSelection(newJson) !== removeSelection(prevData?.Data))) {
                     this._applyingChange = this.fieldKey;
                     const textChange = newText !== prevData?.Text;
                     textChange && (dataDoc[this.fieldKey + '_modificationDate'] = new DateField(new Date(Date.now())));
                     if ((!prevData && !protoData) || newText || (!newText && !templateData)) {
                         // if no template, or there's text that didn't come from the layout template, write it to the document. (if this is driven by a template, then this overwrites the template text which is intended)
-                        if ((this._finishingLink || this._props.isContentActive() || this._inDrop) && removeSelection(newJson) !== removeSelection(prevData?.Data)) {
+                        if (force || ((this._finishingLink || this._props.isContentActive() || this._inDrop) && removeSelection(newJson) !== removeSelection(prevData?.Data))) {
                             const numstring = NumCast(dataDoc[this.fieldKey], null);
                             dataDoc[this.fieldKey] = numstring !== undefined ? Number(newText) : new RichTextField(newJson, newText);
                             dataDoc[this.fieldKey + '_noTemplate'] = true; // mark the data field as being split from the template if it has been edited
@@ -480,7 +494,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
             var alink: Doc | undefined;
             this.findInNode(editorView, editorView.state.doc, autoLinkTerm).forEach(sel => {
                 const splitter = editorView.state.schema.marks.splitter.create({ id: Utils.GenerateGuid() });
-                if (!sel.$anchor.pos || editorView.state.doc.textBetween(sel.$anchor.pos - 1, sel.$to.pos).trim() === autoLinkTerm) {
+                if (!sel.$anchor.pos || [autoLinkTerm, StrCast(target.title)].includes(editorView.state.doc.textBetween(sel.$anchor.pos - 1, sel.$to.pos).trim())) {
                     tr = tr.addMark(sel.from, sel.to, splitter);
                     tr.doc.nodesBetween(sel.from, sel.to, (node: any, pos: number, parent: any) => {
                         if (node.firstChild === null && !node.marks.find((m: Mark) => m.type.name === schema.marks.noAutoLinkAnchor.name) && node.marks.find((m: Mark) => m.type.name === schema.marks.splitter.name)) {
@@ -919,6 +933,17 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
                 event: () => (this.layoutDoc._createDocOnCR = !this.layoutDoc._createDocOnCR),
                 icon: !this.Document._createDocOnCR ? 'grip-lines' : 'bars',
             });
+        optionItems.push({
+            description: 'Make Paint Function',
+            event: () => {
+                this.dataDoc.layout_painted = CollectionView.LayoutString('painted');
+                this.layoutDoc.layout_fieldKey = 'layout_painted';
+                this.layoutDoc.type_collection = CollectionViewType.Freeform;
+                this.DocumentView?.().setToggleDetail();
+                this.dataDoc.paintFunc = ComputedField.MakeFunction(`toJavascriptString(this['${this.fieldKey}']?.Text)`);
+            },
+            icon: !this.Document._layout_enableAltContentUI ? 'eye-slash' : 'eye',
+        });
         !Doc.noviceMode &&
             optionItems.push({
                 description: `${this.Document._layout_autoHeight ? 'Lock' : 'Auto'} Height`,
@@ -1682,7 +1707,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
         FormattedTextBox.LiveTextUndo = undefined;
 
         const state = this._editorView!.state;
-        const curText = state.doc.textBetween(0, state.doc.content.size, ' \n');
         if (StrCast(this.Document.title).startsWith('@') && !this.dataDoc.title_custom) {
             UndoManager.RunInBatch(() => {
                 this.dataDoc.title_custom = true;
diff --git a/src/client/views/nodes/formattedText/nodes_rts.ts b/src/client/views/nodes/formattedText/nodes_rts.ts
index d023020e1..31f001b11 100644
--- a/src/client/views/nodes/formattedText/nodes_rts.ts
+++ b/src/client/views/nodes/formattedText/nodes_rts.ts
@@ -2,6 +2,8 @@ import * as React from 'react';
 import { DOMOutputSpec, Node, NodeSpec } from 'prosemirror-model';
 import { listItem, orderedList } from 'prosemirror-schema-list';
 import { ParagraphNodeSpec, toParagraphDOM, getParagraphNodeAttrs } from './ParagraphNodeSpec';
+import { DocServer } from '../../../DocServer';
+import { Doc, Field } from '../../../../fields/Doc';
 
 const blockquoteDOM: DOMOutputSpec = ['blockquote', 0],
     hrDOM: DOMOutputSpec = ['hr'],
@@ -264,6 +266,7 @@ export const nodes: { [index: string]: NodeSpec } = {
             hideKey: { default: false },
             editable: { default: true },
         },
+        leafText: node => Field.toString((DocServer.GetCachedRefField(node.attrs.docId as string) as Doc)?.[node.attrs.fieldKey as string] as Field),
         group: 'inline',
         draggable: false,
         toDOM(node) {
diff --git a/src/fields/CursorField.ts b/src/fields/CursorField.ts
index 84917ae53..870dfbeac 100644
--- a/src/fields/CursorField.ts
+++ b/src/fields/CursorField.ts
@@ -1,6 +1,6 @@
 import { createSimpleSchema, object, serializable } from 'serializr';
 import { Deserializable } from '../client/util/SerializationHelper';
-import { Copy, FieldChanged, ToScriptString, ToString } from './FieldSymbols';
+import { Copy, FieldChanged, ToJavascriptString, ToScriptString, ToString } from './FieldSymbols';
 import { ObjectField } from './ObjectField';
 
 export type CursorPosition = {
@@ -55,6 +55,9 @@ export default class CursorField extends ObjectField {
         return new CursorField(this.data);
     }
 
+    [ToJavascriptString]() {
+        return 'invalid';
+    }
     [ToScriptString]() {
         return 'invalid';
     }
diff --git a/src/fields/DateField.ts b/src/fields/DateField.ts
index 2ea619bd9..1e5222fb6 100644
--- a/src/fields/DateField.ts
+++ b/src/fields/DateField.ts
@@ -1,7 +1,7 @@
 import { Deserializable } from '../client/util/SerializationHelper';
 import { serializable, date } from 'serializr';
 import { ObjectField } from './ObjectField';
-import { Copy, ToScriptString, ToString } from './FieldSymbols';
+import { Copy, ToJavascriptString, ToScriptString, ToString } from './FieldSymbols';
 import { scriptingGlobal, ScriptingGlobals } from '../client/util/ScriptingGlobals';
 
 @scriptingGlobal
@@ -23,6 +23,9 @@ export class DateField extends ObjectField {
         return `${this.date.toLocaleString()}`;
     }
 
+    [ToJavascriptString]() {
+        return `"${this.date.toISOString()}"`;
+    }
     [ToScriptString]() {
         return `new DateField(new Date("${this.date.toISOString()}"))`;
     }
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index ff416bbe7..f4141cf46 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -18,7 +18,7 @@ import {
     DocAcl, DocCss, DocData, DocFields, DocLayout, DocViews, FieldKeys, FieldTuples, ForceServerWrite, Height, Highlight,
     Initializing, Self, SelfProxy, UpdatingFromServer, Width
 } from './DocSymbols'; // prettier-ignore
-import { Copy, FieldChanged, HandleUpdate, Id, Parent, ToScriptString, ToString } from './FieldSymbols';
+import { Copy, FieldChanged, HandleUpdate, Id, Parent, ToJavascriptString, ToScriptString, ToString } from './FieldSymbols';
 import { InkField, InkTool } from './InkField';
 import { List, ListFieldName } from './List';
 import { ObjectField } from './ObjectField';
@@ -52,6 +52,23 @@ export namespace Field {
             default:       return field?.[ToScriptString]?.() ?? 'null';
         } // prettier-ignore
     }
+    export function toJavascriptString(field: Field) {
+        var rawjava = '';
+
+        switch (typeof field) {
+            case 'string':
+            case 'number':
+            case 'boolean':rawjava = String(field);
+                           break;
+            default:       rawjava = field?.[ToJavascriptString]?.() ?? 'null';
+        } // prettier-ignore
+        var script = rawjava;
+        Doc.MyPublishedDocs.forEach(doc => {
+            const regex = new RegExp(`^\\^${doc.title}\\s`, 'm');
+            script = script.replace(regex, Cast(doc.text, RichTextField, null)?.Text ?? '');
+        });
+        return script;
+    }
     export function toString(field: Field) {
         if (typeof field === 'string' || typeof field === 'number' || typeof field === 'boolean') return String(field);
         return field?.[ToString]?.() || '';
@@ -287,6 +304,7 @@ export class Doc extends RefField {
     public [DocFields] = () => this[Self][FieldTuples]; // Object.keys(this).reduce((fields, key) => { fields[key] = this[key]; return fields; }, {} as any);
     public [Width] = () => NumCast(this[SelfProxy]._width);
     public [Height] = () => NumCast(this[SelfProxy]._height);
+    public [ToJavascriptString] = () => `idToDoc("${this[Self][Id]}")`; // what should go here?
     public [ToScriptString] = () => `idToDoc("${this[Self][Id]}")`;
     public [ToString] = () => `Doc(${GetEffectiveAcl(this[SelfProxy]) === AclPrivate ? '-inaccessible-' : this[SelfProxy].title})`;
     public get [DocLayout]() { return this[SelfProxy].__LAYOUT__; } // prettier-ignore
@@ -508,12 +526,9 @@ export namespace Doc {
      * Removes doc from the list of Docs at listDoc[fieldKey]
      * @returns true if successful, false otherwise.
      */
-    export function RemoveDocFromList(listDoc: Doc, fieldKey: string | undefined, doc: Doc) {
+    export function RemoveDocFromList(listDoc: Doc, fieldKey: string | undefined, doc: Doc, ignoreProto = false) {
         const key = fieldKey ? fieldKey : Doc.LayoutFieldKey(listDoc);
-        if (listDoc[key] === undefined) {
-            listDoc[DocData][key] = new List<Doc>();
-        }
-        const list = Cast(listDoc[key], listSpec(Doc));
+        const list = Doc.Get(listDoc, key, ignoreProto) === undefined ? (listDoc[DocData][key] = new List<Doc>()) : Cast(listDoc[key], listSpec(Doc));
         if (list) {
             const ind = list.indexOf(doc);
             if (ind !== -1) {
@@ -528,12 +543,9 @@ export namespace Doc {
      * Adds doc to the list of Docs stored at listDoc[fieldKey].
      * @returns true if successful, false otherwise.
      */
-    export function AddDocToList(listDoc: Doc, fieldKey: string | undefined, doc: Doc, relativeTo?: Doc, before?: boolean, first?: boolean, allowDuplicates?: boolean, reversed?: boolean) {
+    export function AddDocToList(listDoc: Doc, fieldKey: string | undefined, doc: Doc, relativeTo?: Doc, before?: boolean, first?: boolean, allowDuplicates?: boolean, reversed?: boolean, ignoreProto?: boolean) {
         const key = fieldKey ? fieldKey : Doc.LayoutFieldKey(listDoc);
-        if (listDoc[key] === undefined) {
-            listDoc[DocData][key] = new List<Doc>();
-        }
-        const list = Cast(listDoc[key], listSpec(Doc));
+        const list = Doc.Get(listDoc, key, ignoreProto) === undefined ? (listDoc[DocData][key] = new List<Doc>()) : Cast(listDoc[key], listSpec(Doc));
         if (list) {
             if (!allowDuplicates) {
                 const pind = list.findIndex(d => d instanceof Doc && d[Id] === doc[Id]);
@@ -1020,7 +1032,7 @@ export namespace Doc {
     //
     export function MakeMetadataFieldTemplate(templateField: Doc, templateDoc: Opt<Doc>): boolean {
         // find the metadata field key that this template field doc will display (indicated by its title)
-        const metadataFieldKey = StrCast(templateField.isTemplateForField) || StrCast(templateField.title).replace(/^-/, '');
+        const metadataFieldKey = StrCast(templateField.isTemplateForField) || StrCast(templateField.title).replace(/^-/, '') || Doc.LayoutFieldKey(templateField);
 
         // update the original template to mark it as a template
         templateField.isTemplateForField = metadataFieldKey;
@@ -1640,3 +1652,6 @@ ScriptingGlobals.add(function setDocFilter(container: Doc, key: string, value: a
 ScriptingGlobals.add(function setDocRangeFilter(container: Doc, key: string, range: number[]) {
     Doc.setDocRangeFilter(container, key, range);
 });
+ScriptingGlobals.add(function toJavascriptString(str: string) {
+    return Field.toJavascriptString(str as Field);
+});
diff --git a/src/fields/FieldSymbols.ts b/src/fields/FieldSymbols.ts
index 0dbeb064b..0c44d2417 100644
--- a/src/fields/FieldSymbols.ts
+++ b/src/fields/FieldSymbols.ts
@@ -5,5 +5,6 @@ export const Parent = Symbol('FieldParent');
 export const Copy = Symbol('FieldCopy');
 export const ToValue = Symbol('FieldToValue');
 export const ToScriptString = Symbol('FieldToScriptString');
+export const ToJavascriptString = Symbol('FieldToJavascriptString');
 export const ToPlainText = Symbol('FieldToPlainText');
 export const ToString = Symbol('FieldToString');
diff --git a/src/fields/HtmlField.ts b/src/fields/HtmlField.ts
index 6e8bba977..b67f0f7e9 100644
--- a/src/fields/HtmlField.ts
+++ b/src/fields/HtmlField.ts
@@ -1,9 +1,9 @@
-import { Deserializable } from "../client/util/SerializationHelper";
-import { serializable, primitive } from "serializr";
-import { ObjectField } from "./ObjectField";
-import { Copy, ToScriptString, ToString} from "./FieldSymbols";
+import { Deserializable } from '../client/util/SerializationHelper';
+import { serializable, primitive } from 'serializr';
+import { ObjectField } from './ObjectField';
+import { Copy, ToJavascriptString, ToScriptString, ToString } from './FieldSymbols';
 
-@Deserializable("html")
+@Deserializable('html')
 export class HtmlField extends ObjectField {
     @serializable(primitive())
     readonly html: string;
@@ -17,8 +17,11 @@ export class HtmlField extends ObjectField {
         return new HtmlField(this.html);
     }
 
+    [ToJavascriptString]() {
+        return 'invalid';
+    }
     [ToScriptString]() {
-        return "invalid";
+        return 'invalid';
     }
     [ToString]() {
         return this.html;
diff --git a/src/fields/IconField.ts b/src/fields/IconField.ts
index 76c4ddf1b..4d2badb68 100644
--- a/src/fields/IconField.ts
+++ b/src/fields/IconField.ts
@@ -1,9 +1,9 @@
-import { Deserializable } from "../client/util/SerializationHelper";
-import { serializable, primitive } from "serializr";
-import { ObjectField } from "./ObjectField";
-import { Copy, ToScriptString, ToString } from "./FieldSymbols";
+import { Deserializable } from '../client/util/SerializationHelper';
+import { serializable, primitive } from 'serializr';
+import { ObjectField } from './ObjectField';
+import { Copy, ToJavascriptString, ToScriptString, ToString } from './FieldSymbols';
 
-@Deserializable("icon")
+@Deserializable('icon')
 export class IconField extends ObjectField {
     @serializable(primitive())
     readonly icon: string;
@@ -17,10 +17,13 @@ export class IconField extends ObjectField {
         return new IconField(this.icon);
     }
 
+    [ToJavascriptString]() {
+        return 'invalid';
+    }
     [ToScriptString]() {
-        return "invalid";
+        return 'invalid';
     }
     [ToString]() {
-        return "ICONfield";
+        return 'ICONfield';
     }
 }
diff --git a/src/fields/InkField.ts b/src/fields/InkField.ts
index 22bea3927..b3e01229a 100644
--- a/src/fields/InkField.ts
+++ b/src/fields/InkField.ts
@@ -2,7 +2,7 @@ import { Bezier } from 'bezier-js';
 import { alias, createSimpleSchema, list, object, serializable } from 'serializr';
 import { ScriptingGlobals } from '../client/util/ScriptingGlobals';
 import { Deserializable } from '../client/util/SerializationHelper';
-import { Copy, ToScriptString, ToString } from './FieldSymbols';
+import { Copy, ToJavascriptString, ToScriptString, ToString } from './FieldSymbols';
 import { ObjectField } from './ObjectField';
 
 // Helps keep track of the current ink tool in use.
@@ -85,6 +85,9 @@ export class InkField extends ObjectField {
         return new InkField(this.inkData);
     }
 
+    [ToJavascriptString]() {
+        return '[' + this.inkData.map(i => `{X: ${i.X}, Y: ${i.Y}}`) + ']';
+    }
     [ToScriptString]() {
         return 'new InkField([' + this.inkData.map(i => `{X: ${i.X}, Y: ${i.Y}}`) + '])';
     }
diff --git a/src/fields/List.ts b/src/fields/List.ts
index b8ad552d2..9458a9611 100644
--- a/src/fields/List.ts
+++ b/src/fields/List.ts
@@ -5,7 +5,7 @@ import { ScriptingGlobals } from '../client/util/ScriptingGlobals';
 import { Deserializable, afterDocDeserialize, autoObject } from '../client/util/SerializationHelper';
 import { Field } from './Doc';
 import { FieldTuples, Self, SelfProxy } from './DocSymbols';
-import { Copy, FieldChanged, Parent, ToScriptString, ToString } from './FieldSymbols';
+import { Copy, FieldChanged, Parent, ToJavascriptString, ToScriptString, ToString } from './FieldSymbols';
 import { ObjectField } from './ObjectField';
 import { ProxyField } from './Proxy';
 import { RefField } from './RefField';
@@ -310,6 +310,9 @@ class ListImpl<T extends Field> extends ObjectField {
     private [Self] = this;
     private [SelfProxy]: List<Field>; // also used in utils.ts even though it won't be found using find all references
 
+    [ToJavascriptString]() {
+        return `[${(this as any).map((field: any) => Field.toScriptString(field))}]`;
+    }
     [ToScriptString]() {
         return `new List([${(this as any).map((field: any) => Field.toScriptString(field))}])`;
     }
diff --git a/src/fields/ObjectField.ts b/src/fields/ObjectField.ts
index b5bc2952a..e1b5b036c 100644
--- a/src/fields/ObjectField.ts
+++ b/src/fields/ObjectField.ts
@@ -1,5 +1,5 @@
 import { RefField } from './RefField';
-import { FieldChanged, Parent, Copy, ToScriptString, ToString } from './FieldSymbols';
+import { FieldChanged, Parent, Copy, ToScriptString, ToString, ToJavascriptString } from './FieldSymbols';
 import { ScriptingGlobals } from '../client/util/ScriptingGlobals';
 import { Field } from './Doc';
 
@@ -12,6 +12,7 @@ export abstract class ObjectField {
     public [Parent]?: RefField | ObjectField;
     abstract [Copy](): ObjectField;
 
+    abstract [ToJavascriptString](): string;
     abstract [ToScriptString](): string;
     abstract [ToString](): string;
 }
diff --git a/src/fields/Proxy.ts b/src/fields/Proxy.ts
index 3a46e3581..820d9b6ff 100644
--- a/src/fields/Proxy.ts
+++ b/src/fields/Proxy.ts
@@ -4,7 +4,7 @@ import { DocServer } from '../client/DocServer';
 import { scriptingGlobal } from '../client/util/ScriptingGlobals';
 import { Deserializable } from '../client/util/SerializationHelper';
 import { Field, FieldWaiting, Opt } from './Doc';
-import { Copy, Id, ToScriptString, ToString, ToValue } from './FieldSymbols';
+import { Copy, Id, ToJavascriptString, ToScriptString, ToString, ToValue } from './FieldSymbols';
 import { ObjectField } from './ObjectField';
 import { RefField } from './RefField';
 
@@ -38,6 +38,9 @@ export class ProxyField<T extends RefField> extends ObjectField {
         return new ProxyField<T>(this.fieldId);
     }
 
+    [ToJavascriptString]() {
+        return Field.toScriptString(this[ToValue](undefined)?.value);
+    }
     [ToScriptString]() {
         return Field.toScriptString(this[ToValue](undefined)?.value); // not sure this is quite right since it doesn't recreate a proxy field, but better than 'invalid' ?
     }
diff --git a/src/fields/RefField.ts b/src/fields/RefField.ts
index b6ef69750..01828dd14 100644
--- a/src/fields/RefField.ts
+++ b/src/fields/RefField.ts
@@ -1,11 +1,11 @@
-import { serializable, primitive, alias } from "serializr";
-import { Utils } from "../Utils";
-import { Id, HandleUpdate, ToScriptString, ToString } from "./FieldSymbols";
-import { afterDocDeserialize } from "../client/util/SerializationHelper";
+import { serializable, primitive, alias } from 'serializr';
+import { Utils } from '../Utils';
+import { Id, HandleUpdate, ToScriptString, ToString, ToJavascriptString } from './FieldSymbols';
+import { afterDocDeserialize } from '../client/util/SerializationHelper';
 
 export type FieldId = string;
 export abstract class RefField {
-    @serializable(alias("id", primitive({ afterDeserialize: afterDocDeserialize })))
+    @serializable(alias('id', primitive({ afterDeserialize: afterDocDeserialize })))
     private __id: FieldId;
     readonly [Id]: FieldId;
 
@@ -16,6 +16,7 @@ export abstract class RefField {
 
     protected [HandleUpdate]?(diff: any): void | Promise<void>;
 
+    abstract [ToJavascriptString](): string;
     abstract [ToScriptString](): string;
     abstract [ToString](): string;
 }
diff --git a/src/fields/RichTextField.ts b/src/fields/RichTextField.ts
index 3e75a071f..50cfab988 100644
--- a/src/fields/RichTextField.ts
+++ b/src/fields/RichTextField.ts
@@ -1,7 +1,7 @@
 import { serializable } from 'serializr';
 import { scriptingGlobal } from '../client/util/ScriptingGlobals';
 import { Deserializable } from '../client/util/SerializationHelper';
-import { Copy, ToScriptString, ToString } from './FieldSymbols';
+import { Copy, ToJavascriptString, ToScriptString, ToString } from './FieldSymbols';
 import { ObjectField } from './ObjectField';
 
 @scriptingGlobal
@@ -27,6 +27,9 @@ export class RichTextField extends ObjectField {
         return new RichTextField(this.Data, this.Text);
     }
 
+    [ToJavascriptString]() {
+        return '`' + this.Text + '`';
+    }
     [ToScriptString]() {
         return `new RichTextField("${this.Data.replace(/"/g, '\\"')}", "${this.Text}")`;
     }
diff --git a/src/fields/SchemaHeaderField.ts b/src/fields/SchemaHeaderField.ts
index 6dde2e5aa..fb4dc4e5b 100644
--- a/src/fields/SchemaHeaderField.ts
+++ b/src/fields/SchemaHeaderField.ts
@@ -1,7 +1,7 @@
 import { Deserializable } from '../client/util/SerializationHelper';
 import { serializable, primitive } from 'serializr';
 import { ObjectField } from './ObjectField';
-import { Copy, ToScriptString, ToString, FieldChanged } from './FieldSymbols';
+import { Copy, ToScriptString, ToString, FieldChanged, ToJavascriptString } from './FieldSymbols';
 import { scriptingGlobal, ScriptingGlobals } from '../client/util/ScriptingGlobals';
 import { ColumnType } from '../client/views/collections/collectionSchema/CollectionSchemaView';
 
@@ -114,6 +114,9 @@ export class SchemaHeaderField extends ObjectField {
         return new SchemaHeaderField(this.heading, this.color, this.type, this.width, this.desc, this.collapsed);
     }
 
+    [ToJavascriptString]() {
+        return `["${this.heading}","${this.color}",${this.type},${this.width},${this.desc},${this.collapsed}]`;
+    }
     [ToScriptString]() {
         return `schemaHeaderField("${this.heading}","${this.color}",${this.type},${this.width},${this.desc},${this.collapsed})`;
     }
diff --git a/src/fields/ScriptField.ts b/src/fields/ScriptField.ts
index 62690a9fb..c7fe72ca6 100644
--- a/src/fields/ScriptField.ts
+++ b/src/fields/ScriptField.ts
@@ -6,7 +6,7 @@ import { scriptingGlobal, ScriptingGlobals } from '../client/util/ScriptingGloba
 import { autoObject, Deserializable } from '../client/util/SerializationHelper';
 import { numberRange } from '../Utils';
 import { Doc, Field, Opt } from './Doc';
-import { Copy, Id, ToScriptString, ToString, ToValue } from './FieldSymbols';
+import { Copy, Id, ToJavascriptString, ToScriptString, ToString, ToValue } from './FieldSymbols';
 import { List } from './List';
 import { ObjectField } from './ObjectField';
 import { Cast, StrCast } from './Types';
@@ -113,6 +113,9 @@ export class ScriptField extends ObjectField {
         return `${this.script.originalScript} + ${this.setterscript?.originalScript}`;
     }
 
+    [ToJavascriptString]() {
+        return this.script.originalScript;
+    }
     [ToScriptString]() {
         return this.script.originalScript;
     }
diff --git a/src/fields/URLField.ts b/src/fields/URLField.ts
index 817b62373..87334ad16 100644
--- a/src/fields/URLField.ts
+++ b/src/fields/URLField.ts
@@ -1,7 +1,7 @@
 import { Deserializable } from '../client/util/SerializationHelper';
 import { serializable, custom } from 'serializr';
 import { ObjectField } from './ObjectField';
-import { ToScriptString, ToString, Copy } from './FieldSymbols';
+import { ToScriptString, ToString, Copy, ToJavascriptString } from './FieldSymbols';
 import { scriptingGlobal } from '../client/util/ScriptingGlobals';
 import { Utils } from '../Utils';
 
@@ -36,6 +36,12 @@ export abstract class URLField extends ObjectField {
         }
         return `new ${this.constructor.name}("${this.url?.href}")`;
     }
+    [ToJavascriptString]() {
+        if (Utils.prepend(this.url?.pathname) === this.url?.href) {
+            return `new ${this.constructor.name}("${this.url.pathname}")`;
+        }
+        return `new ${this.constructor.name}("${this.url?.href}")`;
+    }
     [ToString]() {
         if (Utils.prepend(this.url?.pathname) === this.url?.href) {
             return this.url.pathname;
-- 
cgit v1.2.3-70-g09d2


From 8ac814bbb81b690a6a10f5a07aa5ce0e8cafe283 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Tue, 30 Jan 2024 00:40:43 -0500
Subject: changed dropConverter to keep title of dropped Doc. added paintFunc
 node/ checkbox view to formatted text.  changed paintFunc to be computed
 based on layouytfieldkey being text in a freeformview.  changed some
 inputRules to apply to code blocks.  changed : contextmenu to allow regular
 note to be created.  changed experimental tools to be user tmeplate tools. 
 fixed focus on search bar when opening context menu

---
 package-lock.json                                  |  10 +-
 package.json                                       |   2 +-
 src/Utils.ts                                       |   8 +-
 src/client/documents/Documents.ts                  |  39 ++++-
 src/client/util/CurrentUserUtils.ts                |  70 ++++-----
 src/client/util/DropConverter.ts                   |  16 +-
 src/client/views/ContextMenu.tsx                   |  18 ++-
 src/client/views/GlobalKeyHandler.ts               |   2 +-
 src/client/views/InkControlPtHandles.tsx           |   2 +
 .../collectionFreeForm/CollectionFreeFormView.tsx  |   6 +-
 src/client/views/nodes/DocumentView.tsx            |  10 +-
 .../views/nodes/FontIconBox/FontIconBox.scss       |  10 ++
 src/client/views/nodes/FontIconBox/FontIconBox.tsx |   8 +-
 .../nodes/formattedText/DashDocCommentView.tsx     |  39 ++++-
 .../views/nodes/formattedText/FormattedTextBox.tsx |   7 +-
 .../views/nodes/formattedText/PaintButtonView.tsx  | 113 ++++++++++++++
 .../views/nodes/formattedText/RichTextRules.ts     | 164 +++++++++++++--------
 src/client/views/nodes/formattedText/nodes_rts.ts  |  12 ++
 src/fields/Doc.ts                                  |  10 +-
 19 files changed, 397 insertions(+), 149 deletions(-)
 create mode 100644 src/client/views/nodes/formattedText/PaintButtonView.tsx

(limited to 'src/client/views/nodes/FontIconBox/FontIconBox.tsx')

diff --git a/package-lock.json b/package-lock.json
index 3564c4f1f..aa4108243 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -154,7 +154,7 @@
                 "prosemirror-commands": "^1.5.2",
                 "prosemirror-find-replace": "^0.9.0",
                 "prosemirror-history": "^1.3.2",
-                "prosemirror-inputrules": "^1.3.0",
+                "prosemirror-inputrules": "github:bobzel/prosemirror-inputrules#3b3b3e0b1a1dcf80490d81675206369b6be96276",
                 "prosemirror-keymap": "^1.2.2",
                 "prosemirror-model": "^1.19.3",
                 "prosemirror-schema-list": "^1.3.0",
@@ -28700,8 +28700,9 @@
         },
         "node_modules/prosemirror-inputrules": {
             "version": "1.3.0",
-            "resolved": "https://registry.npmjs.org/prosemirror-inputrules/-/prosemirror-inputrules-1.3.0.tgz",
-            "integrity": "sha512-z1GRP2vhh5CihYMQYsJSa1cOwXb3SYxALXOIfAkX8nZserARtl9LiL+CEl+T+OFIsXc3mJIHKhbsmRzC0HDAXA==",
+            "resolved": "git+ssh://git@github.com/bobzel/prosemirror-inputrules.git#3b3b3e0b1a1dcf80490d81675206369b6be96276",
+            "integrity": "sha512-l81tcwS7ugPJEvCy78RtEvR2/2mVaTkFHJD9ACRxRDXhrfPWV0FkFYBAO7GolN4XlzIbIsal2MVvq2baLQ2guw==",
+            "license": "MIT",
             "dependencies": {
                 "prosemirror-state": "^1.0.0",
                 "prosemirror-transform": "^1.0.0"
@@ -32239,7 +32240,8 @@
         "node_modules/text-table": {
             "version": "0.2.0",
             "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
-            "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw=="
+            "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
+            "dev": true
         },
         "node_modules/textarea-caret": {
             "version": "3.1.0",
diff --git a/package.json b/package.json
index 760099bd5..f8144a0b3 100644
--- a/package.json
+++ b/package.json
@@ -237,7 +237,7 @@
         "prosemirror-commands": "^1.5.2",
         "prosemirror-find-replace": "^0.9.0",
         "prosemirror-history": "^1.3.2",
-        "prosemirror-inputrules": "^1.3.0",
+        "prosemirror-inputrules": "github:bobzel/prosemirror-inputrules#3b3b3e0b1a1dcf80490d81675206369b6be96276",
         "prosemirror-keymap": "^1.2.2",
         "prosemirror-model": "^1.19.3",
         "prosemirror-schema-list": "^1.3.0",
diff --git a/src/Utils.ts b/src/Utils.ts
index 502cf7db7..e8bd35ac4 100644
--- a/src/Utils.ts
+++ b/src/Utils.ts
@@ -514,8 +514,8 @@ export function intersectRect(r1: { left: number; top: number; width: number; he
     return !(r2.left > r1.left + r1.width || r2.left + r2.width < r1.left || r2.top > r1.top + r1.height || r2.top + r2.height < r1.top);
 }
 
-export function stringHash(s?:string) {
-    return !s? undefined:  Math.abs(s.split('').reduce((a: any, b: any) => ((a) => a & a)((a << 5) - a + b.charCodeAt(0)),0));
+export function stringHash(s?: string) {
+    return !s ? undefined : Math.abs(s.split('').reduce((a: any, b: any) => (a => a & a)((a << 5) - a + b.charCodeAt(0)), 0));
 }
 
 export function percent2frac(percent: string) {
@@ -852,9 +852,11 @@ export function setupMoveUpEvents(
     (target as any)._downX = (target as any)._lastX = e.clientX;
     (target as any)._downY = (target as any)._lastY = e.clientY;
     (target as any)._noClick = false;
+    var moving = false;
 
     const _moveEvent = (e: PointerEvent): void => {
-        if (Math.abs(e.clientX - (target as any)._downX) > Utils.DRAG_THRESHOLD || Math.abs(e.clientY - (target as any)._downY) > Utils.DRAG_THRESHOLD) {
+        if (moving || Math.abs(e.clientX - (target as any)._downX) > Utils.DRAG_THRESHOLD || Math.abs(e.clientY - (target as any)._downY) > Utils.DRAG_THRESHOLD) {
+            moving = true;
             if ((target as any)._doubleTime) {
                 clearTimeout((target as any)._doubleTime);
                 (target as any)._doubleTime = undefined;
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index ac418ecfe..cc983ffa7 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -1667,10 +1667,37 @@ export namespace DocUtils {
     }
 
     export function addDocumentCreatorMenuItems(docTextAdder: (d: Doc) => void, docAdder: (d: Doc) => void, x: number, y: number, simpleMenu: boolean = false, pivotField?: string, pivotValue?: string): void {
+        const documentList: ContextMenuProps[] = DocListCast(DocListCast(Doc.MyTools?.data)[0]?.data)
+            .filter(btnDoc => !btnDoc.hidden)
+            .map(btnDoc => Cast(btnDoc?.dragFactory, Doc, null))
+            .filter(doc => doc && doc !== Doc.UserDoc().emptyTrail && doc.title)
+            .map((dragDoc, i) => ({
+                description: ':' + StrCast(dragDoc.title).replace('Untitled ', ''),
+                event: undoable((args: { x: number; y: number }) => {
+                    const newDoc = DocUtils.copyDragFactory(dragDoc);
+                    if (newDoc) {
+                        newDoc.author = Doc.CurrentUserEmail;
+                        newDoc.x = x;
+                        newDoc.y = y;
+                        EquationBox.SelectOnLoad = newDoc[Id];
+                        if (newDoc.type === DocumentType.RTF) FormattedTextBox.SetSelectOnLoad(newDoc);
+                        if (pivotField) {
+                            newDoc[pivotField] = pivotValue;
+                        }
+                        docAdder?.(newDoc);
+                    }
+                }, StrCast(dragDoc.title)),
+                icon: Doc.toIcon(dragDoc),
+            })) as ContextMenuProps[];
+        ContextMenu.Instance.addItem({
+            description: 'Create document',
+            subitems: documentList,
+            icon: 'file',
+        });
         !simpleMenu &&
             ContextMenu.Instance.addItem({
-                description: 'Quick Notes',
-                subitems: DocListCast((Doc.UserDoc()['template_notes'] as Doc).data).map((note, i) => ({
+                description: 'Styled Notes',
+                subitems: DocListCast((Doc.UserDoc().template_notes as Doc).data).map((note, i) => ({
                     description: ':' + StrCast(note.title),
                     event: undoable((args: { x: number; y: number }) => {
                         const textDoc = Docs.Create.TextDocument('', {
@@ -1691,14 +1718,14 @@ export namespace DocUtils {
                 })) as ContextMenuProps[],
                 icon: 'sticky-note',
             });
-        const documentList: ContextMenuProps[] = DocListCast(DocListCast(Doc.MyTools?.data)[0]?.data)
+        const userDocList: ContextMenuProps[] = DocListCast(DocListCast(Doc.MyTools?.data)[1]?.data)
             .filter(btnDoc => !btnDoc.hidden)
             .map(btnDoc => Cast(btnDoc?.dragFactory, Doc, null))
             .filter(doc => doc && doc !== Doc.UserDoc().emptyTrail && doc !== Doc.UserDoc().emptyNote && doc.title)
             .map((dragDoc, i) => ({
                 description: ':' + StrCast(dragDoc.title).replace('Untitled ', ''),
                 event: undoable((args: { x: number; y: number }) => {
-                    const newDoc = DocUtils.copyDragFactory(dragDoc);
+                    const newDoc = DocUtils.delegateDragFactory(dragDoc);
                     if (newDoc) {
                         newDoc.author = Doc.CurrentUserEmail;
                         newDoc.x = x;
@@ -1714,8 +1741,8 @@ export namespace DocUtils {
                 icon: Doc.toIcon(dragDoc),
             })) as ContextMenuProps[];
         ContextMenu.Instance.addItem({
-            description: 'Create document',
-            subitems: documentList,
+            description: 'User Templates',
+            subitems: userDocList,
             icon: 'file',
         });
     } // applies a custom template to a document.  the template is identified by it's short name (e.g, slideView not layout_slideView)
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index 8f46f844c..31f0308b7 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -61,49 +61,16 @@ export let resolvedPorts: { server: number, socket: number };
 export class CurrentUserUtils {
 
     // initializes experimental advanced template views - slideView,  headerView
-    static setupExperimentalTemplateButtons(doc: Doc, tempDocs:Opt<Doc>, userBtns:Doc[]) {
-        const requiredTypeNameFields:{btnOpts:DocumentOptions, templateOpts:DocumentOptions, template:(opts:DocumentOptions) => Doc}[] = [
-            {
-                btnOpts: { title: "slide", icon: "address-card" }, 
-                templateOpts: { _width: 400, _height: 300, title: "slideView",  _xMargin: 3, _yMargin: 3, isSystem: true }, 
-                template: (opts:DocumentOptions) => Docs.Create.MultirowDocument(
-                    [
-                        Docs.Create.MulticolumnDocument([], { title: "hero", _height: 200,  isSystem: true }),
-                        Docs.Create.TextDocument("", { title: "text", _layout_fitWidth:true,  _height: 100, isSystem: true, _text_fontFamily: StrCast(Doc.UserDoc().fontFamily), _text_fontSize: StrCast(Doc.UserDoc().fontSize) })
-                    ], opts)
-            },
-            {
-                btnOpts: { title: "mobile", icon: "mobile" }, 
-                templateOpts: { title: "NEW MOBILE BUTTON", onClick: undefined, }, 
-                template: (opts:DocumentOptions) => this.mobileButton(opts,
-                        [this.createToolButton({ ignoreClick: true, icon: "mobile", backgroundColor: "transparent" }),
-                        this.mobileTextContainer({},
-                            [this.mobileButtonText({}, "NEW MOBILE BUTTON"), this.mobileButtonInfo({}, "You can customize this button and make it your own.")])
-                        ]
-                )
-            },
-        ];
-        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,this.openFactoryAsDelegate); }' };
-            const assignBtnAndTempOpts = (templateBtn:Opt<Doc>, btnOpts:DocumentOptions, templateOptions:DocumentOptions) => {
-                if (templateBtn) {
-                    DocUtils.AssignOpts(templateBtn,btnOpts);
-                    DocUtils.AssignDocField(templateBtn, "dragFactory", opts => template(opts), templateOptions);
-                }
-                return templateBtn;
-            };
-            return DocUtils.AssignScripts(assignBtnAndTempOpts(tempBtn, btnOpts, templateOpts) ?? this.createToolButton( {...btnOpts, dragFactory: MakeTemplate(template(templateOpts))}), reqdScripts);
-        }), ...userBtns];
-
+    static setupUserDocumentCreatorButtons(doc: Doc, userDocTemplates: Opt<Doc>) {
+        const userTemplates = DocListCast(userDocTemplates?.data).filter(doc => !Doc.IsSystem(doc));
         const reqdOpts:DocumentOptions = {
-            title: "Experimental Tools", _xMargin: 0, _layout_showTitle: "title", _chromeHidden: true,
+            title: "User Tools", _xMargin: 0, _layout_showTitle: "title", _chromeHidden: true, hidden: false,
             _dragOnlyWithinContainer: true, _layout_hideContextMenu: true, isSystem: true, _forceActive: true,
             _layout_autoHeight: true, _width: 500, _height: 300, _layout_fitWidth: true, _columnWidth: 35, ignoreClick: true, _lockedPosition: true,
         };
         const reqdScripts = { dropConverter : "convertToButtons(dragData)" };
-        const reqdFuncs = { hidden: "IsNoviceMode()" };
-        return DocUtils.AssignScripts(DocUtils.AssignOpts(tempDocs, reqdOpts, requiredTypes) ?? Docs.Create.MasonryDocument(requiredTypes, reqdOpts), reqdScripts, reqdFuncs);
+        const reqdFuncs = { /* hidden: "IsNoviceMode()"  */ };
+        return DocUtils.AssignScripts(DocUtils.AssignOpts(userDocTemplates, reqdOpts, userTemplates) ?? Docs.Create.MasonryDocument(userTemplates, reqdOpts), reqdScripts, reqdFuncs);
     }
 
     /// Initializes templates for editing click funcs of a document 
@@ -148,7 +115,7 @@ export class CurrentUserUtils {
     static setupNoteTemplates(doc: Doc, field="template_notes") {
         const tempNotes = DocCast(doc[field]);
         const reqdTempOpts:DocumentOptions[] = [
-            { noteType: "Note", backgroundColor: "yellow", icon: "sticky-note"},
+            { noteType: "Postit", backgroundColor: "yellow", icon: "sticky-note"},
             { noteType: "Idea", backgroundColor: "pink", icon: "lightbulb" },
             { noteType: "Topic", backgroundColor: "lightblue", icon: "book-open" }];
         const reqdNoteList = reqdTempOpts.map(opts => {
@@ -257,6 +224,16 @@ export class CurrentUserUtils {
             MakeTemplate(Doc.GetProto(header), true, "Untitled Header");
             return header; 
         }
+        const slideView = (opts:DocumentOptions) => {
+            const slide = Docs.Create.MultirowDocument(
+                    [
+                        Docs.Create.MulticolumnDocument([], { title: "hero", _height: 200,  isSystem: true }),
+                        Docs.Create.TextDocument("", { title: "text", _layout_fitWidth:true,  _height: 100, isSystem: true, _text_fontFamily: StrCast(Doc.UserDoc().fontFamily), _text_fontSize: StrCast(Doc.UserDoc().fontSize) })
+                    ], opts);
+
+            MakeTemplate(Doc.GetProto(slide), true, "Untitled Slide View");
+            return slide; 
+        }
         const emptyThings:{key:string,  // the field name where the empty thing will be stored
                           opts:DocumentOptions,   // the document options that are required for the empty thing
                           funcs?:{[key:string]: any},  // computed fields that are rquired for the empth thing
@@ -279,6 +256,7 @@ export class CurrentUserUtils {
             {key: "Script",     creator: opts => Docs.Create.ScriptingDocument(null, opts), opts: { _width: 200, _height: 250, }},
             {key: "DataViz",    creator: opts => Docs.Create.DataVizDocument("/users/rz/Downloads/addresses.csv", opts),   opts: { _width: 300, _height: 300 }},
             {key: "Header",     creator: headerTemplate,                                    opts: { _width: 300, _height: 70, _headerPointerEvents: "all", _headerHeight: 12, _headerFontSize: 9, _layout_autoHeight: true, treeView_HideUnrendered: true}},
+            {key: "ViewSlide",  creator: slideView,                                         opts: { _width: 400, _height: 300, _xMargin: 3, _yMargin: 3,}},
             {key: "Trail",      creator: Docs.Create.PresDocument,                          opts: { _width: 400, _height: 30, _type_collection: CollectionViewType.Stacking, dropAction: "embed" as dropActionType, treeView_HideTitle: true, _layout_fitWidth:true, layout_boxShadow: "0 0" }},
             {key: "Tab",        creator: opts => Docs.Create.FreeformDocument([], opts),    opts: { _width: 500, _height: 800, _layout_fitWidth: true, _freeform_backgroundGrid: true, }},
             {key: "Slide",      creator: opts => Docs.Create.TreeDocument([], opts),        opts: { _width: 300, _height: 200, _type_collection: CollectionViewType.Tree, 
@@ -307,6 +285,7 @@ export class CurrentUserUtils {
             { toolTip: "Tap or drag to create a scripting box",   title: "Script", icon: "terminal",    dragFactory: doc.emptyScript as Doc,     clickFactory: DocCast(doc.emptyScript),                                             funcs: { hidden: "IsNoviceMode()"}},
             { toolTip: "Tap or drag to create a data viz node",   title: "DataViz", icon: "chart-bar",  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, funcs: { hidden: "IsNoviceMode()"}},
+            { toolTip: "Tap or drag to create a view slide",      title: "View Slide", icon: "address-card", dragFactory: doc.emptyViewSlide as Doc,clickFactory: DocCast(doc.emptyViewSlide),openFactoryLocation: OpenWhere.overlay,funcs: { hidden: "IsNoviceMode()"}},
             { 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,            funcs: { hidden: "IsNoviceMode()"} },
             { toolTip: "Toggle a Calculator REPL",                title: "replviewer",  icon: "calculator",                                      clickFactory: '<ScriptingRepl />' as any,   openFactoryLocation: OpenWhere.overlay}, // hack: clickFactory is not a Doc but will get interpreted as a custom UI by the openDoc() onClick script
            // { toolTip: "Toggle an UndoStack",                     title: "undostacker", icon: "calculator",                                      clickFactory: "<UndoStack />" as any,         openFactoryLocation: OpenWhere.overlay},
@@ -330,7 +309,7 @@ export class CurrentUserUtils {
         });
 
         const reqdOpts:DocumentOptions = {
-            title: "Basic Item Creators", _layout_showTitle: "title", _xMargin: 0, _dragOnlyWithinContainer: true, _layout_hideContextMenu: true, _chromeHidden: true, isSystem: true,
+            title: "Document Creators", _layout_showTitle: "title", _xMargin: 0, _dragOnlyWithinContainer: true, _layout_hideContextMenu: true, _chromeHidden: true, isSystem: true,
             _layout_autoHeight: true, _width: 500, _height: 300, _layout_fitWidth: true, _columnWidth: 40, ignoreClick: true, _lockedPosition: true, _forceActive: true,
              childDragAction: 'embed' 
         };
@@ -464,16 +443,17 @@ export class CurrentUserUtils {
     /// Initializes the panel of draggable tools that is opened from the left sidebar.
     static setupToolsBtnPanel(doc: Doc, field:string) {
         const myTools = DocCast(doc[field]);
-        const creatorBtns = CurrentUserUtils.setupCreatorButtons(doc, DocListCast(myTools?.data)?.length ? DocListCast(myTools.data)[0]:undefined);
-        const tempBtns = DocListCast(myTools?.data)?.length > 1 ? DocListCast(myTools.data)[1]:undefined;
-        const userTemplateBtns = DocListCast(tempBtns?.data).filter(btn => !btn.isSystem);
-        const templateBtns = CurrentUserUtils.setupExperimentalTemplateButtons(doc, tempBtns, userTemplateBtns);
+        const allTools = DocListCast(myTools?.data);
+        const creatorBtns = CurrentUserUtils.setupCreatorButtons(doc, allTools?.length ? allTools[0]:undefined);
+        const userTools = allTools && allTools?.length > 1 ? allTools[1]:undefined;
+        const userBtns = CurrentUserUtils.setupUserDocumentCreatorButtons(doc, userTools);
+        //doc.myUserBtns = new PrefetchProxy(userBtns);
         const reqdToolOps:DocumentOptions = {
             title: "My Tools", isSystem: true, ignoreClick: true, layout_boxShadow: "0 0",
             layout_explainer: "This is a palette of documents that can be created.",
             _layout_showTitle: "title", _width: 500, _yMargin: 20, _lockedPosition: true, _forceActive: true, _dragOnlyWithinContainer: true, _layout_hideContextMenu: true, _chromeHidden: true, 
         };
-        return DocUtils.AssignDocField(doc, field, (opts, items) => Docs.Create.StackingDocument(items??[], opts), reqdToolOps, [creatorBtns, templateBtns]);
+        return DocUtils.AssignDocField(doc, field, (opts, items) => Docs.Create.StackingDocument(items??[], opts), reqdToolOps, [creatorBtns, userBtns]);
     }
 
     /// initializes the left sidebar dashboard pane
diff --git a/src/client/util/DropConverter.ts b/src/client/util/DropConverter.ts
index 8c3b56452..ba981145d 100644
--- a/src/client/util/DropConverter.ts
+++ b/src/client/util/DropConverter.ts
@@ -3,7 +3,7 @@ import { DocData } from '../../fields/DocSymbols';
 import { ObjectField } from '../../fields/ObjectField';
 import { RichTextField } from '../../fields/RichTextField';
 import { listSpec } from '../../fields/Schema';
-import { ScriptField } from '../../fields/ScriptField';
+import { ComputedField, ScriptField } from '../../fields/ScriptField';
 import { Cast, StrCast } from '../../fields/Types';
 import { ImageField } from '../../fields/URLField';
 import { Docs } from '../documents/Documents';
@@ -39,15 +39,12 @@ function makeTemplate(doc: Doc, first: boolean = true, rename: Opt<string> = und
             any = makeTemplate(d, false) || any;
         }
     });
-    if (first) {
-        if (!docs.length) {
-            // bcz: feels hacky : if the root level document has items, it's not a field template
-            any = Doc.MakeMetadataFieldTemplate(doc, layoutDoc[DocData]) || any;
-        }
-    }
-    if (layoutDoc[fieldKey] instanceof RichTextField || layoutDoc[fieldKey] instanceof ImageField) {
+    if (first && !docs.length) {
+        // bcz: feels hacky : if the root level document has items, it's not a field template
+        any = Doc.MakeMetadataFieldTemplate(doc, layoutDoc[DocData], true) || any;
+    } else if (layoutDoc[fieldKey] instanceof RichTextField || layoutDoc[fieldKey] instanceof ImageField) {
         if (!StrCast(layoutDoc.title).startsWith('-')) {
-            any = Doc.MakeMetadataFieldTemplate(layoutDoc, layoutDoc[DocData]);
+            any = Doc.MakeMetadataFieldTemplate(layoutDoc, layoutDoc[DocData], true);
         }
     }
     rename && (doc.title = rename);
@@ -82,6 +79,7 @@ export function convertDropDataToButtons(data: DragManager.DocumentDragData) {
                 icon: 'bolt',
                 isSystem: false,
             });
+            dbox.title = ComputedField.MakeFunction('this.dragFactory.title');
             dbox.dragFactory = layoutDoc;
             dbox.dropPropertiesToRemove = doc.dropPropertiesToRemove instanceof ObjectField ? ObjectField.MakeCopy(doc.dropPropertiesToRemove) : undefined;
             dbox.onDragStart = ScriptField.MakeFunction('makeDelegate(this.dragFactory)');
diff --git a/src/client/views/ContextMenu.tsx b/src/client/views/ContextMenu.tsx
index 8dcdd80e5..8c3c9df2e 100644
--- a/src/client/views/ContextMenu.tsx
+++ b/src/client/views/ContextMenu.tsx
@@ -183,11 +183,12 @@ export class ContextMenu extends ObservableReactComponent<{}> {
 
     @computed get menuItems() {
         if (!this._searchString) {
-            return this._items.map((item, ind) => <ContextMenuItem {...item} noexpand={this.itemsNeedSearch ? true : (item as any).noexpand} key={ind + item.description} closeMenu={this.closeMenu} />);
+            return this._items.map((item, ind) => <ContextMenuItem key={item.description + ind} {...item} noexpand={this.itemsNeedSearch ? true : (item as any).noexpand} closeMenu={this.closeMenu} />);
         }
         return this.filteredItems.map((value, index) =>
             Array.isArray(value) ? (
                 <div
+                    key={index + value.join(' -> ')}
                     className="contextMenu-group"
                     style={{
                         background: StrCast(SettingsManager.userVariantColor),
@@ -204,7 +205,10 @@ export class ContextMenu extends ObservableReactComponent<{}> {
         return this._showSearch ? 1 : this._items.reduce((p, mi) => p + ((mi as any).noexpand ? 1 : (mi as any).subitems?.length || 1), 0) > 15;
     }
 
+    _searchRef = React.createRef<HTMLInputElement>(); // bcz: we shouldn't need this, since we set autoFocus on the <input> tag, but for some reason we do...
+
     render() {
+        this.itemsNeedSearch && setTimeout(() => this._searchRef.current?.focus());
         return (
             <div
                 className="contextMenu-cont"
@@ -226,7 +230,17 @@ export class ContextMenu extends ObservableReactComponent<{}> {
                         <span className="icon-background">
                             <FontAwesomeIcon icon="search" size="lg" />
                         </span>
-                        <input style={{ color: 'black' }} className="contextMenu-item contextMenu-description search" type="text" placeholder="Filter Menu..." value={this._searchString} onKeyDown={this.onKeyDown} onChange={this.onChange} autoFocus />
+                        <input
+                            ref={this._searchRef}
+                            style={{ color: 'black' }}
+                            className="contextMenu-item contextMenu-description search"
+                            type="text"
+                            placeholder="Filter Menu..."
+                            value={this._searchString}
+                            onKeyDown={this.onKeyDown}
+                            onChange={this.onChange}
+                            autoFocus
+                        />
                     </span>
                 )}
                 {this.menuItems}
diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts
index d134d9e7b..733383002 100644
--- a/src/client/views/GlobalKeyHandler.ts
+++ b/src/client/views/GlobalKeyHandler.ts
@@ -160,7 +160,7 @@ export class KeyManager {
                     if (LightboxView.LightboxDoc) {
                         LightboxView.Instance.SetLightboxDoc(undefined);
                         SelectionManager.DeselectAll();
-                    } else DocumentDecorations.Instance.onCloseClick(true);
+                    } else if (!window.getSelection()?.toString()) DocumentDecorations.Instance.onCloseClick(true);
                     return { stopPropagation: true, preventDefault: true };
                 }
                 break;
diff --git a/src/client/views/InkControlPtHandles.tsx b/src/client/views/InkControlPtHandles.tsx
index 7dd57e04d..31b13d2c8 100644
--- a/src/client/views/InkControlPtHandles.tsx
+++ b/src/client/views/InkControlPtHandles.tsx
@@ -189,6 +189,7 @@ export class InkEndPtHandles extends React.Component<InkEndProps> {
     @observable _overStart: boolean = false;
     @observable _overEnd: boolean = false;
 
+    _throttle = 0; // need to throttle dragging since the position may change when the control points change.  this allows the stroke to settle so that we don't get increasingly bad jitter
     @action
     dragRotate = (e: React.PointerEvent, pt1: () => { X: number; Y: number }, pt2: () => { X: number; Y: number }) => {
         SnappingManager.SetIsDragging(true);
@@ -196,6 +197,7 @@ export class InkEndPtHandles extends React.Component<InkEndProps> {
             this,
             e,
             action(e => {
+                if (this._throttle++ % 2 !== 0) return false;
                 if (!this.props.inkView.controlUndo) this.props.inkView.controlUndo = UndoManager.StartBatch('stretch ink');
                 // compute stretch factor by finding scaling along axis between start and end points
                 const p1 = pt1();
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 82ada4fb5..54e8b08b6 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -5,7 +5,7 @@ import { observer } from 'mobx-react';
 import { computedFn } from 'mobx-utils';
 import * as React from 'react';
 import { DateField } from '../../../../fields/DateField';
-import { Doc, DocListCast, Opt } from '../../../../fields/Doc';
+import { Doc, DocListCast, Field, Opt } from '../../../../fields/Doc';
 import { DocData, Height, Width } from '../../../../fields/DocSymbols';
 import { Id } from '../../../../fields/FieldSymbols';
 import { InkData, InkField, InkTool, PointData, Segment } from '../../../../fields/InkField';
@@ -53,6 +53,7 @@ import { CollectionFreeFormPannableContents } from './CollectionFreeFormPannable
 import { CollectionFreeFormRemoteCursors } from './CollectionFreeFormRemoteCursors';
 import './CollectionFreeFormView.scss';
 import { MarqueeView } from './MarqueeView';
+import { RichTextField } from '../../../../fields/RichTextField';
 
 export interface collectionFreeformViewProps {
     NativeWidth?: () => number;
@@ -75,7 +76,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
 
     @observable _paintedId = 'id' + Utils.GenerateGuid().replace(/-/g, '');
     @computed get paintFunc() {
-        const paintFunc = StrCast(this.Document[DocData].paintFunc).trim();
+        const field = this.layoutDoc[this.fieldKey];
+        const paintFunc = StrCast(Field.toJavascriptString(Cast(field, RichTextField, null)?.Text as Field)).trim();
         return !paintFunc
             ? ''
             : `const dashDiv = document.querySelector('#${this._paintedId}'); 
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 2e1ba2a7e..51f4b1a68 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -456,11 +456,11 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
 
     deleteClicked = undoable(() => this._props.removeDocument?.(this.Document), 'delete doc');
     setToggleDetail = undoable(
-        () =>
+        (defaultLayout: string) =>
             (this.Document.onClick = ScriptField.MakeScript(
                 `toggleDetail(documentView, "${StrCast(this.Document.layout_fieldKey)
                     .replace('layout_', '')
-                    .replace(/^layout$/, 'detail')}")`,
+                    .replace(/^layout$/, 'detail')}", "${defaultLayout}")`,
                 { documentView: 'any' }
             )),
         'set toggle detail'
@@ -1212,7 +1212,7 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
     };
     public noOnClick = () => this._docViewInternal?.noOnClick();
     public toggleFollowLink = (zoom?: boolean, setTargetToggle?: boolean): void => this._docViewInternal?.toggleFollowLink(zoom, setTargetToggle);
-    public setToggleDetail = () => this._docViewInternal?.setToggleDetail();
+    public setToggleDetail = (defaultLayout = '') => this._docViewInternal?.setToggleDetail(defaultLayout);
     public onContextMenu = (e?: React.MouseEvent, pageX?: number, pageY?: number) => this._docViewInternal?.onContextMenu?.(e, pageX, pageY);
     public cleanupPointerEvents = () => this._docViewInternal?.cleanupPointerEvents();
     public startDragging = (x: number, y: number, dropAction: dropActionType, hideSource = false) => this._docViewInternal?.startDragging(x, y, dropAction, hideSource);
@@ -1460,8 +1460,8 @@ ScriptingGlobals.add(function deiconifyViewToLightbox(documentView: DocumentView
     LightboxView.Instance.AddDocTab(documentView.Document, OpenWhere.lightbox, 'layout'); //, 0);
 });
 
-ScriptingGlobals.add(function toggleDetail(dv: DocumentView, detailLayoutKeySuffix: string) {
-    if (dv.Document.layout_fieldKey === 'layout_' + detailLayoutKeySuffix) dv.switchViews(false, 'layout');
+ScriptingGlobals.add(function toggleDetail(dv: DocumentView, detailLayoutKeySuffix: string, defaultLayout = '') {
+    if (dv.Document.layout_fieldKey === 'layout_' + detailLayoutKeySuffix) dv.switchViews(defaultLayout ? true : false, defaultLayout, undefined, true);
     else dv.switchViews(true, detailLayoutKeySuffix, undefined, true);
 });
 
diff --git a/src/client/views/nodes/FontIconBox/FontIconBox.scss b/src/client/views/nodes/FontIconBox/FontIconBox.scss
index db2ffa756..2db285910 100644
--- a/src/client/views/nodes/FontIconBox/FontIconBox.scss
+++ b/src/client/views/nodes/FontIconBox/FontIconBox.scss
@@ -1,5 +1,15 @@
 @import '../../global/globalCssVariables.module.scss';
 
+// bcz: something's messed up with the IconButton css.  this mostly fixes the fit-all button, the color buttons, the undo +/- expander and the dropdown doc type list (eg 'text')
+.iconButton-container {
+    width: unset !important;
+    min-width: 30px !important;
+    height: unset !important;
+    min-height: 30px;
+    .color {
+        height: 3px !important;
+    }
+}
 .menuButton {
     height: 100%;
     display: flex;
diff --git a/src/client/views/nodes/FontIconBox/FontIconBox.tsx b/src/client/views/nodes/FontIconBox/FontIconBox.tsx
index 8290e102c..3577cc8d9 100644
--- a/src/client/views/nodes/FontIconBox/FontIconBox.tsx
+++ b/src/client/views/nodes/FontIconBox/FontIconBox.tsx
@@ -381,7 +381,7 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() {
             case ButtonType.ColorButton:        return this.colorButton; 
             case ButtonType.MultiToggleButton:  return this.multiToggleButton;
             case ButtonType.ToggleButton:       return this.toggleButton;
-            case ButtonType.ClickButton:
+            case ButtonType.ClickButton:return <IconButton {...btnProps} size={Size.MEDIUM} color={color} />;
             case ButtonType.ToolButton: return <IconButton {...btnProps} size={Size.LARGE} color={color} />;
             case ButtonType.TextButton: return <Button     {...btnProps} color={color} 
                         background={SettingsManager.userBackgroundColor} text={StrCast(this.dataDoc.buttonText)}/>;
@@ -392,6 +392,10 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() {
     };
 
     render() {
-        return <div onContextMenu={this.specificContextMenu}>{this.renderButton()}</div>;
+        return (
+            <div style={{ margin: 'auto', width: '100%' }} onContextMenu={this.specificContextMenu}>
+                {this.renderButton()}
+            </div>
+        );
     }
 }
diff --git a/src/client/views/nodes/formattedText/DashDocCommentView.tsx b/src/client/views/nodes/formattedText/DashDocCommentView.tsx
index b7d2a24c2..a72ed1813 100644
--- a/src/client/views/nodes/formattedText/DashDocCommentView.tsx
+++ b/src/client/views/nodes/formattedText/DashDocCommentView.tsx
@@ -3,6 +3,8 @@ import * as ReactDOM from 'react-dom/client';
 import { Doc } from '../../../../fields/Doc';
 import { DocServer } from '../../../DocServer';
 import * as React from 'react';
+import { IReactionDisposer, computed, reaction } from 'mobx';
+import { NumCast } from '../../../../fields/Types';
 
 // creates an inline comment in a note when '>>' is typed.
 // the comment sits on the right side of the note and vertically aligns with its anchor in the text.
@@ -10,8 +12,10 @@ import * as React from 'react';
 export class DashDocCommentView {
     dom: HTMLDivElement; // container for label and value
     root: any;
+    node: any;
 
     constructor(node: any, view: any, getPos: any) {
+        this.node = node;
         this.dom = document.createElement('div');
         this.dom.style.width = node.attrs.width;
         this.dom.style.height = node.attrs.height;
@@ -32,10 +36,14 @@ export class DashDocCommentView {
         };
 
         this.root = ReactDOM.createRoot(this.dom);
-        this.root.render(<DashDocCommentViewInternal view={view} getPos={getPos} docId={node.attrs.docId} />);
+        this.root.render(<DashDocCommentViewInternal view={view} getPos={getPos} setHeight={this.setHeight} docId={node.attrs.docId} />);
         (this as any).dom = this.dom;
     }
 
+    setHeight = (hgt: number) => {
+        !this.node.attrs.reflow && DocServer.GetRefField(this.node.attrs.docId).then(doc => doc instanceof Doc && (this.dom.style.height = hgt + ''));
+    };
+
     destroy() {
         this.root.unmount();
     }
@@ -51,9 +59,15 @@ interface IDashDocCommentViewInternal {
     docId: string;
     view: any;
     getPos: any;
+    setHeight: (height: number) => void;
 }
 
 export class DashDocCommentViewInternal extends React.Component<IDashDocCommentViewInternal> {
+    _reactionDisposer: IReactionDisposer | undefined;
+
+    @computed get _dashDoc() {
+        return DocServer.GetRefField(this.props.docId);
+    }
     constructor(props: any) {
         super(props);
         this.onPointerLeaveCollapsed = this.onPointerLeaveCollapsed.bind(this);
@@ -61,15 +75,32 @@ export class DashDocCommentViewInternal extends React.Component<IDashDocCommentV
         this.onPointerUpCollapsed = this.onPointerUpCollapsed.bind(this);
         this.onPointerDownCollapsed = this.onPointerDownCollapsed.bind(this);
     }
+    componentDidMount(): void {
+        this._reactionDisposer?.();
+        this._dashDoc.then(
+            doc =>
+                doc instanceof Doc &&
+                (this._reactionDisposer = reaction(
+                    () => NumCast((doc as Doc)._height),
+                    hgt => this.props.setHeight(hgt),
+                    {
+                        fireImmediately: true,
+                    }
+                ))
+        );
+    }
+    componentWillUnmount(): void {
+        this._reactionDisposer?.();
+    }
 
     onPointerLeaveCollapsed(e: any) {
-        DocServer.GetRefField(this.props.docId).then(async dashDoc => dashDoc instanceof Doc && Doc.linkFollowUnhighlight());
+        this._dashDoc.then(async dashDoc => dashDoc instanceof Doc && Doc.linkFollowUnhighlight());
         e.preventDefault();
         e.stopPropagation();
     }
 
     onPointerEnterCollapsed(e: any) {
-        DocServer.GetRefField(this.props.docId).then(async dashDoc => dashDoc instanceof Doc && Doc.linkFollowHighlight(dashDoc, false));
+        this._dashDoc.then(async dashDoc => dashDoc instanceof Doc && Doc.linkFollowHighlight(dashDoc, false));
         e.preventDefault();
         e.stopPropagation();
     }
@@ -82,7 +113,7 @@ export class DashDocCommentViewInternal extends React.Component<IDashDocCommentV
             const tr = this.props.view.state.tr.setNodeMarkup(target.pos, undefined, { ...target.node.attrs, hidden: target.node.attrs.hidden ? false : true });
             this.props.view.dispatch(tr.setSelection(TextSelection.create(tr.doc, this.props.getPos() + (expand ? 2 : 1)))); // update the attrs
             setTimeout(() => {
-                expand && DocServer.GetRefField(this.props.docId).then(async dashDoc => dashDoc instanceof Doc && Doc.linkFollowHighlight(dashDoc));
+                expand && this._dashDoc.then(async dashDoc => dashDoc instanceof Doc && Doc.linkFollowHighlight(dashDoc));
                 try {
                     this.props.view.dispatch(this.props.view.state.tr.setSelection(TextSelection.create(this.props.view.state.tr.doc, this.props.getPos() + (expand ? 2 : 1))));
                 } catch (e) {}
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 0b857794b..973f90501 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -68,6 +68,7 @@ import { RichTextRules } from './RichTextRules';
 import { schema } from './schema_rts';
 import { SummaryView } from './SummaryView';
 import { CollectionView } from '../../collections/CollectionView';
+import { PaintButtonView } from './PaintButtonView';
 // import * as applyDevTools from 'prosemirror-dev-tools';
 @observer
 export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implements ViewBoxInterface {
@@ -1410,6 +1411,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
                     dashField(node: any, view: any, getPos: any) {
                         return new DashFieldView(node, view, getPos, self);
                     },
+                    paintButton(node: any, view: any, getPos: any) {
+                        return new PaintButtonView(node, view, getPos, self);
+                    },
                     equation(node: any, view: any, getPos: any) {
                         return new EquationView(node, view, getPos, self);
                     },
@@ -1538,15 +1542,16 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
         this._downX = e.clientX;
         this._downY = e.clientY;
         this._downTime = Date.now();
-        this._hadDownFocus = this.ProseRef?.children[0].className.includes('focused') ?? false;
         FormattedTextBoxComment.textBox = this;
         if (e.button === 0 && this._props.rootSelected?.() && !e.altKey && !e.ctrlKey && !e.metaKey) {
             if (e.clientX < this.ProseRef!.getBoundingClientRect().right) {
                 // stop propagation if not in sidebar, otherwise nested boxes will lose focus to outer boxes.
                 e.stopPropagation(); // if the text box's content is active, then it consumes all down events
                 document.addEventListener('pointerup', this.onSelectEnd);
+                (this.ProseRef?.children?.[0] as any).focus();
             }
         }
+        this._hadDownFocus = this.ProseRef?.children[0].className.includes('focused') ?? false;
         if (e.button === 2 || (e.button === 0 && e.ctrlKey)) {
             e.preventDefault();
         }
diff --git a/src/client/views/nodes/formattedText/PaintButtonView.tsx b/src/client/views/nodes/formattedText/PaintButtonView.tsx
new file mode 100644
index 000000000..74423c772
--- /dev/null
+++ b/src/client/views/nodes/formattedText/PaintButtonView.tsx
@@ -0,0 +1,113 @@
+import { action, computed, IReactionDisposer, makeObservable } from 'mobx';
+import { observer } from 'mobx-react';
+import * as React from 'react';
+import * as ReactDOM from 'react-dom/client';
+import { Doc } from '../../../../fields/Doc';
+import { ObservableReactComponent } from '../../ObservableReactComponent';
+import './DashFieldView.scss';
+import { FormattedTextBox } from './FormattedTextBox';
+import { CollectionViewType } from '../../../documents/DocumentTypes';
+import { CollectionView } from '../../collections/CollectionView';
+import { StrCast } from '../../../../fields/Types';
+
+export class PaintButtonView {
+    dom: HTMLDivElement; // container for label and value
+    root: any;
+    node: any;
+    tbox: FormattedTextBox;
+
+    constructor(node: any, view: any, getPos: any, tbox: FormattedTextBox) {
+        this.node = node;
+        this.tbox = tbox;
+        this.dom = document.createElement('div');
+        this.dom.style.width = node.attrs.width;
+        this.dom.style.height = node.attrs.height;
+        this.dom.style.position = 'relative';
+        this.dom.style.display = 'inline-block';
+        this.dom.onkeypress = function (e: any) {
+            e.stopPropagation();
+        };
+        this.dom.onkeydown = function (e: any) {
+            e.stopPropagation();
+        };
+        this.dom.onkeyup = function (e: any) {
+            e.stopPropagation();
+        };
+        this.dom.onmousedown = function (e: any) {
+            e.stopPropagation();
+        };
+
+        this.root = ReactDOM.createRoot(this.dom);
+        this.root.render(<PaintButtonViewInternal node={node} getPos={getPos} width={node.attrs.width} height={node.attrs.height} tbox={tbox} />);
+    }
+    destroy() {
+        setTimeout(() => {
+            try {
+                this.root.unmount();
+            } catch {}
+        });
+    }
+    deselectNode() {
+        this.dom.classList.remove('ProseMirror-selectednode');
+    }
+    selectNode() {
+        this.dom.classList.add('ProseMirror-selectednode');
+    }
+}
+
+interface IPaintButtonViewInternal {
+    tbox: FormattedTextBox;
+    width: number;
+    height: number;
+    node: any;
+    getPos: any;
+}
+
+@observer
+export class PaintButtonViewInternal extends ObservableReactComponent<IPaintButtonViewInternal> {
+    _reactionDisposer: IReactionDisposer | undefined;
+    _textBoxDoc: Doc;
+
+    constructor(props: IPaintButtonViewInternal) {
+        super(props);
+        makeObservable(this);
+        this._textBoxDoc = this._props.tbox.Document;
+    }
+
+    return100 = () => 100;
+    @computed get _checked() {
+        return this._props.tbox.Document.onClick ? true : false;
+    }
+
+    onCheckClick = () => {
+        const textView = this._props.tbox.DocumentView?.();
+        if (textView) {
+            const paintedField = 'layout_' + this._props.tbox.fieldKey + 'Painted';
+            const layoutFieldKey = StrCast(textView.layoutDoc.layout_fieldKey, 'layout');
+            if (textView.layoutDoc.onClick) {
+                textView.layoutDoc[paintedField] = undefined;
+                textView.layoutDoc.onClick = undefined;
+            } else {
+                textView.layoutDoc.type_collection = CollectionViewType.Freeform;
+                textView.dataDoc[paintedField] = CollectionView.LayoutString(this._props.tbox.fieldKey);
+                textView.layoutDoc.layout_fieldKey = paintedField;
+                textView.setToggleDetail(layoutFieldKey.replace('layout_', '').replace('layout', ''));
+                textView.layoutDoc.layout_fieldKey = layoutFieldKey;
+            }
+        }
+    };
+
+    render() {
+        return (
+            <div
+                className="dashFieldView"
+                style={{
+                    width: this._props.width,
+                    height: this._props.height,
+                    pointerEvents: this._props.tbox._props.isSelected() || this._props.tbox.isAnyChildContentActive?.() ? undefined : 'none',
+                }}>
+                <input type="checkbox" value="paint" checked={this._checked} onChange={e => this.onCheckClick()} />
+            </div>
+        );
+    }
+}
diff --git a/src/client/views/nodes/formattedText/RichTextRules.ts b/src/client/views/nodes/formattedText/RichTextRules.ts
index be32a2c4a..8f7bc5282 100644
--- a/src/client/views/nodes/formattedText/RichTextRules.ts
+++ b/src/client/views/nodes/formattedText/RichTextRules.ts
@@ -4,8 +4,7 @@ import { Doc, StrListCast } from '../../../../fields/Doc';
 import { DocData } from '../../../../fields/DocSymbols';
 import { Id } from '../../../../fields/FieldSymbols';
 import { List } from '../../../../fields/List';
-import { ComputedField } from '../../../../fields/ScriptField';
-import { NumCast } from '../../../../fields/Types';
+import { NumCast, StrCast } from '../../../../fields/Types';
 import { Utils } from '../../../../Utils';
 import { DocServer } from '../../../DocServer';
 import { Docs, DocUtils } from '../../../documents/Documents';
@@ -15,6 +14,7 @@ import { RichTextMenu } from './RichTextMenu';
 import { schema } from './schema_rts';
 import { CollectionView } from '../../collections/CollectionView';
 import { CollectionViewType } from '../../../documents/DocumentTypes';
+import { ContextMenu } from '../../ContextMenu';
 
 export class RichTextRules {
     public Document: Doc;
@@ -69,23 +69,39 @@ export class RichTextRules {
 
             // ``` create code block
             textblockTypeInputRule(/^```$/, schema.nodes.code_block),
-
-            new InputRule(new RegExp(/^\^@paint/), (state, match, start, end) => {
-                const { dataDoc, layoutDoc, fieldKey } = this.TextBox;
-                layoutDoc.type_collection = CollectionViewType.Freeform;
-                dataDoc.layout_painted = CollectionView.LayoutString('painted');
-                const layoutFieldKey = layoutDoc.layout_fieldKey;
-                layoutDoc.layout_fieldKey = 'layout_painted';
-                this.TextBox.DocumentView?.().setToggleDetail();
-                layoutDoc.layout_fieldKey = layoutFieldKey;
-                dataDoc.paintFunc = ComputedField.MakeFunction(`toJavascriptString(this['${fieldKey}']?.Text)`);
-                const comment = '/* this is now a paint func */';
-                const tr = state.tr
-                    .deleteRange(start, end)
-                    .insertText(comment)
-                    .insert(start + comment.length, schema.nodes.code_block.create());
-                return tr.setSelection(new TextSelection(tr.doc.resolve(start + comment.length + 2)));
-            }),
+            new InputRule(
+                new RegExp(/(^|\n)\^@paint/), // for code blocks '^' means the beginning of the block, not the line, so need to test for \n
+                (state, match, start, end) => {
+                    const { dataDoc, layoutDoc, fieldKey } = this.TextBox;
+                    layoutDoc.type_collection = CollectionViewType.Freeform;
+                    const paintedField = 'layout_' + this.TextBox.fieldKey + 'Painted';
+                    dataDoc[paintedField] = CollectionView.LayoutString(this.TextBox.fieldKey);
+                    const layoutFieldKey = StrCast(layoutDoc.layout_fieldKey);
+                    layoutDoc.layout_fieldKey = paintedField;
+                    this.TextBox.DocumentView?.().setToggleDetail(layoutFieldKey.replace('layout_', '').replace('layout', ''));
+                    layoutDoc.layout_fieldKey = layoutFieldKey;
+                    const comment = '/* enable as paint function ';
+                    const endComment = ' */\n';
+                    const inCode = state.tr.selection.$anchor.node().type === schema.nodes.code_block;
+                    if (inCode) {
+                        const tr = state.tr
+                            .deleteRange(start, end)
+                            .insertText(comment)
+                            .insert(start + comment.length, schema.nodes.paintButton.create())
+                            .insertText(endComment);
+                        return tr.setSelection(new TextSelection(tr.doc.resolve(start + comment.length + endComment.length + 1)));
+                    } else {
+                        const tr = state.tr
+                            .deleteRange(start, end)
+                            .insertText(comment)
+                            .insert(start + comment.length, schema.nodes.paintButton.create())
+                            .insertText(endComment)
+                            .insert(start + comment.length + endComment.length + 1, schema.nodes.code_block.create());
+                        return tr.setSelection(new TextSelection(tr.doc.resolve(start + comment.length + endComment.length + 3)));
+                    }
+                },
+                { inCode: true }
+            ),
 
             // %<font-size> set the font size
             new InputRule(new RegExp(/%([0-9]+)\s$/), (state, match, start, end) => {
@@ -93,6 +109,32 @@ export class RichTextRules {
                 return state.tr.deleteRange(start, end).addStoredMark(schema.marks.pFontSize.create({ fontSize: size }));
             }),
 
+            //Create annotation to a field on the text document
+            new InputRule(new RegExp(/>::$/), (state, match, start, end) => {
+                const creator = (doc: Doc) => {
+                    const textDoc = this.Document[DocData];
+                    const numInlines = NumCast(textDoc.inlineTextCount);
+                    textDoc.inlineTextCount = numInlines + 1;
+                    const node = (state.doc.resolve(start) as any).nodeAfter;
+                    const newNode = schema.nodes.dashComment.create({ docId: doc[Id], reflow: false });
+                    const dashDoc = schema.nodes.dashDoc.create({ width: 75, height: 35, title: 'dashDoc', docId: doc[Id], float: 'right' });
+                    const sm = state.storedMarks || undefined;
+                    this.TextBox.EditorView?.dispatch(
+                        node
+                            ? this.TextBox.EditorView.state.tr
+                                  .insert(start, newNode)
+                                  .replaceRangeWith(start + 1, end + 2, dashDoc)
+                                  .insertText(' ', start + 2)
+                                  .setStoredMarks([...node.marks, ...(sm ? sm : [])])
+                            : this.TextBox.EditorView.state.tr
+                    );
+                };
+                DocUtils.addDocumentCreatorMenuItems(creator, creator, 200, 200);
+                const cm = ContextMenu.Instance;
+                cm.displayMenu(200, 200, undefined, true);
+
+                return null;
+            }),
             //Create annotation to a field on the text document
             new InputRule(new RegExp(/>>$/), (state, match, start, end) => {
                 const textDoc = this.Document[DocData];
@@ -117,7 +159,7 @@ export class RichTextRules {
                 textDoc[inlineLayoutKey] = FormattedTextBox.LayoutString(inlineFieldKey); // create a layout string for the layout key that will render the annotation text
                 textDoc[inlineFieldKey] = ''; // set a default value for the annotation
                 const node = (state.doc.resolve(start) as any).nodeAfter;
-                const newNode = schema.nodes.dashComment.create({ docId: textDocInline[Id] });
+                const newNode = schema.nodes.dashComment.create({ docId: textDocInline[Id], reflow: true });
                 const dashDoc = schema.nodes.dashDoc.create({ width: 75, height: 35, title: 'dashDoc', docId: textDocInline[Id], float: 'right' });
                 const sm = state.storedMarks || undefined;
                 const replaced = node
@@ -265,49 +307,53 @@ export class RichTextRules {
             // [[fieldKey]] => show field
             // [[fieldKey=value]] => show field and also set its value
             // [[fieldKey:docTitle]] => show field of doc
-            new InputRule(new RegExp(/\[\[([a-zA-Z_\? \-0-9]*)(=[a-zA-Z_@\? /\-0-9]*)?(:[a-zA-Z_@:\.\? \-0-9]+)?\]\]$/), (state, match, start, end) => {
-                const fieldKey = match[1];
-                const docTitle = match[3]?.replace(':', '');
-                const value = match[2]?.substring(1);
-                const linkToDoc = (target: Doc) => {
-                    const rstate = this.TextBox.EditorView?.state;
-                    const selection = rstate?.selection.$from.pos;
-                    if (rstate) {
-                        this.TextBox.EditorView?.dispatch(rstate.tr.setSelection(new TextSelection(rstate.doc.resolve(start), rstate.doc.resolve(end - 3))));
-                    }
+            new InputRule(
+                new RegExp(/\[\[([a-zA-Z_\? \-0-9]*)(=[a-zA-Z_@\? /\-0-9]*)?(:[a-zA-Z_@:\.\? \-0-9]+)?\]\]$/),
+                (state, match, start, end) => {
+                    const fieldKey = match[1];
+                    const docTitle = match[3]?.replace(':', '');
+                    const value = match[2]?.substring(1);
+                    const linkToDoc = (target: Doc) => {
+                        const rstate = this.TextBox.EditorView?.state;
+                        const selection = rstate?.selection.$from.pos;
+                        if (rstate) {
+                            this.TextBox.EditorView?.dispatch(rstate.tr.setSelection(new TextSelection(rstate.doc.resolve(start), rstate.doc.resolve(end - 3))));
+                        }
 
-                    DocUtils.MakeLink(this.TextBox.getAnchor(true), target, { link_relationship: 'portal to:portal from' });
+                        DocUtils.MakeLink(this.TextBox.getAnchor(true), target, { link_relationship: 'portal to:portal from' });
 
-                    const fstate = this.TextBox.EditorView?.state;
-                    if (fstate && selection) {
-                        this.TextBox.EditorView?.dispatch(fstate.tr.setSelection(new TextSelection(fstate.doc.resolve(selection))));
-                    }
-                };
-                const getTitledDoc = (docTitle: string) => {
-                    if (!DocServer.FindDocByTitle(docTitle)) {
-                        Doc.AddToMyPublished(Docs.Create.TextDocument('', { title: docTitle, _width: 400, _layout_autoHeight: true }));
-                    }
-                    const titledDoc = DocServer.FindDocByTitle(docTitle);
-                    return titledDoc ? Doc.BestEmbedding(titledDoc) : titledDoc;
-                };
-                if (!fieldKey) {
-                    if (docTitle) {
-                        const target = getTitledDoc(docTitle);
-                        if (target) {
-                            setTimeout(() => linkToDoc(target));
-                            return state.tr.deleteRange(end - 1, end).deleteRange(start, start + 3);
+                        const fstate = this.TextBox.EditorView?.state;
+                        if (fstate && selection) {
+                            this.TextBox.EditorView?.dispatch(fstate.tr.setSelection(new TextSelection(fstate.doc.resolve(selection))));
+                        }
+                    };
+                    const getTitledDoc = (docTitle: string) => {
+                        if (!DocServer.FindDocByTitle(docTitle)) {
+                            Doc.AddToMyPublished(Docs.Create.TextDocument('', { title: docTitle, _width: 400, _layout_autoHeight: true }));
+                        }
+                        const titledDoc = DocServer.FindDocByTitle(docTitle);
+                        return titledDoc ? Doc.BestEmbedding(titledDoc) : titledDoc;
+                    };
+                    if (!fieldKey) {
+                        if (docTitle) {
+                            const target = getTitledDoc(docTitle);
+                            if (target) {
+                                setTimeout(() => linkToDoc(target));
+                                return state.tr.deleteRange(end - 1, end).deleteRange(start, start + 3);
+                            }
                         }
+                        return state.tr;
                     }
-                    return state.tr;
-                }
-                if (value !== '' && value !== undefined) {
-                    const num = value.match(/^[0-9.]$/);
-                    this.Document[DocData][fieldKey] = value === 'true' ? true : value === 'false' ? false : num ? Number(value) : value;
-                }
-                const target = getTitledDoc(docTitle);
-                const fieldView = state.schema.nodes.dashField.create({ fieldKey, docId: target?.[Id], hideKey: false });
-                return state.tr.setSelection(new TextSelection(state.doc.resolve(start), state.doc.resolve(end))).replaceSelectionWith(fieldView, true);
-            }),
+                    if (value !== '' && value !== undefined) {
+                        const num = value.match(/^[0-9.]$/);
+                        this.Document[DocData][fieldKey] = value === 'true' ? true : value === 'false' ? false : num ? Number(value) : value;
+                    }
+                    const target = getTitledDoc(docTitle);
+                    const fieldView = state.schema.nodes.dashField.create({ fieldKey, docId: target?.[Id], hideKey: false });
+                    return state.tr.setSelection(new TextSelection(state.doc.resolve(start), state.doc.resolve(end))).replaceSelectionWith(fieldView, true);
+                },
+                { inCode: true }
+            ),
 
             // create a text display of a metadata field on this or another document, or create a hyperlink portal to another document
             // wiki:title
diff --git a/src/client/views/nodes/formattedText/nodes_rts.ts b/src/client/views/nodes/formattedText/nodes_rts.ts
index 31f001b11..4706a97fa 100644
--- a/src/client/views/nodes/formattedText/nodes_rts.ts
+++ b/src/client/views/nodes/formattedText/nodes_rts.ts
@@ -179,6 +179,7 @@ export const nodes: { [index: string]: NodeSpec } = {
     dashComment: {
         attrs: {
             docId: { default: '' },
+            reflow: { default: true },
         },
         inline: true,
         group: 'inline',
@@ -275,6 +276,17 @@ export const nodes: { [index: string]: NodeSpec } = {
         },
     },
 
+    paintButton: {
+        inline: true,
+        attrs: {},
+        group: 'inline',
+        draggable: false,
+        toDOM(node) {
+            const attrs = { style: `width: ${node.attrs.width}, height: ${node.attrs.height}` };
+            return ['div', { ...node.attrs, ...attrs }];
+        },
+    },
+
     video: {
         inline: true,
         attrs: {
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index 67f09f37b..30e3aa5f0 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -60,7 +60,7 @@ export namespace Field {
             case 'number':
             case 'boolean':rawjava = String(field);
                            break;
-            default:       rawjava = field?.[ToJavascriptString]?.() ?? 'null';
+            default:       rawjava = field?.[ToJavascriptString]?.() ?? '';
         } // prettier-ignore
         var script = rawjava;
         // this is a bit hacky, but we treat '^@' references to a published document
@@ -866,7 +866,7 @@ export namespace Doc {
     export function FindReferences(infield: Doc | List<any>, references: Set<Doc>, system: boolean | undefined) {
         if (infield instanceof Promise) return;
         if (!(infield instanceof Doc)) {
-            infield.forEach(val => (val instanceof Doc || val instanceof List) && FindReferences(val, references, system));
+            infield?.forEach(val => (val instanceof Doc || val instanceof List) && FindReferences(val, references, system));
             return;
         }
         const doc = infield as Doc;
@@ -1032,13 +1032,13 @@ export namespace Doc {
     //  This function converts a generic field layout display into a field layout that displays a specific
     // metadata field indicated by the title of the template field (not the default field that it was rendering)
     //
-    export function MakeMetadataFieldTemplate(templateField: Doc, templateDoc: Opt<Doc>): boolean {
+    export function MakeMetadataFieldTemplate(templateField: Doc, templateDoc: Opt<Doc>, keepFieldKey = false): boolean {
         // find the metadata field key that this template field doc will display (indicated by its title)
-        const metadataFieldKey = StrCast(templateField.isTemplateForField) || StrCast(templateField.title).replace(/^-/, '') || Doc.LayoutFieldKey(templateField);
+        const metadataFieldKey = keepFieldKey ? Doc.LayoutFieldKey(templateField) : StrCast(templateField.isTemplateForField) || StrCast(templateField.title).replace(/^-/, '') || Doc.LayoutFieldKey(templateField);
 
         // update the original template to mark it as a template
         templateField.isTemplateForField = metadataFieldKey;
-        templateField.title = metadataFieldKey;
+        !keepFieldKey && (templateField.title = metadataFieldKey);
 
         const templateFieldValue = templateField[metadataFieldKey] || templateField[Doc.LayoutFieldKey(templateField)];
         const templateCaptionValue = templateField.caption;
-- 
cgit v1.2.3-70-g09d2


From 4cce99ec8d0bb2a45e19dd967dbbb8348c68abe8 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Wed, 28 Feb 2024 22:46:23 -0500
Subject: fixed double click of some icons to make them persist (circle,
 rectangle, fitall).

---
 src/client/util/CurrentUserUtils.ts                |  4 +---
 src/client/views/global/globalScripts.ts           | 12 ++++++------
 src/client/views/nodes/FontIconBox/FontIconBox.tsx | 10 ++++++++--
 3 files changed, 15 insertions(+), 11 deletions(-)

(limited to 'src/client/views/nodes/FontIconBox/FontIconBox.tsx')

diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index 289384be0..84bed08fa 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -611,9 +611,7 @@ export class CurrentUserUtils {
         return [
             { title: "Snap",      icon: "th",                  toolTip: "Show Snap Lines",     btnType: ButtonType.ToggleButton, ignoreClick: true, expertMode: false, toolType:"snaplines",  funcs: {}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}}, // Only when floating document is selected in freeform
             { title: "Grid",      icon: "border-all",          toolTip: "Show Grid",           btnType: ButtonType.ToggleButton, ignoreClick: true, expertMode: false, toolType:"grid",       funcs: {}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}}, // Only when floating document is selected in freeform
-            { title: "View All",  icon: "object-group",        toolTip: "Keep all Docs in View",btnType: ButtonType.ToggleButton, ignoreClick:true, expertMode: false, toolType:"viewAll",    funcs: {}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}}, // Only when floating document is selected in freeform
-            // want the same style as toggle button, but don't want it to act as an actual toggle, so set disableToggle to true,
-            { title: "Fit All",   icon: "arrows-left-right",   toolTip: "Fit Docs to View (once)",btnType: ButtonType.ClickButton,ignoreClick:false,expertMode: false, toolType:"fitOnce",    funcs: {}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}}, // Only when floating document is selected in freeform
+            { title: "Fit All",   icon: "object-group",        toolTip: "Fit Docs to View (double click to make sticky)",btnType: ButtonType.ToggleButton, ignoreClick:true, expertMode: false, toolType:"viewAll",    funcs: {}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}', onDoubleClick: '{ return showFreeform(this.toolType, _readOnly_, true);}'}}, // Only when floating document is selected in freeform
             { title: "Clusters",  icon: "braille",             toolTip: "Show Doc Clusters",   btnType: ButtonType.ToggleButton, ignoreClick: true, expertMode: false, toolType:"clusters",   funcs: {}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}}, // Only when floating document is selected in freeform
             { title: "Cards",     icon: "brain",               toolTip: "Flashcards",          btnType: ButtonType.ToggleButton, ignoreClick: true, expertMode: false, toolType:"flashcards", funcs: {}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}}, // Only when floating document is selected in freeform
             { title: "Arrange",   icon:"arrow-down-short-wide",toolTip:"Toggle Auto Arrange",  btnType: ButtonType.ToggleButton, ignoreClick: true, expertMode: false, toolType:"arrange",    funcs: {hidden: 'IsNoviceMode()'}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}}, // Only when floating document is selected in freeform
diff --git a/src/client/views/global/globalScripts.ts b/src/client/views/global/globalScripts.ts
index e73ab25b5..33704e8fe 100644
--- a/src/client/views/global/globalScripts.ts
+++ b/src/client/views/global/globalScripts.ts
@@ -98,7 +98,7 @@ ScriptingGlobals.add(function toggleOverlay(checkResult?: boolean) {
     selected ? selected.CollectionFreeFormDocumentView?.float() : console.log('[FontIconBox.tsx] toggleOverlay failed');
 });
 
-ScriptingGlobals.add(function showFreeform(attr: 'flashcards' | 'center' | 'grid' | 'snaplines' | 'clusters' | 'arrange' | 'viewAll' | 'fitOnce', checkResult?: boolean) {
+ScriptingGlobals.add(function showFreeform(attr: 'flashcards' | 'center' | 'grid' | 'snaplines' | 'clusters' | 'arrange' | 'viewAll' | 'fitOnce', checkResult?: boolean, persist?: boolean) {
     const selected = SelectionManager.Docs.lastElement();
     // prettier-ignore
     const map: Map<'flashcards' | 'center' |'grid' | 'snaplines' | 'clusters' | 'arrange'| 'viewAll' | 'fitOnce', { waitForRender?: boolean, checkResult: (doc:Doc) => any; setDoc: (doc:Doc, dv:DocumentView) => void;}> = new Map([
@@ -112,16 +112,16 @@ ScriptingGlobals.add(function showFreeform(attr: 'flashcards' | 'center' | 'grid
         }],
         ['viewAll', {
             checkResult: (doc:Doc) => BoolCast(doc?._freeform_fitContentsToBox, false),
-            setDoc: (doc:Doc,dv:DocumentView) => doc._freeform_fitContentsToBox = !doc._freeform_fitContentsToBox,
+            setDoc: (doc:Doc,dv:DocumentView) => {
+                if (persist) doc._freeform_fitContentsToBox = !doc._freeform_fitContentsToBox;
+                else if (doc._freeform_fitContentsToBox) doc._freeform_fitContentsToBox = undefined;
+                else (dv.ComponentView as CollectionFreeFormView)?.fitContentOnce();
+            },
         }],
         ['center', {
             checkResult: (doc:Doc) => BoolCast(doc?._stacking_alignCenter, false),
             setDoc: (doc:Doc,dv:DocumentView) => doc._stacking_alignCenter = !doc._stacking_alignCenter,
         }],
-        ['fitOnce', {
-            checkResult: (doc:Doc) => false,
-            setDoc: (doc:Doc, dv:DocumentView) => (dv.ComponentView as CollectionFreeFormView)?.fitContentOnce()
-        }],
         ['clusters', {
             waitForRender: true, // flags that undo batch should terminate after a re-render giving the script the chance to fire
             checkResult: (doc:Doc) => BoolCast(doc?._freeform_useClusters, false),
diff --git a/src/client/views/nodes/FontIconBox/FontIconBox.tsx b/src/client/views/nodes/FontIconBox/FontIconBox.tsx
index 3577cc8d9..91b6de80b 100644
--- a/src/client/views/nodes/FontIconBox/FontIconBox.tsx
+++ b/src/client/views/nodes/FontIconBox/FontIconBox.tsx
@@ -7,7 +7,7 @@ import * as React from 'react';
 import { Doc, DocListCast, StrListCast } from '../../../../fields/Doc';
 import { ScriptField } from '../../../../fields/ScriptField';
 import { BoolCast, Cast, DocCast, NumCast, ScriptCast, StrCast } from '../../../../fields/Types';
-import { emptyFunction, setupMoveUpEvents, Utils } from '../../../../Utils';
+import { emptyFunction, returnTrue, setupMoveUpEvents, Utils } from '../../../../Utils';
 import { CollectionViewType, DocumentType } from '../../../documents/DocumentTypes';
 import { SelectionManager } from '../../../util/SelectionManager';
 import { SettingsManager } from '../../../util/SettingsManager';
@@ -314,6 +314,7 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() {
         const tooltip = StrCast(this.Document.toolTip);
 
         const script = ScriptCast(this.Document.onClick);
+        const double = ScriptCast(this.Document.onDoubleClick);
         const toggleStatus = script ? script.script.run({ this: this.Document, self: this.Document, value: undefined, _readOnly_: true }).result : false;
         // Colors
         const color = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Color);
@@ -330,7 +331,12 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() {
                 //background={SettingsManager.userBackgroundColor}
                 icon={this.Icon(color)!}
                 label={this.label}
-                onPointerDown={() => script.script.run({ this: this.Document, self: this.Document, value: !toggleStatus, _readOnly_: false })}
+                onPointerDown={e =>
+                    setupMoveUpEvents(this, e, returnTrue, emptyFunction, (e, doubleTap) => {
+                        !doubleTap && script.script.run({ this: this.Document, self: this.Document, value: !toggleStatus, _readOnly_: false });
+                        doubleTap && double.script.run({ this: this.Document, self: this.Document, value: !toggleStatus, _readOnly_: false });
+                    })
+                }
             />
         );
     }
-- 
cgit v1.2.3-70-g09d2

