From 2e99ef0512b8f1b5c089637643ff6addf5afd943 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Mon, 4 Mar 2024 16:07:52 -0500
Subject: cleaned up a number of things related to autoHeight and
 stacking/masonry/notetaking views

---
 src/client/views/PropertiesButtons.tsx | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

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

diff --git a/src/client/views/PropertiesButtons.tsx b/src/client/views/PropertiesButtons.tsx
index cb38ab602..3cb835e39 100644
--- a/src/client/views/PropertiesButtons.tsx
+++ b/src/client/views/PropertiesButtons.tsx
@@ -486,8 +486,7 @@ export class PropertiesButtons extends React.Component<{}, {}> {
         const isImage = layoutField instanceof ImageField;
         const isMap = this.selectedDoc?.type === DocumentType.MAP;
         const isCollection = this.selectedDoc?.type === DocumentType.COL;
-        //TODO: will likely need to create separate note-taking view type here
-        const isStacking = this.selectedDoc?._type_collection === CollectionViewType.Stacking || this.selectedDoc?._type_collection === CollectionViewType.NoteTaking;
+        const isStacking = [CollectionViewType.Stacking, CollectionViewType.Masonry, CollectionViewType.NoteTaking].includes(this.selectedDoc?._type_collection as any);
         const isFreeForm = this.selectedDoc?._type_collection === CollectionViewType.Freeform;
         const isTree = this.selectedDoc?._type_collection === CollectionViewType.Tree;
         const isTabView = this.selectedTabView;
-- 
cgit v1.2.3-70-g09d2


From c563aec906c5728a5563fefac6ab573a31375641 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Tue, 12 Mar 2024 09:09:59 -0400
Subject: made text templates be both layout templates and prototypes of new
 text documents.  fixed onPaint funcs to be undoable.  fixed comparisonBox to
 render a text box if it's fieldKey has a richtext field - this makes
 flashcard templates much easier.  fixed right-click on hyperlinks to bring up
 menu.   fixed layout_centered to be settable on templates.  added enable
 flashcard property for text.

---
 src/client/documents/Documents.ts                  | 32 ++++++++++++----------
 src/client/util/CurrentUserUtils.ts                |  4 +--
 src/client/views/PropertiesButtons.tsx             | 21 ++++++++++++--
 src/client/views/StyleProvider.tsx                 |  2 +-
 src/client/views/nodes/ComparisonBox.tsx           | 13 ++++++---
 src/client/views/nodes/DocumentView.tsx            | 27 ++++++++++++++----
 src/client/views/nodes/KeyValueBox.tsx             |  2 +-
 .../views/nodes/formattedText/FormattedTextBox.tsx |  4 +--
 .../views/nodes/formattedText/RichTextMenu.tsx     |  4 +--
 .../views/nodes/formattedText/RichTextRules.ts     |  2 +-
 10 files changed, 75 insertions(+), 36 deletions(-)

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

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 7a3b965fe..a13edec77 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -1982,27 +1982,29 @@ export namespace DocUtils {
     }
 
     export function GetNewTextDoc(title: string, x: number, y: number, width?: number, height?: number, annotationOn?: Doc, backgroundColor?: string) {
+        const defaultTextTemplate = DocCast(Doc.UserDoc().defaultTextLayout);
         const tbox = Docs.Create.TextDocument('', {
             annotationOn,
             backgroundColor,
-            _width: width || 200,
-            _height: 35,
-            x: x,
-            y: y,
-            _layout_centered: BoolCast(Doc.UserDoc().layout_centered),
-            _layout_fitWidth: true,
-            _layout_autoHeight: true,
-            _layout_enableAltContentUI: BoolCast(Doc.UserDoc().defaultToFlashcards),
+            x,
+            y,
             title,
+            ...(defaultTextTemplate
+                ? {} // if the new doc will inherit from a template, don't set any layout fields since that would block the inheritance
+                : {
+                      _width: width || 200,
+                      _height: 35,
+                      _layout_centered: BoolCast(Doc.UserDoc()._layout_centered),
+                      _layout_fitWidth: true,
+                      _layout_autoHeight: true,
+                      _layout_enableAltContentUI: BoolCast(Doc.UserDoc().defaultToFlashcards),
+                  }),
         });
 
-        const template = Doc.UserDoc().defaultTextLayout;
-        if (template instanceof Doc) {
-            // if a default text template is specified
-            tbox._width = NumCast(template._width);
-            tbox.layout_fieldKey = 'layout_' + StrCast(template.title);
-            Doc.GetProto(tbox)[StrCast(tbox.layout_fieldKey)] = template; // set the text doc's layout to render with the text template
-            tbox[DocData].proto = template; // and also set the text doc to inherit from the template (this allows the template to specify default field values)
+        if (defaultTextTemplate) {
+            tbox.layout_fieldKey = 'layout_' + StrCast(defaultTextTemplate.title);
+            Doc.GetProto(tbox)[StrCast(tbox.layout_fieldKey)] = defaultTextTemplate; // set the text doc's layout to render with the text template
+            tbox[DocData].proto = defaultTextTemplate; // and also set the text doc to inherit from the template (this allows the template to specify default field values)
         }
         return tbox;
     }
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index fc315fdbe..84a33500d 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -276,7 +276,7 @@ export class CurrentUserUtils {
             slide[DocData].text  = rtfield;
             slide[DocData].layout_textPainted = `<CollectionView {...props} fieldKey={'text'}/>`;
             slide[DocData]._type_collection = CollectionViewType.Freeform;
-            slide.onPaint = ScriptField.MakeScript(`toggleDetail(documentView, "textPainted", "")`, {documentView:"any"});
+            slide.onPaint = ScriptField.MakeScript(`toggleDetail(documentView, "textPainted")`, {documentView:"any"});
             return slide; 
         }
         const mermaidsApi = () => {
@@ -330,7 +330,7 @@ pie title Minerals in my tap water
             slide[DocData].text  = rtfield;
             slide[DocData].layout_textPainted = `<CollectionView {...props} fieldKey={'text'}/>`;
             slide[DocData]._type_collection = CollectionViewType.Freeform;
-            slide.onPaint = ScriptField.MakeScript(`toggleDetail(documentView, "textPainted", "")`, {documentView:"any"});
+            slide.onPaint = ScriptField.MakeScript(`toggleDetail(documentView, "textPainted")`, {documentView:"any"});
             return slide; 
         }
         const apis = [plotlyApi(), mermaidsApi()]
diff --git a/src/client/views/PropertiesButtons.tsx b/src/client/views/PropertiesButtons.tsx
index 3cb835e39..02f288a68 100644
--- a/src/client/views/PropertiesButtons.tsx
+++ b/src/client/views/PropertiesButtons.tsx
@@ -13,7 +13,8 @@ import { RxWidth } from 'react-icons/rx';
 import { TbEditCircle, TbEditCircleOff, TbHandOff, TbHandStop, TbHighlight, TbHighlightOff } from 'react-icons/tb';
 import { TfiBarChart } from 'react-icons/tfi';
 import { Doc, DocListCast, Opt } from '../../fields/Doc';
-import { RichTextField } from '../../fields/RichTextField';
+import { DocData } from '../../fields/DocSymbols';
+import { ScriptField } from '../../fields/ScriptField';
 import { BoolCast, ScriptCast } from '../../fields/Types';
 import { ImageField } from '../../fields/URLField';
 import { CollectionViewType, DocumentType } from '../documents/DocumentTypes';
@@ -27,6 +28,7 @@ import { InkingStroke } from './InkingStroke';
 import './PropertiesButtons.scss';
 import { Colors } from './global/globalEnums';
 import { DocumentView, OpenWhere } from './nodes/DocumentView';
+import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox';
 
 @observer
 export class PropertiesButtons extends React.Component<{}, {}> {
@@ -174,6 +176,20 @@ export class PropertiesButtons extends React.Component<{}, {}> {
         );
     }
 
+    @computed get flashcardButton() {
+        return this.propertyToggleBtn(
+            on => (on ? 'DISABLE FLASHCARD' : 'ENABLE FLASHCARD'),
+            'layout_textPainted',
+            on => `${on ? 'Flashcard enabled' : 'Flashcard disabled'} `,
+            on => <MdTouchApp />,
+            (dv, doc) => {
+                const on = doc.onPaint ? true : false;
+                doc[DocData].onPaint = on ? undefined : ScriptField.MakeScript(`toggleDetail(documentView, "textPainted")`, { documentView: 'any' });
+                doc[DocData].layout_textPainted = on ? undefined : `<ComparisonBox {...props} fieldKey={'${dv?.LayoutFieldKey ?? 'text'}'}/>`;
+            }
+        );
+    }
+
     @computed get fitContentButton() {
         return this.propertyToggleBtn(
             on => (on ? 'PREVIOUS VIEW' : 'VIEW ALL'), //'View All',
@@ -481,7 +497,7 @@ export class PropertiesButtons extends React.Component<{}, {}> {
 
     render() {
         const layoutField = this.selectedDoc?.[Doc.LayoutFieldKey(this.selectedDoc)];
-        const isText = layoutField instanceof RichTextField;
+        const isText = SelectionManager.Views.lastElement()?.ComponentView instanceof FormattedTextBox;
         const isInk = this.selectedDoc?.layout_isSvg;
         const isImage = layoutField instanceof ImageField;
         const isMap = this.selectedDoc?.type === DocumentType.MAP;
@@ -507,6 +523,7 @@ export class PropertiesButtons extends React.Component<{}, {}> {
                 {/* {toggle(this.freezeThumb)} */}
                 {toggle(this.forceActiveButton)}
                 {toggle(this.verticalAlignButton, { display: !isText ? 'none' : '' })}
+                {toggle(this.flashcardButton, { display: !isText ? 'none' : '' })}
                 {toggle(this.fitContentButton, { display: !isFreeForm && !isMap ? 'none' : '' })}
                 {/* {toggle(this.isLightboxButton, { display: !isFreeForm && !isMap ? 'none' : '' })} */}
                 {toggle(this.layout_autoHeightButton, { display: !isText && !isStacking && !isTree ? 'none' : '' })}
diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx
index 0794efe4c..ab811858a 100644
--- a/src/client/views/StyleProvider.tsx
+++ b/src/client/views/StyleProvider.tsx
@@ -61,7 +61,7 @@ function togglePaintView(e: React.MouseEvent, doc: Opt<Doc>, props: Opt<FieldVie
         value: undefined,
     };
     e.stopPropagation();
-    UndoManager.RunInBatch(() => doc && ScriptCast(doc.onPaint).script.run(scriptProps), 'togglePaintView');
+    ScriptCast(doc?.onPaint)?.script.run(scriptProps);
 }
 
 export function wavyBorderPath(pw: number, ph: number, inset: number = 0.05) {
diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx
index ef8c045cc..2b57178f4 100644
--- a/src/client/views/nodes/ComparisonBox.tsx
+++ b/src/client/views/nodes/ComparisonBox.tsx
@@ -14,6 +14,8 @@ import './ComparisonBox.scss';
 import { DocumentView } from './DocumentView';
 import { FieldView, FieldViewProps } from './FieldView';
 import { PinProps, PresBox } from './trails';
+import { FormattedTextBox } from './formattedText/FormattedTextBox';
+import { RichTextField } from '../../../fields/RichTextField';
 
 @observer
 export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implements ViewBoxInterface {
@@ -172,12 +174,15 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
         const displayDoc = (which: string) => {
             const whichDoc = DocCast(this.dataDoc[which]);
             const targetDoc = DocCast(whichDoc?.annotationOn, whichDoc);
-            return targetDoc ? (
+            // if there is no Doc in the first comparison slot, but the comparison box's fieldKey slot has a RichTextField, then render a text box to show the contents of the document's field key slot
+            const layoutTemplateString = !targetDoc && which.endsWith('1') && this.Document[this.fieldKey] instanceof RichTextField ? FormattedTextBox.LayoutString(this.fieldKey) : undefined;
+            return targetDoc || layoutTemplateString ? (
                 <>
                     <DocumentView
                         {...this._props}
-                        Document={targetDoc}
-                        TemplateDataDocument={undefined}
+                        renderDepth={this.props.renderDepth + 1}
+                        LayoutTemplateString={layoutTemplateString}
+                        Document={layoutTemplateString ? this.Document : targetDoc}
                         containerViewPath={this.DocumentView?.().docViewPath}
                         moveDocument={which.endsWith('1') ? this.moveDoc1 : this.moveDoc2}
                         removeDocument={which.endsWith('1') ? this.remDoc1 : this.remDoc2}
@@ -190,7 +195,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
                         hideLinkButton={true}
                         pointerEvents={this._isAnyChildContentActive ? undefined : returnNone}
                     />
-                    {clearButton(which)}
+                    {layoutTemplateString ? null : clearButton(which)}
                 </> // placeholder image if doc is missing
             ) : (
                 <div className="placeholder">
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 29266bd8e..40592c2cd 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -457,11 +457,11 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
 
     deleteClicked = undoable(() => this._props.removeDocument?.(this.Document), 'delete doc');
     setToggleDetail = undoable(
-        (defaultLayout: string, scriptFieldKey: 'onClick') =>
+        (scriptFieldKey: 'onClick') =>
             (this.Document[scriptFieldKey] = ScriptField.MakeScript(
                 `toggleDetail(documentView, "${StrCast(this.Document.layout_fieldKey)
                     .replace('layout_', '')
-                    .replace(/^layout$/, 'detail')}", "${defaultLayout}")`,
+                    .replace(/^layout$/, 'detail')}")`,
                 { documentView: 'any' }
             )),
         'set toggle detail'
@@ -1201,7 +1201,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 = (defaultLayout = '', scriptFieldKey = 'onClick') => this._docViewInternal?.setToggleDetail(defaultLayout, scriptFieldKey);
+    public setToggleDetail = (scriptFieldKey = 'onClick') => this._docViewInternal?.setToggleDetail(scriptFieldKey);
     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);
@@ -1279,7 +1279,22 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
         custom && DocUtils.makeCustomViewClicked(this.Document, Docs.Create.StackingDocument, layout, undefined);
     }, 'set custom view');
 
+    /**
+     * This switches between the current view of a Doc and a specified alternate layout view.
+     * The current view of the Doc is stored in the layout_default field so that it can be restored.
+     * If the current view of the Doc is already the specified alternate layout view, this will switch
+     * back to the original layout (stored in layout_default)
+     * @param detailLayoutKeySuffix the name of the alternate layout field key (NOTE: 'layout_' will be prepended to this string to get the actual field nam)
+     */
+    public toggleDetail = (detailLayoutKeySuffix: string) => {
+        const curLayout = StrCast(this.Document.layout_fieldKey).replace('layout_', '').replace('layout', '');
+        if (!this.Document.layout_default && curLayout !== detailLayoutKeySuffix) this.Document.layout_default = curLayout;
+        const defaultLayout = StrCast(this.Document.layout_default);
+        if (this.Document.layout_fieldKey === 'layout_' + detailLayoutKeySuffix) this.switchViews(defaultLayout ? true : false, defaultLayout, undefined, true);
+        else this.switchViews(true, detailLayoutKeySuffix, undefined, true);
+    };
     public switchViews = (custom: boolean, view: string, finished?: () => void, useExistingLayout = false) => {
+        const batch = UndoManager.StartBatch('switchView:' + view);
         runInAction(() => this._docViewInternal && (this._docViewInternal._animateScalingTo = 0.1)); // shrink doc
         setTimeout(
             action(() => {
@@ -1292,6 +1307,7 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
                 setTimeout(
                     action(() => {
                         this._docViewInternal && (this._docViewInternal._animateScalingTo = 0);
+                        batch.end();
                         finished?.();
                     }),
                     Math.max(0, (this._docViewInternal?.animateScaleTime() ?? 0) - 10)
@@ -1451,9 +1467,8 @@ ScriptingGlobals.add(function deiconifyViewToLightbox(documentView: DocumentView
     LightboxView.Instance.AddDocTab(documentView.Document, OpenWhere.lightbox, 'layout'); //, 0);
 });
 
-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);
+ScriptingGlobals.add(function toggleDetail(dv: DocumentView, detailLayoutKeySuffix: string) {
+    dv.toggleDetail(detailLayoutKeySuffix);
 });
 
 ScriptingGlobals.add(function updateLinkCollection(linkCollection: Doc, linkSource: Doc) {
diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx
index 39a45693e..89a5ac0b8 100644
--- a/src/client/views/nodes/KeyValueBox.tsx
+++ b/src/client/views/nodes/KeyValueBox.tsx
@@ -76,7 +76,7 @@ export class KeyValueBox extends ObservableReactComponent<FieldViewProps> {
         value = eq ? value.substring(1) : value;
         const dubEq = value.startsWith(':=') ? 'computed' : value.startsWith('$=') ? 'script' : false;
         value = dubEq ? value.substring(2) : value;
-        const options: ScriptOptions = { addReturn: true, typecheck: false, params: { this: Doc.name, self: Doc.name, _last_: 'any', _readOnly_: 'boolean' }, editable: true };
+        const options: ScriptOptions = { addReturn: true, typecheck: false, params: { this: Doc.name, self: Doc.name, documentView: 'any', _last_: 'any', _readOnly_: 'boolean' }, editable: true };
         if (dubEq) options.typecheck = false;
         const script = CompileScript(value, { ...options, transformer: DocumentIconContainer.getTransformer() });
         return !script.compiled ? undefined : { script, type: dubEq, onDelegate: eq };
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 1ff7274f8..1bd230891 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -1574,7 +1574,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
         }
         if (!state || !editor || !this.ProseRef?.children[0].className.includes('-focused')) return;
         if (!state.selection.empty && !(state.selection instanceof NodeSelection)) this.setupAnchorMenu();
-        else if (this._props.isContentActive()) {
+        else if (this._props.isContentActive() && !e.button) {
             const pcords = editor.posAtCoords({ left: e.clientX, top: e.clientY });
             let xpos = pcords?.pos || 0;
             while (xpos > 0 && !state.doc.resolve(xpos).node()?.isTextblock) {
@@ -2074,7 +2074,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
                         onScroll={this.onScroll}
                         onDrop={this.ondrop}>
                         <div
-                            className={`formattedTextBox-inner${rounded} ${this.layoutDoc.layout_centered ? 'centered' : ''}`}
+                            className={`formattedTextBox-inner${rounded} ${this.layoutDoc._layout_centered ? 'centered' : ''}`}
                             ref={this.createDropTarget}
                             style={{
                                 padding: StrCast(this.layoutDoc._textBoxPadding),
diff --git a/src/client/views/nodes/formattedText/RichTextMenu.tsx b/src/client/views/nodes/formattedText/RichTextMenu.tsx
index cd0cdaa74..dc2c06701 100644
--- a/src/client/views/nodes/formattedText/RichTextMenu.tsx
+++ b/src/client/views/nodes/formattedText/RichTextMenu.tsx
@@ -104,7 +104,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
         return this._activeAlignment;
     }
     @computed get textVcenter() {
-        return BoolCast(this.layoutDoc?.layout_centered);
+        return BoolCast(this.layoutDoc?._layout_centered);
     }
     _disposer: IReactionDisposer | undefined;
     componentDidMount() {
@@ -450,7 +450,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
     }
 
     vcenterToggle = (view: EditorView, dispatch: any) => {
-        this.layoutDoc && (this.layoutDoc.layout_centered = !this.layoutDoc.layout_centered);
+        this.layoutDoc && (this.layoutDoc._layout_centered = !this.layoutDoc._layout_centered);
     };
     align = (view: EditorView, dispatch: any, alignment: 'left' | 'right' | 'center') => {
         if (this.TextView?._props.rootSelected?.()) {
diff --git a/src/client/views/nodes/formattedText/RichTextRules.ts b/src/client/views/nodes/formattedText/RichTextRules.ts
index 9bd41f42c..d5c91fc09 100644
--- a/src/client/views/nodes/formattedText/RichTextRules.ts
+++ b/src/client/views/nodes/formattedText/RichTextRules.ts
@@ -78,7 +78,7 @@ export class RichTextRules {
                 this.TextBox.dataDoc[paintedField] = CollectionView.LayoutString(this.TextBox.fieldKey);
                 const layoutFieldKey = StrCast(this.TextBox.layoutDoc.layout_fieldKey); // save the current layout fieldkey
                 this.TextBox.layoutDoc.layout_fieldKey = paintedField; // setup the paint layout field key
-                this.TextBox.DocumentView?.().setToggleDetail(layoutFieldKey.replace('layout_', '').replace('layout', ''), 'onPaint'); // create the script to toggle between the painted and regular view
+                this.TextBox.DocumentView?.().setToggleDetail('onPaint'); // create the script to toggle between the painted and regular view
                 this.TextBox.layoutDoc.layout_fieldKey = layoutFieldKey; // restore the layout field key to text
 
                 return state.tr.delete(start, end).setBlockType(start, start, schema.nodes.code_block);
-- 
cgit v1.2.3-70-g09d2


From 1b592f74a7df8f6dd7b2881032725f26aedff403 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Thu, 28 Mar 2024 11:38:56 -0400
Subject: fixed keyvaluebox to show props document, never the doc in the
 fieldKey slot.  changed computedFIelds to do mobx caching.  changed text
 boxes to do updating from templates based on a fieldKey_autoUpdate flag
 combined with modification timestamps.  enabled comparison box to work with
 text fields in addition to docs.

---
 src/client/util/Scripting.ts                       |  9 +--
 src/client/views/InkControlPtHandles.tsx           | 85 ++++++++++++----------
 src/client/views/PropertiesButtons.tsx             |  4 +-
 src/client/views/nodes/ComparisonBox.tsx           |  6 +-
 src/client/views/nodes/KeyValueBox.tsx             |  2 +-
 .../views/nodes/formattedText/FormattedTextBox.tsx | 31 +++++---
 src/fields/Doc.ts                                  |  2 +-
 src/fields/ScriptField.ts                          | 22 ++++--
 8 files changed, 90 insertions(+), 71 deletions(-)

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

diff --git a/src/client/util/Scripting.ts b/src/client/util/Scripting.ts
index f5e162d16..31222aa50 100644
--- a/src/client/util/Scripting.ts
+++ b/src/client/util/Scripting.ts
@@ -88,15 +88,10 @@ function Run(script: string | undefined, customParams: string[], diagnostics: an
             }
 
             const result = compiledFunction.apply(thisParam, params).apply(thisParam, argsArray);
-            if (batch) {
-                batch.end();
-            }
-
+            batch?.end();
             return { success: true, result };
         } catch (error) {
-            if (batch) {
-                batch.end();
-            }
+            batch?.end();
             onError?.(script + ' ' + error);
             return { success: false, error, result: errorVal };
         }
diff --git a/src/client/views/InkControlPtHandles.tsx b/src/client/views/InkControlPtHandles.tsx
index 31b13d2c8..01d52135a 100644
--- a/src/client/views/InkControlPtHandles.tsx
+++ b/src/client/views/InkControlPtHandles.tsx
@@ -1,5 +1,5 @@
 import * as React from 'react';
-import { action, observable } from 'mobx';
+import { action, makeObservable, observable } from 'mobx';
 import { observer } from 'mobx-react';
 import { Doc } from '../../fields/Doc';
 import { ControlPoint, InkData, PointData } from '../../fields/InkField';
@@ -13,6 +13,7 @@ import { Colors } from './global/globalEnums';
 import { InkingStroke } from './InkingStroke';
 import { InkStrokeProperties } from './InkStrokeProperties';
 import { SnappingManager } from '../util/SnappingManager';
+import { ObservableReactComponent } from './ObservableReactComponent';
 
 export interface InkControlProps {
     inkDoc: Doc;
@@ -24,10 +25,15 @@ export interface InkControlProps {
 }
 
 @observer
-export class InkControlPtHandles extends React.Component<InkControlProps> {
+export class InkControlPtHandles extends ObservableReactComponent<InkControlProps> {
     @observable private _overControl = -1;
     get docView() {
-        return this.props.inkView.DocumentView?.();
+        return this._props.inkView.DocumentView?.();
+    }
+
+    constructor(props: InkControlProps) {
+        super(props);
+        makeObservable(this);
     }
 
     componentDidMount() {
@@ -42,51 +48,51 @@ export class InkControlPtHandles extends React.Component<InkControlProps> {
      */
     @action
     onControlDown = (e: React.PointerEvent, controlIndex: number): void => {
-        const ptFromScreen = this.props.inkView.ptFromScreen;
+        const ptFromScreen = this._props.inkView.ptFromScreen;
         if (ptFromScreen) {
             const order = controlIndex % 4;
-            const handleIndexA = ((order === 3 ? controlIndex - 1 : controlIndex - 2) + this.props.inkCtrlPoints.length) % this.props.inkCtrlPoints.length;
-            const handleIndexB = (order === 3 ? controlIndex + 2 : controlIndex + 1) % this.props.inkCtrlPoints.length;
-            const brokenIndices = Cast(this.props.inkDoc.brokenInkIndices, listSpec('number'));
+            const handleIndexA = ((order === 3 ? controlIndex - 1 : controlIndex - 2) + this._props.inkCtrlPoints.length) % this._props.inkCtrlPoints.length;
+            const handleIndexB = (order === 3 ? controlIndex + 2 : controlIndex + 1) % this._props.inkCtrlPoints.length;
+            const brokenIndices = Cast(this._props.inkDoc.brokenInkIndices, listSpec('number'));
             const wasSelected = InkStrokeProperties.Instance._currentPoint === controlIndex;
             if (!wasSelected) InkStrokeProperties.Instance._currentPoint = -1;
-            const origInk = this.props.inkCtrlPoints.slice();
+            const origInk = this._props.inkCtrlPoints.slice();
             setupMoveUpEvents(
                 this,
                 e,
                 action((e: PointerEvent, down: number[], delta: number[]) => {
-                    if (!this.props.inkView.controlUndo) this.props.inkView.controlUndo = UndoManager.StartBatch('drag ink ctrl pt');
+                    if (!this._props.inkView.controlUndo) this._props.inkView.controlUndo = UndoManager.StartBatch('drag ink ctrl pt');
                     const inkMoveEnd = ptFromScreen({ X: delta[0], Y: delta[1] });
                     const inkMoveStart = ptFromScreen({ X: 0, Y: 0 });
                     this.docView && InkStrokeProperties.Instance.moveControlPtHandle(this.docView, inkMoveEnd.X - inkMoveStart.X, inkMoveEnd.Y - inkMoveStart.Y, controlIndex, origInk);
                     return false;
                 }),
                 action(() => {
-                    if (this.props.inkView.controlUndo && this.docView) {
+                    if (this._props.inkView.controlUndo && this.docView) {
                         InkStrokeProperties.Instance.snapControl(this.docView, controlIndex);
                     }
-                    this.props.inkView.controlUndo?.end();
-                    this.props.inkView.controlUndo = undefined;
+                    this._props.inkView.controlUndo?.end();
+                    this._props.inkView.controlUndo = undefined;
                     UndoManager.FilterBatches(['data', 'x', 'y', 'width', 'height']);
                 }),
                 action((e: PointerEvent, doubleTap: boolean | undefined) => {
-                    const equivIndex = controlIndex === 0 ? this.props.inkCtrlPoints.length - 1 : controlIndex === this.props.inkCtrlPoints.length - 1 ? 0 : controlIndex;
+                    const equivIndex = controlIndex === 0 ? this._props.inkCtrlPoints.length - 1 : controlIndex === this._props.inkCtrlPoints.length - 1 ? 0 : controlIndex;
                     if (doubleTap || e.button === 2) {
                         if (!brokenIndices?.includes(equivIndex) && !brokenIndices?.includes(controlIndex)) {
                             if (brokenIndices) brokenIndices.push(controlIndex);
-                            else this.props.inkDoc.brokenInkIndices = new List<number>([controlIndex]);
+                            else this._props.inkDoc.brokenInkIndices = new List<number>([controlIndex]);
                         } else {
                             if (brokenIndices?.includes(equivIndex)) {
-                                if (!this.props.inkView.controlUndo) this.props.inkView.controlUndo = UndoManager.StartBatch('make smooth');
+                                if (!this._props.inkView.controlUndo) this._props.inkView.controlUndo = UndoManager.StartBatch('make smooth');
                                 this.docView && InkStrokeProperties.Instance.snapHandleTangent(this.docView, equivIndex, handleIndexA, handleIndexB);
                             }
                             if (equivIndex !== controlIndex && brokenIndices?.includes(controlIndex)) {
-                                if (!this.props.inkView.controlUndo) this.props.inkView.controlUndo = UndoManager.StartBatch('make smooth');
+                                if (!this._props.inkView.controlUndo) this._props.inkView.controlUndo = UndoManager.StartBatch('make smooth');
                                 this.docView && InkStrokeProperties.Instance.snapHandleTangent(this.docView, controlIndex, handleIndexA, handleIndexB);
                             }
                         }
-                        this.props.inkView.controlUndo?.end();
-                        this.props.inkView.controlUndo = undefined;
+                        this._props.inkView.controlUndo?.end();
+                        this._props.inkView.controlUndo = undefined;
                     }
                     this.changeCurrPoint(controlIndex);
                 }),
@@ -126,14 +132,14 @@ export class InkControlPtHandles extends React.Component<InkControlProps> {
 
     render() {
         // Accessing the current ink's data and extracting all control points.
-        const scrData = this.props.screenCtrlPoints;
+        const scrData = this._props.screenCtrlPoints;
         const sreenCtrlPoints: ControlPoint[] = [];
         for (let i = 0; i <= scrData.length - 4; i += 4) {
             sreenCtrlPoints.push({ ...scrData[i], I: i });
             sreenCtrlPoints.push({ ...scrData[i + 3], I: i + 3 });
         }
 
-        const inkData = this.props.inkCtrlPoints;
+        const inkData = this._props.inkCtrlPoints;
         const inkCtrlPts: ControlPoint[] = [];
         for (let i = 0; i <= inkData.length - 4; i += 4) {
             inkCtrlPts.push({ ...inkData[i], I: i });
@@ -141,23 +147,23 @@ export class InkControlPtHandles extends React.Component<InkControlProps> {
         }
 
         const closed = InkingStroke.IsClosed(inkData);
-        const nearestScreenPt = this.props.nearestScreenPt();
+        const nearestScreenPt = this._props.nearestScreenPt();
         const TagType = (broken?: boolean) => (broken ? 'rect' : 'circle');
         const hdl = (control: { X: number; Y: number; I: number }, scale: number, color: string) => {
-            const broken = Cast(this.props.inkDoc.brokenInkIndices, listSpec('number'))?.includes(control.I);
+            const broken = Cast(this._props.inkDoc.brokenInkIndices, listSpec('number'))?.includes(control.I);
             const Tag = TagType((control.I === 0 || control.I === inkData.length - 1) && !closed) as keyof JSX.IntrinsicElements;
             return (
                 <Tag
                     key={control.I.toString() + scale}
-                    x={control.X - this.props.screenSpaceLineWidth * 2 * scale}
-                    y={control.Y - this.props.screenSpaceLineWidth * 2 * scale}
+                    x={control.X - this._props.screenSpaceLineWidth * 2 * scale}
+                    y={control.Y - this._props.screenSpaceLineWidth * 2 * scale}
                     cx={control.X}
                     cy={control.Y}
-                    r={this.props.screenSpaceLineWidth * 2 * scale}
-                    opacity={this.props.inkView.controlUndo ? 0.35 : 1}
-                    height={this.props.screenSpaceLineWidth * 4 * scale}
-                    width={this.props.screenSpaceLineWidth * 4 * scale}
-                    strokeWidth={this.props.screenSpaceLineWidth / 2}
+                    r={this._props.screenSpaceLineWidth * 2 * scale}
+                    opacity={this._props.inkView.controlUndo ? 0.35 : 1}
+                    height={this._props.screenSpaceLineWidth * 4 * scale}
+                    width={this._props.screenSpaceLineWidth * 4 * scale}
+                    strokeWidth={this._props.screenSpaceLineWidth / 2}
                     stroke={Colors.MEDIUM_BLUE}
                     fill={broken ? Colors.MEDIUM_BLUE : color}
                     onPointerDown={(e: React.PointerEvent) => this.onControlDown(e, control.I)}
@@ -170,7 +176,7 @@ export class InkControlPtHandles extends React.Component<InkControlProps> {
         };
         return (
             <svg>
-                {!nearestScreenPt ? null : <circle key={'npt'} cx={nearestScreenPt.X} cy={nearestScreenPt.Y} r={this.props.screenSpaceLineWidth * 2} fill={'#00007777'} stroke={'#00007777'} strokeWidth={0} pointerEvents="none" />}
+                {!nearestScreenPt ? null : <circle key={'npt'} cx={nearestScreenPt.X} cy={nearestScreenPt.Y} r={this._props.screenSpaceLineWidth * 2} fill={'#00007777'} stroke={'#00007777'} strokeWidth={0} pointerEvents="none" />}
                 {sreenCtrlPoints.map(control => hdl(control, this._overControl !== control.I ? 1 : 3 / 2, Colors.WHITE))}
             </svg>
         );
@@ -185,10 +191,15 @@ export interface InkEndProps {
     endPt: () => PointData;
 }
 @observer
-export class InkEndPtHandles extends React.Component<InkEndProps> {
+export class InkEndPtHandles extends ObservableReactComponent<InkEndProps> {
     @observable _overStart: boolean = false;
     @observable _overEnd: boolean = false;
 
+    constructor(props: InkEndProps) {
+        super(props);
+        makeObservable(this);
+    }
+
     _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 }) => {
@@ -198,7 +209,7 @@ export class InkEndPtHandles extends React.Component<InkEndProps> {
             e,
             action(e => {
                 if (this._throttle++ % 2 !== 0) return false;
-                if (!this.props.inkView.controlUndo) this.props.inkView.controlUndo = UndoManager.StartBatch('stretch ink');
+                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();
                 const p2 = pt2();
@@ -216,8 +227,8 @@ export class InkEndPtHandles extends React.Component<InkEndProps> {
             }),
             action(() => {
                 SnappingManager.SetIsDragging(false);
-                this.props.inkView.controlUndo?.end();
-                this.props.inkView.controlUndo = undefined;
+                this._props.inkView.controlUndo?.end();
+                this._props.inkView.controlUndo = undefined;
                 UndoManager.FilterBatches(['stroke', 'x', 'y', 'width', 'height']);
             }),
             returnFalse
@@ -230,7 +241,7 @@ export class InkEndPtHandles extends React.Component<InkEndProps> {
                 key={key}
                 cx={pt.X}
                 cy={pt.Y}
-                r={this.props.screenSpaceLineWidth * 2}
+                r={this._props.screenSpaceLineWidth * 2}
                 fill={this._overStart ? '#aaaaaa' : '#99999977'}
                 stroke={'#00007777'}
                 strokeWidth={0}
@@ -242,8 +253,8 @@ export class InkEndPtHandles extends React.Component<InkEndProps> {
         );
         return (
             <svg>
-                {hdl('start', this.props.startPt(), e => this.dragRotate(e, this.props.startPt, this.props.endPt))}
-                {hdl('end', this.props.endPt(), e => this.dragRotate(e, this.props.endPt, this.props.startPt))}
+                {hdl('start', this._props.startPt(), e => this.dragRotate(e, this._props.startPt, this._props.endPt))}
+                {hdl('end', this._props.endPt(), e => this.dragRotate(e, this._props.endPt, this._props.startPt))}
             </svg>
         );
     }
diff --git a/src/client/views/PropertiesButtons.tsx b/src/client/views/PropertiesButtons.tsx
index 02f288a68..517a80d63 100644
--- a/src/client/views/PropertiesButtons.tsx
+++ b/src/client/views/PropertiesButtons.tsx
@@ -87,7 +87,7 @@ export class PropertiesButtons extends React.Component<{}, {}> {
                     containerDoc._isLightbox = !containerDoc._isLightbox;
                     //containerDoc._xPadding = containerDoc._yPadding = containerDoc._isLightbox ? 10 : undefined;
                     const containerContents = DocListCast(dv.dataDoc[Doc.LayoutFieldKey(containerDoc)]);
-                    //dv.Docuemnt.onClick = ScriptField.MakeScript('{self.data = undefined; documentView.select(false)}', { documentView: 'any' });
+                    //dv.Docuemnt.onClick = ScriptField.MakeScript('{this.data = undefined; documentView.select(false)}', { documentView: 'any' });
                     containerContents.forEach(doc => LinkManager.Links(doc).forEach(link => (link.link_displayLine = false)));
                 });
             }
@@ -219,7 +219,7 @@ export class PropertiesButtons extends React.Component<{}, {}> {
     //                 containerDoc._isLightbox = !containerDoc._isLightbox;
     //                 //containerDoc._xPadding = containerDoc._yPadding = containerDoc._isLightbox ? 10 : undefined;
     //                 const containerContents = DocListCast(dv.dataDoc[dv.props.fieldKey ?? Doc.LayoutFieldKey(containerDoc)]);
-    //                 //dv.Document.onClick = ScriptField.MakeScript('{self.data = undefined; documentView.select(false)}', { documentView: 'any' });
+    //                 //dv.Document.onClick = ScriptField.MakeScript('{this.data = undefined; documentView.select(false)}', { documentView: 'any' });
     //                 containerContents.forEach(doc => LinkManager.Links(doc).forEach(link => (link.layout_linkDisplay = false)));
     //             });
     //         }
diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx
index 59d99b371..9ffdc350d 100644
--- a/src/client/views/nodes/ComparisonBox.tsx
+++ b/src/client/views/nodes/ComparisonBox.tsx
@@ -69,8 +69,8 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
                 action((e, doubleTap) => {
                     if (doubleTap) {
                         this._isAnyChildContentActive = true;
-                        if (!this.dataDoc[this.fieldKey + '_1']) this.dataDoc[this.fieldKey + '_1'] = DocUtils.copyDragFactory(Doc.UserDoc().emptyNote as Doc);
-                        if (!this.dataDoc[this.fieldKey + '_2']) this.dataDoc[this.fieldKey + '_2'] = DocUtils.copyDragFactory(Doc.UserDoc().emptyNote as Doc);
+                        if (!this.dataDoc[this.fieldKey + '_1'] && !this.dataDoc[this.fieldKey]) this.dataDoc[this.fieldKey + '_1'] = DocUtils.copyDragFactory(Doc.UserDoc().emptyNote as Doc);
+                        if (!this.dataDoc[this.fieldKey + '_2'] && !this.dataDoc[this.fieldKey + '_alternate']) this.dataDoc[this.fieldKey + '_2'] = DocUtils.copyDragFactory(Doc.UserDoc().emptyNote as Doc);
                     }
                 }),
                 false,
@@ -131,8 +131,6 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
         return false;
     };
 
-    whenChildContentsActiveChanged = action((isActive: boolean) => (this._isAnyChildContentActive = isActive));
-
     closeDown = (e: React.PointerEvent, which: string) => {
         setupMoveUpEvents(
             this,
diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx
index 78e4435ce..31a2367fc 100644
--- a/src/client/views/nodes/KeyValueBox.tsx
+++ b/src/client/views/nodes/KeyValueBox.tsx
@@ -55,7 +55,7 @@ export class KeyValueBox extends ObservableReactComponent<FieldViewProps> {
     @observable _splitPercentage = 50;
 
     get fieldDocToLayout() {
-        return this._props.fieldKey ? DocCast(this._props.Document[this._props.fieldKey], DocCast(this._props.Document)) : this._props.Document;
+        return DocCast(this._props.Document);
     }
 
     @action
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index d25101844..f06e5fad0 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -21,7 +21,7 @@ import { List } from '../../../../fields/List';
 import { PrefetchProxy } from '../../../../fields/Proxy';
 import { RichTextField } from '../../../../fields/RichTextField';
 import { ComputedField } from '../../../../fields/ScriptField';
-import { BoolCast, Cast, DocCast, FieldValue, NumCast, RTFCast, ScriptCast, StrCast } from '../../../../fields/Types';
+import { BoolCast, Cast, DateCast, DocCast, FieldValue, NumCast, RTFCast, ScriptCast, StrCast } from '../../../../fields/Types';
 import { GetEffectiveAcl, TraceMobx } from '../../../../fields/util';
 import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, DivWidth, emptyFunction, numberRange, returnFalse, returnZero, setupMoveUpEvents, smoothScroll, unimplementedFunction, Utils } from '../../../../Utils';
 import { gptAPICall, GPTCallType } from '../../../apis/gpt/GPT';
@@ -349,6 +349,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
             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
             const protoData = Cast(Cast(dataDoc.proto, Doc, null)?.[this.fieldKey], RichTextField, null); // the default text inherited from a prototype
+            const layoutData = this.layoutDoc.isTemplateDoc ? Cast(this.layoutDoc[this.fieldKey], RichTextField, null) : undefined; // the default text inherited from a prototype
             const effectiveAcl = GetEffectiveAcl(dataDoc);
 
             const removeSelection = (json: string | undefined) => json?.replace(/"selection":.*/, '');
@@ -369,22 +370,20 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
                 if (this._applyingChange !== this.fieldKey && (force || textChange || removeSelection(newJson) !== removeSelection(prevData?.Data))) {
                     this._applyingChange = this.fieldKey;
                     textChange && (dataDoc[this.fieldKey + '_modificationDate'] = new DateField(new Date(Date.now())));
-                    if ((!prevData && !protoData) || newText || (!newText && !protoData)) {
+                    if ((!prevData && !protoData && !layoutData) || newText || (!newText && !protoData && !layoutData)) {
                         // 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 (force || ((this._finishingLink || this._props.isContentActive() || this._inDrop) && (textChange || removeSelection(newJson) !== removeSelection(prevData?.Data)))) {
                             const numstring = NumCast(dataDoc[this.fieldKey], null);
-                            dataDoc[this.fieldKey] = numstring !== undefined ? Number(newText) : newText || DocCast(dataDoc.proto)?.[this.fieldKey] === undefined ? new RichTextField(newJson, newText) : undefined;
-
+                            dataDoc[this.fieldKey] =
+                                numstring !== undefined ? Number(newText) : newText || (DocCast(dataDoc.proto)?.[this.fieldKey] === undefined && this.layoutDoc[this.fieldKey] === undefined) ? new RichTextField(newJson, newText) : undefined;
                             textChange && ScriptCast(this.layoutDoc.onTextChanged, null)?.script.run({ this: this.Document, text: newText });
                             this._applyingChange = ''; // turning this off here allows a Doc to retrieve data from template if noTemplate below is changed to false
-                            dataDoc[this.fieldKey + '_noTemplate'] = newText ? true : false; // mark the data field as being split from the template if it has been edited
                             unchanged = false;
                         }
                     } else {
                         // if we've deleted all the text in a note driven by a template, then restore the template data
                         dataDoc[this.fieldKey] = undefined;
-                        this._editorView.updateState(EditorState.fromJSON(this.config, JSON.parse((protoData || prevData).Data)));
-                        dataDoc[this.fieldKey + '_noTemplate'] = undefined; // mark the data field as not being split from any template it might have
+                        this._editorView.updateState(EditorState.fromJSON(this.config, JSON.parse((layoutData ?? protoData ?? prevData).Data)));
                         ScriptCast(this.layoutDoc.onTextChanged, null)?.script.run({ this: this.layoutDoc, text: newText });
                         unchanged = false;
                     }
@@ -958,6 +957,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
 
         const options = cm.findByDescription('Options...');
         const optionItems = options && 'subitems' in options ? options.subitems : [];
+        optionItems.push({ description: `Toggle auto update from template`, event: () => (this.dataDoc[this.fieldKey + '_autoUpdate'] = !this.dataDoc[this.fieldKey + '_autoUpdate']), icon: 'star' });
         optionItems.push({ description: `Generate Dall-E Image`, event: () => this.generateImage(), icon: 'star' });
         optionItems.push({ description: `Ask GPT-3`, event: () => this.askGPT(), icon: 'lightbulb' });
         this._props.renderDepth &&
@@ -1244,9 +1244,15 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
         );
         this._disposers.editorState = reaction(
             () => {
-                const dataDoc = Doc.IsDelegateField(DocCast(this.layoutDoc?.proto), this.fieldKey) ? DocCast(this.layoutDoc?.proto) : this?.dataDoc;
-                const whichDoc = !this.dataDoc || !this.layoutDoc ? undefined : dataDoc?.[this.fieldKey + '_noTemplate'] || !this.layoutDoc[this.fieldKey] ? dataDoc : this.layoutDoc;
-                return !whichDoc ? undefined : { data: Cast(whichDoc[this.fieldKey], RichTextField, null), str: Field.toString(DocCast(whichDoc[this.fieldKey]) ?? StrCast(whichDoc[this.fieldKey])) };
+                const protoData = DocCast(this.dataDoc.proto)?.[this.fieldKey];
+                const dataData = this.dataDoc[this.fieldKey];
+                const layoutData = Doc.AreProtosEqual(this.layoutDoc, this.dataDoc) ? undefined : this.layoutDoc[this.fieldKey];
+                const dataTime = dataData ? DateCast(this.dataDoc[this.fieldKey + '_modificationDate'])?.date.getTime() ?? 0 : 0;
+                const layoutTime = layoutData && this.dataDoc[this.fieldKey + '_autoUpdate'] ? DateCast(DocCast(this.layoutDoc)[this.fieldKey + '_modificationDate'])?.date.getTime() ?? 0 : 0;
+                const protoTime = protoData && this.dataDoc[this.fieldKey + '_autoUpdate'] ? DateCast(DocCast(this.dataDoc.proto)[this.fieldKey + '_modificationDate'])?.date.getTime() ?? 0 : 0;
+                const recentData = dataTime >= layoutTime ? (protoTime >= dataTime ? protoData : dataData) : layoutTime >= protoTime ? layoutData : protoData;
+                const whichData = recentData ?? (this.layoutDoc.isTemplateDoc ? layoutData : protoData) ?? protoData;
+                return !whichData ? undefined : { data: RTFCast(whichData), str: Field.toString(DocCast(whichData) ?? StrCast(whichData)) };
             },
             incomingValue => {
                 if (this._editorView && this._applyingChange !== this.fieldKey) {
@@ -1256,11 +1262,12 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
                             this._editorView.updateState(EditorState.fromJSON(this.config, updatedState));
                             this.tryUpdateScrollHeight();
                         }
-                    } else {
+                    } else if (this._editorView.state.doc.textContent !== incomingValue?.str) {
                         selectAll(this._editorView.state, tx => this._editorView?.dispatch(tx.insertText(incomingValue?.str ?? '')));
                     }
                 }
-            }
+            },
+            { fireImmediately: true }
         );
 
         this._disposers.search = reaction(
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index bdf600475..200896e25 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -48,7 +48,7 @@ export namespace Field {
         const valFunc = (field: Field): string => {
             const res =
                 field instanceof ComputedField && showComputedValue
-                    ? field._lastComputedResult
+                    ? field.value(doc)
                     : field instanceof ComputedField
                       ? `:=${field.script.originalScript.replace(/dashCallChat\(_setCacheResult_, this, `(.*)`\)/, '(($1))')}`
                       : field instanceof ScriptField
diff --git a/src/fields/ScriptField.ts b/src/fields/ScriptField.ts
index 9021c8896..8b51088b2 100644
--- a/src/fields/ScriptField.ts
+++ b/src/fields/ScriptField.ts
@@ -198,14 +198,22 @@ export class ComputedField extends ScriptField {
     }
 
     _lastComputedResult: FieldResult;
-    value = computedFn((doc: Doc) => this._valueOutsideReaction(doc));
-    _valueOutsideReaction = (doc: Doc) => {
-        this._lastComputedResult =
-            this._cachedResult ?? (this.script.compiled && this.script.run({ this: doc, self: doc, value: '', _setCacheResult_: this.setCacheResult, _last_: this._lastComputedResult, _readOnly_: true }, console.log).result);
-        return this._lastComputedResult;
-    };
+    value = (doc:Doc) => (this._lastComputedResult = this._cachedResult ?? 
+        computedFn((doc: Doc) =>
+            this.script.compiled &&
+            this.script.run( {
+                    this: doc, 
+                    //value: '',
+                    _setCacheResult_: this.setCacheResult,
+                    _last_: this._lastComputedResult,
+                    _readOnly_: true,
+                },
+                console.log
+            ).result
+        )(doc)
+    ); // prettier-ignore
 
-    [ToValue](doc: Doc)   { if (ComputedField.useComputed) return { value: this._valueOutsideReaction(doc) };  } // prettier-ignore
+    [ToValue](doc: Doc)   { if (ComputedField.useComputed) return { value: this.value(doc) };  } // prettier-ignore
     [Copy](): ObjectField { return new ComputedField(this.script, this.setterscript, this.rawscript); } // prettier-ignore
 
     public static MakeFunction(script: string, params: object = {}, capturedVariables?: { [name: string]: Doc | string | number | boolean }, setterscript?: string) {
-- 
cgit v1.2.3-70-g09d2

