aboutsummaryrefslogtreecommitdiff
path: root/src/client/views/nodes/formattedText
diff options
context:
space:
mode:
authorbobzel <zzzman@gmail.com>2025-03-10 16:13:04 -0400
committerbobzel <zzzman@gmail.com>2025-03-10 16:13:04 -0400
commitb7989dded8bb001876de6cbca59bf77935f0daf7 (patch)
tree0dba0665674db7bb84770833df0a4100d0520701 /src/client/views/nodes/formattedText
parent4979415d4604d280e81a162bf9a9d39c731d3738 (diff)
parent5bf944035c0ba94ad15245416f51ca0329a51bde (diff)
Merge branch 'master' into alyssa-starter
Diffstat (limited to 'src/client/views/nodes/formattedText')
-rw-r--r--src/client/views/nodes/formattedText/DailyJournal.tsx107
-rw-r--r--src/client/views/nodes/formattedText/DashFieldView.scss4
-rw-r--r--src/client/views/nodes/formattedText/DashFieldView.tsx24
-rw-r--r--src/client/views/nodes/formattedText/EquationEditor.tsx11
-rw-r--r--src/client/views/nodes/formattedText/EquationView.tsx8
-rw-r--r--src/client/views/nodes/formattedText/FormattedTextBox.scss20
-rw-r--r--src/client/views/nodes/formattedText/FormattedTextBox.tsx137
-rw-r--r--src/client/views/nodes/formattedText/RichTextMenu.scss4
-rw-r--r--src/client/views/nodes/formattedText/RichTextMenu.tsx52
-rw-r--r--src/client/views/nodes/formattedText/RichTextRules.ts2
10 files changed, 252 insertions, 117 deletions
diff --git a/src/client/views/nodes/formattedText/DailyJournal.tsx b/src/client/views/nodes/formattedText/DailyJournal.tsx
new file mode 100644
index 000000000..ec1f7a023
--- /dev/null
+++ b/src/client/views/nodes/formattedText/DailyJournal.tsx
@@ -0,0 +1,107 @@
+import { action, makeObservable, observable } from 'mobx';
+import * as React from 'react';
+import { RichTextField } from '../../../../fields/RichTextField';
+import { Docs } from '../../../documents/Documents';
+import { DocumentType } from '../../../documents/DocumentTypes';
+import { ViewBoxAnnotatableComponent } from '../../DocComponent';
+import { FieldView, FieldViewProps } from '../FieldView';
+import { FormattedTextBox, FormattedTextBoxProps } from './FormattedTextBox';
+
+export class DailyJournal extends ViewBoxAnnotatableComponent<FieldViewProps>() {
+ @observable journalDate: string;
+
+ public static LayoutString(fieldStr: string) {
+ return FieldView.LayoutString(DailyJournal, fieldStr);
+ }
+
+ constructor(props: FormattedTextBoxProps) {
+ super(props);
+ makeObservable(this);
+ this.journalDate = this.getFormattedDate();
+
+ console.log('Constructor: Setting initial title and text...');
+ this.setDailyTitle();
+ this.setDailyText();
+ }
+
+ getFormattedDate(): string {
+ const date = new Date().toLocaleDateString(undefined, {
+ weekday: 'long',
+ year: 'numeric',
+ month: 'long',
+ day: 'numeric',
+ });
+ console.log('getFormattedDate():', date);
+ return date;
+ }
+
+ @action
+ setDailyTitle() {
+ console.log('setDailyTitle() called...');
+ console.log('Current title before update:', this.dataDoc.title);
+
+ if (!this.dataDoc.title || this.dataDoc.title !== this.journalDate) {
+ console.log('Updating title to:', this.journalDate);
+ this.dataDoc.title = this.journalDate;
+ }
+
+ console.log('New title after update:', this.dataDoc.title);
+ }
+
+ @action
+ setDailyText() {
+ console.log('setDailyText() called...');
+ const placeholderText = 'Start writing here...';
+ const initialText = `Journal Entry - ${this.journalDate}\n${placeholderText}`;
+
+ console.log('Checking if dataDoc has text field...');
+
+ const styles = {
+ bold: true, // Make the journal date bold
+ color: 'blue', // Set the journal date color to blue
+ fontSize: 18, // Set the font size to 18px for the whole text
+ };
+
+ console.log('Setting new text field with:', initialText);
+ this.dataDoc[this.fieldKey] = RichTextField.textToRtf(
+ initialText,
+ undefined, // No image DocId
+ styles, // Pass the styles object here
+ placeholderText.length // The position for text selection
+ );
+
+ console.log('Current text field:', this.dataDoc[this.fieldKey]);
+ }
+
+ componentDidMount(): void {
+ console.log('componentDidMount() triggered...');
+ // bcz: This should be moved into Docs.Create.DailyJournalDocument()
+ // otherwise, it will override all the text whenever the note is reloaded
+ this.setDailyTitle();
+ this.setDailyText();
+ }
+
+ render() {
+ return (
+ <div style={{ background: 'beige', width: '100%', height: '100%' }}>
+ <FormattedTextBox {...this._props} fieldKey={'text'} Document={this.Document} TemplateDataDocument={undefined} />
+ </div>
+ );
+ }
+}
+
+Docs.Prototypes.TemplateMap.set(DocumentType.JOURNAL, {
+ layout: { view: DailyJournal, dataField: 'text' },
+ options: {
+ acl: '',
+ _height: 35,
+ _xMargin: 10,
+ _yMargin: 10,
+ _layout_autoHeight: true,
+ _layout_nativeDimEditable: true,
+ _layout_reflowVertical: true,
+ _layout_reflowHorizontal: true,
+ defaultDoubleClick: 'ignore',
+ systemIcon: 'BsFileEarmarkTextFill',
+ },
+});
diff --git a/src/client/views/nodes/formattedText/DashFieldView.scss b/src/client/views/nodes/formattedText/DashFieldView.scss
index d79df4272..78bbb520e 100644
--- a/src/client/views/nodes/formattedText/DashFieldView.scss
+++ b/src/client/views/nodes/formattedText/DashFieldView.scss
@@ -1,4 +1,4 @@
-@import '../../global/globalCssVariables.module.scss';
+@use '../../global/globalCssVariables.module.scss' as global;
.dashFieldView-active,
.dashFieldView {
@@ -64,5 +64,5 @@
}
.ProseMirror-selectedNode {
- outline: solid 1px $light-blue !important;
+ outline: solid 1px global.$light-blue !important;
}
diff --git a/src/client/views/nodes/formattedText/DashFieldView.tsx b/src/client/views/nodes/formattedText/DashFieldView.tsx
index f0313fba4..e899b49bc 100644
--- a/src/client/views/nodes/formattedText/DashFieldView.tsx
+++ b/src/client/views/nodes/formattedText/DashFieldView.tsx
@@ -5,7 +5,7 @@ import { observer } from 'mobx-react';
import { NodeSelection } from 'prosemirror-state';
import * as React from 'react';
import * as ReactDOM from 'react-dom/client';
-import { returnFalse, returnZero, setupMoveUpEvents } from '../../../../ClientUtils';
+import { returnFalse, returnTrue, returnZero, setupMoveUpEvents } from '../../../../ClientUtils';
import { Doc, DocListCast, Field } from '../../../../fields/Doc';
import { List } from '../../../../fields/List';
import { listSpec } from '../../../../fields/Schema';
@@ -25,6 +25,7 @@ import './DashFieldView.scss';
import { FormattedTextBox } from './FormattedTextBox';
import { Node } from 'prosemirror-model';
import { EditorView } from 'prosemirror-view';
+import { DocumentOptions, FInfo } from '../../../documents/Documents';
@observer
export class DashFieldViewMenu extends AntimodeMenu<AntimodeMenuProps> {
@@ -151,12 +152,14 @@ export class DashFieldViewInternal extends ObservableReactComponent<IDashFieldVi
selectedCells = () => (this._dashDoc ? [this._dashDoc] : undefined);
columnWidth = () => Math.min(this._props.tbox._props.PanelWidth(), Math.max(50, this._props.tbox._props.PanelWidth() - 100)); // try to leave room for the fieldKey
+ finfo = (fieldKey: string) => (new DocumentOptions() as Record<string, FInfo>)[fieldKey];
+
// set the display of the field's value (checkbox for booleans, span of text for strings)
@computed get fieldValueContent() {
return !this._dashDoc ? null : (
<div
- onClick={action(() => {
- this._expanded = !this._props.editable ? !this._expanded : true;
+ onPointerDown={action(() => {
+ this._expanded = !this._props.editable ? false : !this._expanded;
})}
style={{ fontSize: 'smaller', width: !this._hideKey && this._expanded ? this.columnWidth() : undefined }}>
<SchemaTableCell
@@ -165,16 +168,21 @@ export class DashFieldViewInternal extends ObservableReactComponent<IDashFieldVi
deselectCell={emptyFunction}
selectCell={emptyFunction}
maxWidth={this._props.hideKey || this._hideKey ? undefined : this._props.tbox._props.PanelWidth}
- columnWidth={this._expanded || this._props.nodeSelected() ? this.columnWidth : returnZero}
+ columnWidth={this._expanded || this._props.nodeSelected() ? () => undefined : returnZero}
selectedCells={this.selectedCells}
selectedCol={returnZero}
fieldKey={this._fieldKey}
+ highlightCells={emptyFunction} // fix
+ refSelectModeInfo={{ enabled: false, currEditing: undefined }} // fix
+ selectReference={emptyFunction} //
+ eqHighlightFunc={() => []} // fix
+ isolatedSelection={() => [true, true]} // fix
+ rowSelected={returnTrue} //fix
rowHeight={returnZero}
isRowActive={this.isRowActive}
padding={0}
- getFinfo={emptyFunction}
+ getFinfo={this.finfo}
setColumnValues={returnFalse}
- setSelectedColumnValues={returnFalse}
allowCRs
oneLine={!this._expanded && !this._props.nodeSelected()}
finishEdit={this.finishEdit}
@@ -191,7 +199,7 @@ export class DashFieldViewInternal extends ObservableReactComponent<IDashFieldVi
const container = this._props.tbox.DocumentView?.().containerViewPath?.().lastElement();
if (container) {
const embedding = Doc.MakeEmbedding(container.Document);
- embedding._type_collection = CollectionViewType.Time;
+ embedding._type_collection = CollectionViewType.Pivot;
const colHdrKey = '_' + container.LayoutFieldKey + '_columnHeaders';
let list = Cast(embedding[colHdrKey], listSpec(SchemaHeaderField));
if (!list) {
@@ -259,7 +267,7 @@ export class DashFieldViewInternal extends ObservableReactComponent<IDashFieldVi
className={`dashFieldView${this.isRowActive() ? '-active' : ''}`}
ref={this._fieldRef}
style={{
- width: this._props.width,
+ // width: this._props.width,
height: this._props.height,
pointerEvents: this._props.tbox._props.rootSelected?.() || this._props.tbox.isAnyChildContentActive?.() ? undefined : 'none',
}}>
diff --git a/src/client/views/nodes/formattedText/EquationEditor.tsx b/src/client/views/nodes/formattedText/EquationEditor.tsx
index 8bb4a0a26..48efa6e63 100644
--- a/src/client/views/nodes/formattedText/EquationEditor.tsx
+++ b/src/client/views/nodes/formattedText/EquationEditor.tsx
@@ -1,4 +1,3 @@
-/* eslint-disable react/require-default-props */
import React, { Component, createRef } from 'react';
// Import JQuery, required for the functioning of the equation editor
@@ -7,6 +6,7 @@ import './EquationEditor.scss';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(window as any).jQuery = $;
+// eslint-disable-next-line @typescript-eslint/no-require-imports
require('mathquill/build/mathquill');
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(window as any).MathQuill = (window as any).MathQuill.getInterface(1);
@@ -57,13 +57,8 @@ class EquationEditor extends Component<EquationEditorProps> {
const config = {
handlers: {
edit: () => {
- if (this.ignoreEditEvents > 0) {
- this.ignoreEditEvents -= 1;
- return;
- }
- if (this.mathField.latex() !== value) {
- onChange(this.mathField.latex());
- }
+ if (this.ignoreEditEvents <= 0) onChange(this.mathField.latex());
+ else this.ignoreEditEvents -= 1;
},
enter: onEnter,
},
diff --git a/src/client/views/nodes/formattedText/EquationView.tsx b/src/client/views/nodes/formattedText/EquationView.tsx
index df1421a33..e0450b202 100644
--- a/src/client/views/nodes/formattedText/EquationView.tsx
+++ b/src/client/views/nodes/formattedText/EquationView.tsx
@@ -110,13 +110,7 @@ export class EquationView {
}
selectNode() {
this.view.dispatch(this.view.state.tr.setSelection(new TextSelection(this.view.state.doc.resolve(this.getPos() ?? 0))));
- this.tbox._applyingChange = this.tbox.fieldKey; // setting focus will make prosemirror lose focus, which will cause it to change its selection to a text selection, which causes this view to get rebuilt but it's no longer node selected, so the equationview won't have focus
- setTimeout(() => {
- this._editor?.mathField.focus();
- setTimeout(() => {
- this.tbox._applyingChange = '';
- });
- });
+ setTimeout(() => this._editor?.mathField.focus());
}
deselectNode() {}
}
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.scss b/src/client/views/nodes/formattedText/FormattedTextBox.scss
index 84859b94d..f9de4ab5a 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.scss
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.scss
@@ -1,4 +1,4 @@
-@import '../../global/globalCssVariables.module.scss';
+@use '../../global/globalCssVariables.module.scss' as global;
.ProseMirror {
width: 100%;
@@ -22,7 +22,7 @@
&.h-left * {
display: flex;
- justify-content: flex-start;
+ justify-content: flex-start;
}
&.h-right * {
@@ -32,7 +32,7 @@
&.template * {
::-webkit-scrollbar-track {
- background: none;
+ background: none;
}
}
@@ -64,7 +64,7 @@ audiotag:hover {
background: inherit;
padding: 0;
border-width: 0px;
- border-color: $medium-gray;
+ border-color: global.$medium-gray;
box-sizing: border-box;
background-color: inherit;
border-style: solid;
@@ -79,7 +79,6 @@ audiotag:hover {
transform-origin: left top;
top: 0;
left: 0;
-
}
.formattedTextBox-cont {
@@ -88,7 +87,7 @@ audiotag:hover {
padding: 0;
border-width: 0px;
border-radius: inherit;
- border-color: $medium-gray;
+ border-color: global.$medium-gray;
box-sizing: border-box;
background-color: inherit;
border-style: solid;
@@ -147,13 +146,13 @@ audiotag:hover {
font-size: 11px;
border-radius: 3px;
color: white;
- background: $medium-gray;
+ background: global.$medium-gray;
border-radius: 5px;
display: flex;
justify-content: center;
align-items: center;
cursor: grabbing;
- box-shadow: $standard-box-shadow;
+ box-shadow: global.$standard-box-shadow;
// transition: 0.2s;
opacity: 0.3;
&:hover {
@@ -646,7 +645,7 @@ footnote::before {
}
@media only screen and (max-width: 1000px) {
- @import '../../global/globalCssVariables.module.scss';
+ // @import '../../global/globalCssVariables.module.scss';
.ProseMirror {
width: 100%;
@@ -664,7 +663,7 @@ footnote::before {
padding: 0;
border-width: 0px;
border-radius: inherit;
- border-color: $medium-gray;
+ border-color: global.$medium-gray;
box-sizing: border-box;
background-color: inherit;
border-style: solid;
@@ -1074,4 +1073,3 @@ footnote::before {
}
}
}
-
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 659cda9dd..7feaac6ae 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -17,7 +17,7 @@ import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, ClientUtils, Di
import { DateField } from '../../../../fields/DateField';
import { CreateLinkToActiveAudio, Doc, DocListCast, Field, FieldType, Opt, StrListCast } from '../../../../fields/Doc';
import { AclAdmin, AclAugment, AclEdit, AclSelfEdit, DocCss, DocData, ForceServerWrite, UpdatingFromServer } from '../../../../fields/DocSymbols';
-import { Id } from '../../../../fields/FieldSymbols';
+import { Id, ToString } from '../../../../fields/FieldSymbols';
import { InkTool } from '../../../../fields/InkField';
import { List } from '../../../../fields/List';
import { PrefetchProxy } from '../../../../fields/Proxy';
@@ -66,6 +66,7 @@ import { RichTextRules } from './RichTextRules';
import { schema } from './schema_rts';
import { Property } from 'csstype';
import { LabelBox } from '../LabelBox';
+import { StickerPalette } from '../../smartdraw/StickerPalette';
// import * as applyDevTools from 'prosemirror-dev-tools';
export interface FormattedTextBoxProps extends FieldViewProps {
@@ -98,7 +99,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
public static Init(nodeViews: (self: FormattedTextBox) => { [key: string]: NodeViewConstructor }) { FormattedTextBox._nodeViews = nodeViews; } // prettier-ignore
public static PasteOnLoad: ClipboardEvent | undefined;
- public static DontSelectInitialText = false; // whether initial text should be selected or not
public static SelectOnLoadChar = '';
public static LiveTextUndo: UndoManager.Batch | undefined; // undo batch when typing a new text note into a collection
@@ -130,15 +130,23 @@ 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
- @computed get fontSize() { return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.FontSize) as string; } // prettier-ignore
- @computed get fontFamily() { return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.FontFamily) as string; } // prettier-ignore
- @computed get fontWeight() { return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.FontWeight) as string; } // prettier-ignore
+ @computed get fontColor() { return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.FontColor) as string; } // prettier-ignore
+ @computed get fontSize() { return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.FontSize) as string; } // prettier-ignore
+ @computed get fontFamily() { return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.FontFamily) as string; } // prettier-ignore
+ @computed get fontWeight() { return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.FontWeight) as string; } // prettier-ignore
+ @computed get fontStyle() { return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.FontStyle) as string; } // prettier-ignore
+ @computed get fontDecoration() { return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.FontDecoration) as string; } // prettier-ignore
set _recordingDictation(value) {
!this.dataDoc[`${this.fieldKey}_recordingSource`] && (this.dataDoc.mediaState = value ? mediaState.Recording : undefined);
@@ -277,7 +285,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
e.stopPropagation();
const targetCreator = (annotationOn?: Doc) => {
const target = DocUtils.GetNewTextDoc('Note linked to ' + this.Document.title, 0, 0, 100, 100, annotationOn);
- Doc.SetSelectOnLoad(target);
+ DocumentView.SetSelectOnLoad(target);
return target;
};
@@ -319,6 +327,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
(node.attrs.hideValue ? '' : Field.toJavascriptString(refDoc[fieldKey] as FieldType))
);
}
+ if (node.type === this.EditorView?.state.schema.nodes.dashDoc) {
+ const refDoc = !node.attrs.docId ? DocCast(this.Document.rootDocument, this.Document) : (DocServer.GetCachedRefField(node.attrs.docId as string) as Doc);
+ return refDoc[ToString]();
+ }
return '';
};
dispatchTransaction = (tx: Transaction) => {
@@ -356,8 +368,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)))) {
@@ -367,7 +379,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) {
@@ -378,7 +390,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();
@@ -968,7 +980,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
},
icon: 'star',
});
- optionItems.push({ description: `Generate Dall-E Image`, event: () => this.generateImage(), icon: 'star' });
+ optionItems.push({ description: `Generate Dall-E Image`, event: this.generateImage, icon: 'star' });
// optionItems.push({ description: `Make AI Flashcards`, event: () => this.makeAIFlashcards(), icon: 'lightbulb' });
optionItems.push({ description: `Ask GPT-3`, event: this.askGPT, icon: 'lightbulb' });
this._props.renderDepth &&
@@ -987,6 +999,11 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
},
icon: this.Document._layout_autoHeight ? 'lock' : 'unlock',
});
+ optionItems.push({
+ description: this.Document.savedAsSticker ? 'Sticker Saved!' : 'Save to Stickers',
+ event: action(undoable(async () => await StickerPalette.addToPalette(this.Document), 'save to palette')),
+ icon: this.Document.savedAsSticker ? 'clipboard-check' : 'file-arrow-down',
+ });
!options && cm.addItem({ description: 'Options...', subitems: optionItems, icon: 'eye' });
const help = cm.findByDescription('Help...');
const helpItems = help?.subitems ?? [];
@@ -1004,7 +1021,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) => {
@@ -1030,7 +1046,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
askGPT = action(async () => {
try {
- GPTPopup.Instance.setSidebarId(this.sidebarKey);
+ GPTPopup.Instance.setSidebarFieldKey(this.sidebarKey);
GPTPopup.Instance.addDoc = this.sidebarAddDocument;
const res = await gptAPICall((this.dataDoc.text as RichTextField)?.Text, GPTCallType.COMPLETION);
if (!res) {
@@ -1048,12 +1064,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
}
});
- generateImage = async () => {
+ generateImage = () => {
GPTPopup.Instance?.setTextAnchor(this.getAnchor(false));
- GPTPopup.Instance?.setImgTargetDoc(this.Document);
- GPTPopup.Instance.addToCollection = this._props.addDocument;
- GPTPopup.Instance.setImgDesc((this.dataDoc.text as RichTextField)?.Text);
- GPTPopup.Instance.generateImage();
+ GPTPopup.Instance.generateImage((this.dataDoc.text as RichTextField)?.Text, this.Document, this._props.addDocument);
};
breakupDictation = () => {
@@ -1272,14 +1285,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
);
this._disposers.componentHeights = reaction(
// set the document height when one of the component heights changes and layout_autoHeight is on
- () => ({ sidebarHeight: this.sidebarHeight, textHeight: this.textHeight, layoutAutoHeight: this.layout_autoHeight, marginsHeight: this.layout_autoHeightMargins, tagsHeight: this.tagsHeight }),
- ({ sidebarHeight, textHeight, layoutAutoHeight, marginsHeight, tagsHeight }) => {
- const newHeight = this.contentScaling * (tagsHeight + marginsHeight + Math.max(sidebarHeight, textHeight));
+ () => ({ border: this._props.PanelHeight(), sidebarHeight: this.sidebarHeight, textHeight: this.textHeight, layoutAutoHeight: this.layout_autoHeight, marginsHeight: this.layout_autoHeightMargins }),
+ ({ 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()) && //
layoutAutoHeight &&
newHeight &&
- newHeight !== this.layoutDoc.height &&
+ (newHeight !== this.layoutDoc.height || border < NumCast(this.layoutDoc.height)) &&
!this._props.dontRegisterView
) {
this._props.setHeight?.(newHeight);
@@ -1307,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)) {
@@ -1397,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));
}
@@ -1533,44 +1545,27 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
this._editorView.TextView = this;
}
- const selectOnLoad = Doc.AreProtosEqual(this._props.TemplateDataDocument ?? this.Document, Doc.SelectOnLoad) && (!DocumentView.LightboxDoc() || DocumentView.LightboxContains(this.DocumentView?.()));
+ const selectOnLoad = Doc.AreProtosEqual(this._props.TemplateDataDocument ?? this.Document, DocumentView.SelectOnLoad) && (!DocumentView.LightboxDoc() || DocumentView.LightboxContains(this.DocumentView?.()));
const selLoadChar = FormattedTextBox.SelectOnLoadChar;
if (selectOnLoad) {
- Doc.SetSelectOnLoad(undefined);
+ DocumentView.SetSelectOnLoad(undefined);
FormattedTextBox.SelectOnLoadChar = '';
}
if (this.EditorView && selectOnLoad && !this._props.dontRegisterView && !this._props.dontSelectOnLoad && this.isActiveTab(this.ProseRef)) {
- this._props.select(false);
+ const $from = this.EditorView.state.selection.anchor ? this.EditorView.state.doc.resolve(this.EditorView.state.selection.anchor - 1) : undefined;
+ const mark = schema.marks.user_mark.create({ userid: ClientUtils.CurrentUserEmail(), modified: Math.floor(Date.now() / 1000) });
+ const curMarks = this.EditorView.state.storedMarks ?? $from?.marksAcross(this.EditorView.state.selection.$head) ?? [];
+ const storedMarks = [...curMarks.filter(m => m.type !== mark.type), mark];
+ let { tr } = this.EditorView.state;
if (selLoadChar) {
- const $from = this.EditorView.state.selection.anchor ? this.EditorView.state.doc.resolve(this.EditorView.state.selection.anchor - 1) : undefined;
- const mark = schema.marks.user_mark.create({ userid: ClientUtils.CurrentUserEmail(), modified: Math.floor(Date.now() / 1000) });
- const curMarks = this.EditorView.state.storedMarks ?? $from?.marksAcross(this.EditorView.state.selection.$head) ?? [];
- const storedMarks = [...curMarks.filter(m => m.type !== mark.type), mark];
const tr1 = this.EditorView.state.tr.setStoredMarks(storedMarks);
- const tr2 = selLoadChar === 'Enter' ? tr1.insert(this.EditorView.state.doc.content.size - 1, schema.nodes.paragraph.create()) : tr1.insertText(selLoadChar, this.EditorView.state.doc.content.size - 1);
- const tr = tr2.setStoredMarks(storedMarks);
-
- this.EditorView.dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(tr.doc.content.size))));
- this.tryUpdateDoc(true); // calling select() above will make isContentActive() true only after a render .. which means the selectAll() above won't write to the Document and the incomingValue will overwrite the selection with the non-updated data
- } else if (!FormattedTextBox.DontSelectInitialText) {
- const mark = schema.marks.user_mark.create({ userid: ClientUtils.CurrentUserEmail(), modified: Math.floor(Date.now() / 1000) });
- selectAll(this.EditorView.state, (tx: Transaction) => {
- this.EditorView?.dispatch(tx.addStoredMark(mark));
- });
- this.EditorView?.dispatch(this.EditorView.state.tr.setSelection(new TextSelection(this.EditorView.state.doc.resolve(1))));
- this.tryUpdateDoc(true); // calling select() above will make isContentActive() true only after a render .. which means the selectAll() above won't write to the Document and the incomingValue will overwrite the selection with the non-updated data
- } else {
- const $from = this.EditorView.state.selection.anchor ? this.EditorView.state.doc.resolve(this.EditorView.state.selection.anchor - 1) : undefined;
- const mark = schema.marks.user_mark.create({ userid: ClientUtils.CurrentUserEmail(), modified: Math.floor(Date.now() / 1000) });
- const curMarks = this.EditorView.state.storedMarks ?? $from?.marksAcross(this.EditorView.state.selection.$head) ?? [];
- const storedMarks = [...curMarks.filter(m => m.type !== mark.type), mark];
- const { tr } = this.EditorView.state;
- this.EditorView.dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(tr.doc.content.size))).setStoredMarks(storedMarks));
- this.tryUpdateDoc(true); // calling select() above will make isContentActive() true only after a render .. which means the selectAll() above won't write to the Document and the incomingValue will overwrite the selection with the non-updated data
+ tr = selLoadChar === 'Enter' ? tr1.insert(this.EditorView.state.doc.content.size - 1, schema.nodes.paragraph.create()) : tr1.insertText(selLoadChar, this.EditorView.state.doc.content.size - 1);
}
+ this.EditorView.dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(tr.doc.content.size - 1))).setStoredMarks(storedMarks));
+ this.tryUpdateDoc(true); // calling select() above will make isContentActive() true only after a render .. which means the selectAll() above won't write to the Document and the incomingValue will overwrite the selection with the non-updated data
+ console.log(this.EditorView.state);
}
if (selectOnLoad) {
- FormattedTextBox.DontSelectInitialText = false;
this.EditorView!.focus();
}
if (this._props.isContentActive()) this.prepareForTyping();
@@ -1648,7 +1643,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
}
};
onSelectEnd = () => {
- GPTPopup.Instance.setSidebarId(this.sidebarKey);
+ GPTPopup.Instance.setSidebarFieldKey(this.sidebarKey);
GPTPopup.Instance.addDoc = this.sidebarAddDocument;
document.removeEventListener('pointerup', this.onSelectEnd);
};
@@ -1660,7 +1655,19 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
for (let target: HTMLElement | Element | null = clickTarget as HTMLElement; target instanceof HTMLElement && !target.dataset?.targethrefs; target = target.parentElement);
while (clickTarget instanceof HTMLElement && !clickTarget.dataset?.targethrefs) clickTarget = clickTarget.parentElement;
const dataset = clickTarget instanceof HTMLElement ? clickTarget?.dataset : undefined;
- FormattedTextBoxComment.update(this, this.EditorView!, undefined, dataset?.targethrefs, dataset?.linkdoc, dataset?.nopreview === 'true');
+
+ if (dataset?.targethrefs && !dataset.targethrefs.startsWith('/doc'))
+ window
+ .open(
+ dataset?.targethrefs
+ ?.trim()
+ .split(' ')
+ .filter(h => h)
+ .lastElement(),
+ '_blank'
+ )
+ ?.focus();
+ else FormattedTextBoxComment.update(this, this.EditorView!, undefined, dataset?.targethrefs, dataset?.linkdoc, dataset?.nopreview === 'true');
}
};
@action
@@ -1686,6 +1693,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
setTimeout(() => this.EditorView?.dispatch(this.EditorView.state.tr.setSelection(TextSelection.near(this.EditorView.state.doc.resolve(pos)))), 100);
setTimeout(() => (this.ProseRef?.children?.[0] as HTMLElement).focus(), 200);
};
+
@action
onFocused = (e: React.FocusEvent): void => {
// applyDevTools.applyDevTools(this.EditorView);
@@ -1769,8 +1777,23 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
this._undoTyping = undefined;
}
+ /**
+ * 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
+ }
+ }
+ };
+
@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();
@@ -2119,6 +2142,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
fontSize: this.fontSize,
fontFamily: this.fontFamily,
fontWeight: this.fontWeight,
+ fontStyle: this.fontStyle,
+ textDecoration: this.fontDecoration,
...styleFromLayout,
}}>
<div
diff --git a/src/client/views/nodes/formattedText/RichTextMenu.scss b/src/client/views/nodes/formattedText/RichTextMenu.scss
index d6ed5ebee..fcc816447 100644
--- a/src/client/views/nodes/formattedText/RichTextMenu.scss
+++ b/src/client/views/nodes/formattedText/RichTextMenu.scss
@@ -1,4 +1,4 @@
-@import '../../global/globalCssVariables.module.scss';
+@use '../../global/globalCssVariables.module.scss' as global;
.button-dropdown-wrapper {
position: relative;
@@ -25,7 +25,7 @@
top: 35px;
left: 0;
background-color: #323232;
- color: $light-gray;
+ color: global.$light-gray;
border: 1px solid #4d4d4d;
border-radius: 0 6px 6px 6px;
box-shadow: 3px 3px 3px rgba(0, 0, 0, 0.25);
diff --git a/src/client/views/nodes/formattedText/RichTextMenu.tsx b/src/client/views/nodes/formattedText/RichTextMenu.tsx
index 65a415a23..758b4035e 100644
--- a/src/client/views/nodes/formattedText/RichTextMenu.tsx
+++ b/src/client/views/nodes/formattedText/RichTextMenu.tsx
@@ -41,7 +41,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
@observable private collapsed: boolean = false;
@observable private _noLinkActive: boolean = false;
@observable private _boldActive: boolean = false;
- @observable private _italicsActive: boolean = false;
+ @observable private _italicActive: boolean = false;
@observable private _underlineActive: boolean = false;
@observable private _strikethroughActive: boolean = false;
@observable private _subscriptActive: boolean = false;
@@ -89,8 +89,8 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
@computed get underline() {
return this._underlineActive;
}
- @computed get italics() {
- return this._italicsActive;
+ @computed get italic() {
+ return this._italicActive;
}
@computed get strikeThrough() {
return this._strikethroughActive;
@@ -147,8 +147,12 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
this._activeListType = this.getActiveListStyle();
this._activeAlignment = this.getActiveAlignment();
this._activeFitBox = BoolCast(refDoc[refField + 'fitBox'], BoolCast(Doc.UserDoc().fitBox));
- this._activeFontFamily = !activeFamilies.length ? StrCast(this.TextView?.Document._text_fontFamily, refVal('fontFamily', 'Arial')) : activeFamilies.length === 1 ? String(activeFamilies[0]) : 'various';
- this._activeFontSize = !activeSizes.length ? StrCast(this.TextView?.Document.fontSize, refVal('fontSize', '10px')) : activeSizes[0];
+ this._activeFontFamily = !activeFamilies.length
+ ? StrCast(this.TextView?.Document._text_fontFamily, StrCast(this.dataDoc?.[Doc.LayoutFieldKey(this.dataDoc) + '_fontFamily'], refVal('fontFamily', 'Arial')))
+ : activeFamilies.length === 1
+ ? String(activeFamilies[0])
+ : 'various';
+ this._activeFontSize = !activeSizes.length ? StrCast(this.TextView?.Document.fontSize, StrCast(this.dataDoc?.[Doc.LayoutFieldKey(this.dataDoc) + '_fontSize'], refVal('fontSize', '10px'))) : activeSizes[0];
this._activeFontColor = !activeColors.length ? StrCast(this.TextView?.Document.fontColor, refVal('fontColor', 'black')) : activeColors.length > 0 ? String(activeColors[0]) : '...';
this._activeHighlightColor = !activeHighlights.length ? '' : activeHighlights.length > 0 ? String(activeHighlights[0]) : '...';
@@ -176,6 +180,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
}
}
}
+ this.setActiveMarkButtons(this.getActiveMarksOnSelection());
};
// finds font sizes and families in selection
@@ -280,7 +285,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
this._noLinkActive = false;
this._boldActive = false;
- this._italicsActive = false;
+ this._italicActive = false;
this._underlineActive = false;
this._strikethroughActive = false;
this._subscriptActive = false;
@@ -290,7 +295,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
switch (mark.name) {
case 'noAutoLinkAnchor': this._noLinkActive = true; break;
case 'strong': this._boldActive = true; break;
- case 'em': this._italicsActive = true; break;
+ case 'em': this._italicActive = true; break;
case 'underline': this._underlineActive = true; break;
case 'strikethrough': this._strikethroughActive = true; break;
case 'subscript': this._subscriptActive = true; break;
@@ -352,7 +357,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
}
};
- toggleItalics = () => {
+ toggleItalic = () => {
if (this.view) {
const mark = this.view.state.schema.mark(this.view.state.schema.marks.em);
this.setMark(mark, this.view.state, this.view.dispatch, false);
@@ -361,23 +366,26 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
};
setFontField = (value: string, fontField: 'fitBox' | 'fontSize' | 'fontFamily' | 'fontColor' | 'fontHighlight') => {
- if (this.dataDoc) {
- this.dataDoc[`text_${fontField}`] = value;
- this.updateMenu(undefined, undefined, undefined, this.dataDoc);
- } else if (this.TextView && this.view) {
- const { text, paragraph } = this.view.state.schema.nodes;
- const selNode = this.view.state.selection.$anchor.node();
- if ((fontField === 'fontSize' && value === '0px') || (this.view.state.selection.from === 1 && this.view.state.selection.empty && [undefined, text, paragraph].includes(selNode?.type))) {
- this.TextView.dataDoc[this.TextView.fieldKey + `_${fontField}`] = value;
- this.view.focus();
+ if (this.TextView && this.view && fontField !== 'fitBox') {
+ if (this.view.hasFocus()) {
+ const attrs: { [key: string]: string } = {};
+ attrs[fontField] = value;
+ const fmark = this.view.state.schema.marks['pF' + fontField.substring(1)].create(attrs);
+ this.setMark(fmark, this.view.state, (tx: Transaction) => this.view?.dispatch(tx.addStoredMark(fmark)), true);
+ } else {
+ Array.from(new Set([...DocumentView.Selected(), this.TextView.DocumentView?.()]))
+ .filter(v => v?.ComponentView instanceof FormattedTextBox && v.ComponentView.EditorView?.TextView)
+ .map(v => v!.ComponentView as FormattedTextBox)
+ .forEach(view => {
+ view.EditorView!.TextView!.dataDoc[(view.EditorView!.TextView!.fieldKey ?? 'text') + `_${fontField}`] = value;
+ });
}
- const attrs: { [key: string]: string } = {};
- attrs[fontField] = value;
- const fmark = this.view?.state.schema.marks['pF' + fontField.substring(1)].create(attrs);
- this.setMark(fmark, this.view.state, (tx: Transaction) => this.view!.dispatch(tx.addStoredMark(fmark)), true);
- this.view.focus();
+ } else if (this.dataDoc) {
+ this.dataDoc[`${Doc.LayoutFieldKey(this.dataDoc)}_${fontField}`] = value;
+ this.updateMenu(undefined, undefined, undefined, this.dataDoc);
} else {
Doc.UserDoc()[fontField] = value;
+ this.updateMenu(undefined, undefined, undefined, this.dataDoc);
}
};
diff --git a/src/client/views/nodes/formattedText/RichTextRules.ts b/src/client/views/nodes/formattedText/RichTextRules.ts
index 3d14ce4c0..c332c592b 100644
--- a/src/client/views/nodes/formattedText/RichTextRules.ts
+++ b/src/client/views/nodes/formattedText/RichTextRules.ts
@@ -390,7 +390,7 @@ export class RichTextRules {
// %eq
new InputRule(/%eq/, (state, match, start, end) => {
const fieldKey = 'math' + Utils.GenerateGuid();
- this.TextBox.dataDoc[fieldKey] = 'y=';
+ this.TextBox.dataDoc[fieldKey] = '';
const tr = state.tr.setSelection(new TextSelection(state.tr.doc.resolve(end - 3), state.tr.doc.resolve(end))).replaceSelectionWith(schema.nodes.equation.create({ fieldKey }));
return tr.setSelection(new NodeSelection(tr.doc.resolve(tr.selection.$from.pos - 1)));
}),