diff options
Diffstat (limited to 'src/client/views/nodes/formattedText')
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))); }), |
