From 1525fe600142d955fa24e939322f45cbca9d1cba Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Fri, 11 Apr 2025 12:53:15 -0400
Subject: fixed ViewGuid generation to never start with a number (enables text
 boxes to have css specific to a single Doc).  cleaned up '%' style rules for
 text boxes.  cleaned up custom style sheets in text boxes to only be created
 when needed and to improve highlighting bold text with context.

---
 .../views/nodes/formattedText/FormattedTextBox.tsx | 119 ++++++++++-----------
 1 file changed, 59 insertions(+), 60 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 fbe4fd294..cfb6dfa12 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -13,7 +13,7 @@ import { EditorState, NodeSelection, Plugin, Selection, TextSelection, Transacti
 import { EditorView, NodeViewConstructor } from 'prosemirror-view';
 import * as React from 'react';
 import { BsMarkdownFill } from 'react-icons/bs';
-import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, ClientUtils, DivWidth, returnFalse, returnZero, setupMoveUpEvents, simMouseEvent, smoothScroll, StopEvent } from '../../../../ClientUtils';
+import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, ClientUtils, DivWidth, removeStyleSheet, returnFalse, returnZero, setupMoveUpEvents, simMouseEvent, smoothScroll, StopEvent } from '../../../../ClientUtils';
 import { DateField } from '../../../../fields/DateField';
 import { CreateLinkToActiveAudio, Doc, DocListCast, Field, FieldType, Opt, StrListCast } from '../../../../fields/Doc';
 import { AclAdmin, AclAugment, AclEdit, AclSelfEdit, DocCss, ForceServerWrite, UpdatingFromServer } from '../../../../fields/DocSymbols';
@@ -101,12 +101,12 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
     public static LiveTextUndo: UndoManager.Batch | undefined; // undo batch when typing a new text note into a collection
 
     private static _nodeViews: (self: FormattedTextBox) => { [key: string]: NodeViewConstructor };
-    private static _globalHighlightsCache: string = '';
-    private static _globalHighlights = new ObservableSet<string>(['Audio Tags', 'Text from Others', 'Todo Items', 'Important Items', 'Disagree Items', 'Ignore Items']);
-    private static _highlightStyleSheet = addStyleSheet();
-    private static _bulletStyleSheet = addStyleSheet();
-    private static _userStyleSheet = addStyleSheet();
+    private _curHighlights = new ObservableSet<string>(['Audio Tags']);
+    private static _highlightStyleSheet = addStyleSheet().sheet;
+    private static _bulletStyleSheet = addStyleSheet().sheet;
+    private _userStyleSheetElement: HTMLStyleElement | undefined;
 
+    private _enteringStyle = false;
     private _oldWheel: HTMLDivElement | null = null;
     private _selectionHTML: string | undefined;
     private _sidebarRef = React.createRef<SidebarAnnos>();
@@ -696,45 +696,33 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
     }
 
     updateHighlights = (highlights: string[]) => {
-        if (Array.from(highlights).join('') === FormattedTextBox._globalHighlightsCache) return;
-        setTimeout(() => {
-            FormattedTextBox._globalHighlightsCache = Array.from(highlights).join('');
-        });
-        clearStyleSheetRules(FormattedTextBox._userStyleSheet);
-        if (!highlights.includes('Audio Tags')) {
-            addStyleSheetRule(FormattedTextBox._userStyleSheet, 'audiotag', { display: 'none' }, '');
-        }
-        if (highlights.includes('Text from Others')) {
-            addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UM-remote', { background: 'yellow' });
-        }
-        if (highlights.includes('My Text')) {
-            addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UM-' + ClientUtils.CurrentUserEmail().replace(/\./g, '').replace(/@/g, ''), { background: 'moccasin' });
-        }
-        if (highlights.includes('Todo Items')) {
-            addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UT-todo', { outline: 'black solid 1px' });
-        }
-        if (highlights.includes('Important Items')) {
-            addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UT-important', { 'font-size': 'larger' });
-        }
-        if (highlights.includes('Bold Text')) {
-            addStyleSheetRule(FormattedTextBox._userStyleSheet, '.formattedTextBox-inner .ProseMirror strong > span', { 'font-size': 'large' }, '');
-            addStyleSheetRule(FormattedTextBox._userStyleSheet, '.formattedTextBox-inner .ProseMirror :not(strong > span)', { 'font-size': '0px' }, '');
-        }
-        if (highlights.includes('Disagree Items')) {
-            addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UT-disagree', { 'text-decoration': 'line-through' });
-        }
-        if (highlights.includes('Ignore Items')) {
-            addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UT-ignore', { 'font-size': '1' });
-        }
+        const userStyleSheet = () => {
+            if (!this._userStyleSheetElement) {
+                this._userStyleSheetElement = addStyleSheet();
+            }
+            return this._userStyleSheetElement.sheet;
+        };
+        const viewId = this.DocumentView?.().ViewGuid ?? 1;
+        const userId = ClientUtils.CurrentUserEmail().replace(/\./g, '').replace('@', ''); // must match marks_rts -> user_mark's uid
+        highlights.filter(f => f !== 'Audio Tags').length && clearStyleSheetRules(userStyleSheet());
+        if (!highlights.includes('Audio Tags'))      addStyleSheetRule(userStyleSheet(), `#${viewId} .audiotag`,     { display:           'none' }, ''); // prettier-ignore
+        if (highlights.includes('Text from Others')) addStyleSheetRule(userStyleSheet(), `#${viewId} .UM-remote`,    { background:        'yellow' }, ''); // prettier-ignore
+        if (highlights.includes('My Text'))          addStyleSheetRule(userStyleSheet(), `#${viewId} .UM-${userId}`, { background:        'moccasin' }, ''); // prettier-ignore
+        if (highlights.includes('Todo Items'))       addStyleSheetRule(userStyleSheet(), `#${viewId} .UT-todo`,      { outline:           'black solid 1px' }, ''); // prettier-ignore
+        if (highlights.includes('Important Items'))  addStyleSheetRule(userStyleSheet(), `#${viewId} .UT-important`, { 'font-size':       'larger' }, ''); // prettier-ignore
+        if (highlights.includes('Disagree Items'))   addStyleSheetRule(userStyleSheet(), `#${viewId} .UT-disagree`,  { 'text-decoration': 'line-through' }, ''); // prettier-ignore
+        if (highlights.includes('Ignore Items'))     addStyleSheetRule(userStyleSheet(), `#${viewId} .UT-ignore`,    { 'font-size':       '1' }, ''); // prettier-ignore
+        if (highlights.includes('Bold Text')) {      addStyleSheetRule(userStyleSheet(), `#${viewId} .formattedTextBox-inner .ProseMirror p:not(:has(strong))`,         { 'font-size': '0px' }, '');
+                                                     addStyleSheetRule(userStyleSheet(), `#${viewId} .formattedTextBox-inner .ProseMirror p:not(:has(strong)) ::after`, { content: '...', 'font-size': '5px' }, '')} // prettier-ignore
         if (highlights.includes('By Recent Minute')) {
-            addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UM-' + ClientUtils.CurrentUserEmail().replace('.', '').replace('@', ''), { opacity: '0.1' });
+            addStyleSheetRule(userStyleSheet(), `#${viewId} .UM-${userId}`, { opacity: '0.1' }, '');
             const min = Math.round(Date.now() / 1000 / 60);
-            numberRange(10).map(i => addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UM-min-' + (min - i), { opacity: ((10 - i - 1) / 10).toString() }));
+            numberRange(10).map(i => addStyleSheetRule(userStyleSheet(), `#${viewId} .UM-min-` + (min - i), { opacity: ((10 - i - 1) / 10).toString() }, ''));
         }
         if (highlights.includes('By Recent Hour')) {
-            addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UM-' + ClientUtils.CurrentUserEmail().replace('.', '').replace('@', ''), { opacity: '0.1' });
+            addStyleSheetRule(userStyleSheet(), `#${viewId} .UM-${userId}`, { opacity: '0.1' }, '');
             const hr = Math.round(Date.now() / 1000 / 60 / 60);
-            numberRange(10).map(i => addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UM-hr-' + (hr - i), { opacity: ((10 - i - 1) / 10).toString() }));
+            numberRange(10).map(i => addStyleSheetRule(userStyleSheet(), `#${viewId} .UM-hr-` + (hr - i), { opacity: ((10 - i - 1) / 10).toString() }, ''));
         }
         this.layoutDoc[DocCss] = this.layoutDoc[DocCss] + 1; // css changes happen outside of react/mobx.  so we need to set a flag that will notify anyone interested in layout changes triggered by css changes (eg., CollectionLinkView)
     };
@@ -857,16 +845,16 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
         const expertHighlighting = [...noviceHighlighting, 'Important Items', 'Ignore Items', 'Disagree Items', 'By Recent Minute', 'By Recent Hour'];
         (Doc.noviceMode ? noviceHighlighting : expertHighlighting).forEach(option =>
             highlighting.push({
-                description: (!FormattedTextBox._globalHighlights.has(option) ? 'Highlight ' : 'Unhighlight ') + option,
+                description: (!this._curHighlights.has(option) ? 'Highlight ' : 'Unhighlight ') + option,
                 event: action(() => {
                     e.stopPropagation();
-                    if (!FormattedTextBox._globalHighlights.has(option)) {
-                        FormattedTextBox._globalHighlights.add(option);
+                    if (!this._curHighlights.has(option)) {
+                        this._curHighlights.add(option);
                     } else {
-                        FormattedTextBox._globalHighlights.delete(option);
+                        this._curHighlights.delete(option);
                     }
                 }),
-                icon: !FormattedTextBox._globalHighlights.has(option) ? 'highlighter' : 'remove-format',
+                icon: !this._curHighlights.has(option) ? 'highlighter' : 'remove-format',
             })
         );
         const appearance = cm.findByDescription('Appearance...');
@@ -1191,15 +1179,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
             () => ({ autoHeight: this.layout_autoHeight, fontSize: this.fontSize, css: this.Document[DocCss], xMargin: this.Document.xMargin, yMargin: this.Document.yMargin }),
             autoHeight => setTimeout(() => autoHeight && this.tryUpdateScrollHeight())
         );
-        this._disposers.highlights = reaction(
-            () => Array.from(FormattedTextBox._globalHighlights).slice(),
-            highlights => this.updateHighlights(highlights),
-            { fireImmediately: true }
-        );
-        this._disposers.width = reaction(
-            () => this._props.PanelWidth(),
-            () => this.tryUpdateScrollHeight()
-        );
+        this._disposers.highlights = reaction(() => Array.from(this._curHighlights).slice(), this.updateHighlights, { fireImmediately: true });
+        this._disposers.width = reaction(this._props.PanelWidth, this.tryUpdateScrollHeight);
         this._disposers.scrollHeight = reaction(
             () => ({ scrollHeight: this.scrollHeight, layoutAutoHeight: this.layout_autoHeight, width: NumCast(this.layoutDoc._width) }),
             ({ width, scrollHeight, layoutAutoHeight }) => width && layoutAutoHeight && this.resetNativeHeight(scrollHeight),
@@ -1211,7 +1192,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
             ({ border, sidebarHeight, textHeight, layoutAutoHeight, marginsHeight }) => {
                 const newHeight = this.contentScaling * (marginsHeight + Math.max(sidebarHeight, textHeight));
                 if (
-                    (!Array.from(FormattedTextBox._globalHighlights).includes('Bold Text') || this._props.isSelected()) && //
+                    (!Array.from(this._curHighlights).includes('Bold Text') || this._props.isSelected()) && //
                     layoutAutoHeight &&
                     newHeight &&
                     (newHeight !== this.layoutDoc.height || border < NumCast(this.layoutDoc.height)) &&
@@ -1220,7 +1201,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
                     this._props.setHeight?.(newHeight);
                 }
             },
-            { fireImmediately: !Array.from(FormattedTextBox._globalHighlights).includes('Bold Text') }
+            { fireImmediately: !Array.from(this._curHighlights).includes('Bold Text') }
         );
         this._disposers.links = reaction(
             () => Doc.Links(this.dataDoc), // if a link is deleted, then remove all hyperlinks that reference it from the text's marks
@@ -1275,7 +1256,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
                     });
                 }
                 this.prepareForTyping();
-                if (FormattedTextBox._globalHighlights.has('Bold Text')) {
+                if (this._curHighlights.has('Bold Text')) {
                     this.layoutDoc[DocCss] = this.layoutDoc[DocCss] + 1; // css change happens outside of mobx/react, so this will notify anyone interested in the layout that it has changed
                 }
                 if (((RichTextMenu.Instance?.view === this.EditorView && this.EditorView) || this.isLabel) && !selected) {
@@ -1520,6 +1501,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
         if (this.recordingDictation) {
             this.recordingDictation = !this.recordingDictation;
         }
+        removeStyleSheet(this._userStyleSheetElement);
         Object.values(this._disposers).forEach(disposer => disposer?.());
         this.endUndoTypingBatch();
         FormattedTextBox.LiveTextUndo?.end();
@@ -1766,13 +1748,30 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
         }
         const { state } = _editorView;
         if (!state.selection.empty && e.key === '%') {
-            this._rules!.EnteringStyle = true;
+            this._enteringStyle = true;
             StopEvent(e);
             return;
         }
+        if (this._enteringStyle && 'tix!'.includes(e.key)) {
+            const tag = e.key === 't' ? 'todo' : e.key === 'i' ? 'ignore' : e.key === 'x' ? 'disagree' : e.key === '!' ? 'important' : '??';
+            const node = state.selection.$from.nodeAfter;
+            const start = state.selection.from;
+            const end = state.selection.to;
+
+            if (node) {
+                StopEvent(e);
+                _editorView.dispatch(
+                    state.tr
+                        .removeMark(start, end, schema.marks.user_mark)
+                        .addMark(start, end, schema.marks.user_mark.create({ userid: ClientUtils.CurrentUserEmail(), modified: Math.floor(Date.now() / 1000) }))
+                        .addMark(start, end, schema.marks.user_tag.create({ userid: ClientUtils.CurrentUserEmail(), tag, modified: Math.round(Date.now() / 1000 / 60) }))
+                );
+                return;
+            }
+        }
 
-        if (state.selection.empty || !this._rules!.EnteringStyle) {
-            this._rules!.EnteringStyle = false;
+        if (state.selection.empty || !this._enteringStyle) {
+            this._enteringStyle = false;
         }
         for (let i = state.selection.from; i <= state.selection.to; i++) {
             const node = state.doc.resolve(i);
-- 
cgit v1.2.3-70-g09d2

