From 5d738b4fe832ef1c554448440ede237c32632d10 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Wed, 12 Feb 2025 12:54:53 -0500
Subject: cleaning up how text boxes maintain focus when UI widgets are
 interacted with.

---
 .../views/nodes/formattedText/FormattedTextBox.tsx | 47 +++++++++++++---------
 1 file changed, 27 insertions(+), 20 deletions(-)

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

diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index a6870d65b..eb1f9d07b 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -131,9 +131,15 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
     private _forceUncollapse = true; // if the cursor doesn't move between clicks, then the selection will disappear for some reason.  This flags the 2nd click as happening on a selection which allows bullet points to toggle
     private _break = true;
 
-    public _applyingChange: string = '';
     public ProseRef?: HTMLDivElement;
 
+    /**
+     * ApplyingChange - Marks whether an interactive text edit is currently in the process of being written to the database.
+     * This is needed to distinguish changes to text fields caused by editing vs those caused by changes to 
+     * the prototype or other external edits
+     */
+    public ApplyingChange: string = '';
+
     @observable _showSidebar = false;
 
     @computed get fontColor()      { return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.FontColor) as string; } // prettier-ignore
@@ -359,8 +365,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
                 let unchanged = true;
                 const textChange = newText !== prevData?.Text; // the Text string can change even if the RichText doesn't because dashFieldViews may return new strings as the data they reference changes
                 const rtField = (layoutData !== prevData ? layoutData : undefined) ?? protoData;
-                if (this._applyingChange !== this.fieldKey && (force || textChange || removeSelection(newJson) !== removeSelection(prevData?.Data))) {
-                    this._applyingChange = this.fieldKey;
+                if (this.ApplyingChange !== this.fieldKey && (force || textChange || removeSelection(newJson) !== removeSelection(prevData?.Data))) {
+                    this.ApplyingChange = this.fieldKey;
                     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)))) {
@@ -370,7 +376,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
                             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
+                            this.ApplyingChange = ''; // turning this off here allows a Doc to retrieve data from template if noTemplate below is changed to false
                             unchanged = false;
                         }
                     } else if (rtField) {
@@ -381,7 +387,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
                         ScriptCast(this.layoutDoc.onTextChanged, null)?.script.run({ this: this.layoutDoc, text: newText });
                         unchanged = false;
                     }
-                    this._applyingChange = '';
+                    this.ApplyingChange = '';
                     if (!unchanged) {
                         this.updateTitle();
                         this.tryUpdateScrollHeight();
@@ -1012,7 +1018,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
                 if (i.className !== 'ProseMirror-separator') this.getImageDesc(i.src);
             }
         }
-        // console.log('HI' + this.ProseRef?.getElementsByTagName('img'));
     };
 
     getImageDesc = async (u: string) => {
@@ -1315,7 +1320,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
                 return !whichData ? undefined : { data: RTFCast(whichData), str: Field.toString(DocCast(whichData) ?? StrCast(whichData)) };
             },
             incomingValue => {
-                if (this.EditorView && this._applyingChange !== this.fieldKey) {
+                if (this.EditorView && this.ApplyingChange !== this.fieldKey) {
                     if (incomingValue?.data) {
                         const updatedState = JSON.parse(incomingValue.data.Data);
                         if (JSON.stringify(this.EditorView.state.toJSON()) !== JSON.stringify(updatedState)) {
@@ -1405,7 +1410,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
             // } catch (err) {
             //     console.log('Drop failed', err);
             // }
-            // console.log('LKSDFLJ');
             this.addDocument?.(DocCast(this.Document.image));
         }
 
@@ -1695,10 +1699,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
         setTimeout(() => (this.ProseRef?.children?.[0] as HTMLElement).focus(), 200);
     };
 
-    IsFocused = false;
     @action
     onFocused = (e: React.FocusEvent): void => {
-        this.IsFocused = true;
         // applyDevTools.applyDevTools(this.EditorView);
         e.stopPropagation();
     };
@@ -1780,18 +1782,23 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
         this._undoTyping = undefined;
     }
 
-    @action
-    onBlur = (e: React.FocusEvent) => {
-        let ele: HTMLElement | null = e.relatedTarget instanceof HTMLElement ? e.relatedTarget : null;
-        if (ele?.tabIndex === -1) {
-            for (; ele; ele = ele?.parentElement) {
-                if (ele?.className === 'fonticonbox') {
-                    setTimeout(() => this._ref.current?.focus());
-                    break;
-                }
+    /**
+     * When a text box loses focus, it might be because a text button was clicked (eg, bold, italics) or color picker.
+     * In these cases, force focus back onto the text box.
+     * @param target
+     */
+    tryKeepingFocus = (target: Element | null) => {
+        for (let newFocusEle = target instanceof HTMLElement ? target : null; newFocusEle; newFocusEle = newFocusEle?.parentElement) {
+            // test if parent of new focused element is a UI button (should be more specific than testing className)
+            if (newFocusEle?.className === 'fonticonbox' || newFocusEle?.className === 'popup-container') {
+                return this.EditorView?.focus(); // keep focus on text box
             }
         }
-        if (ele?.className !== 'fonticonbox') this.IsFocused = false;
+    };
+
+    @action
+    onBlur = (e: React.FocusEvent) => {
+        this.tryKeepingFocus(e.relatedTarget);
         if (this.ProseRef?.children[0] !== e.nativeEvent.target) return;
         if (!(this.EditorView?.state.selection instanceof NodeSelection) || this.EditorView.state.selection.node.type !== this.EditorView.state.schema.nodes.footnote) {
             const stordMarks = this.EditorView?.state.storedMarks?.slice();
-- 
cgit v1.2.3-70-g09d2

