aboutsummaryrefslogtreecommitdiff
path: root/src/client/views/nodes/formattedText/FormattedTextBox.tsx
diff options
context:
space:
mode:
authorbobzel <zzzman@gmail.com>2025-04-11 12:53:15 -0400
committerbobzel <zzzman@gmail.com>2025-04-11 12:53:15 -0400
commit1525fe600142d955fa24e939322f45cbca9d1cba (patch)
treea8f35552f017ab3056a8ca2f9564a32e6d8afe39 /src/client/views/nodes/formattedText/FormattedTextBox.tsx
parentfea8bcb6264946b29775394c554d47577bb5995b (diff)
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.
Diffstat (limited to 'src/client/views/nodes/formattedText/FormattedTextBox.tsx')
-rw-r--r--src/client/views/nodes/formattedText/FormattedTextBox.tsx119
1 files changed, 59 insertions, 60 deletions
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);