diff options
author | bobzel <zzzman@gmail.com> | 2025-03-10 16:13:04 -0400 |
---|---|---|
committer | bobzel <zzzman@gmail.com> | 2025-03-10 16:13:04 -0400 |
commit | b7989dded8bb001876de6cbca59bf77935f0daf7 (patch) | |
tree | 0dba0665674db7bb84770833df0a4100d0520701 /src/client/views/nodes/formattedText/FormattedTextBox.tsx | |
parent | 4979415d4604d280e81a162bf9a9d39c731d3738 (diff) | |
parent | 5bf944035c0ba94ad15245416f51ca0329a51bde (diff) |
Merge branch 'master' into alyssa-starter
Diffstat (limited to 'src/client/views/nodes/formattedText/FormattedTextBox.tsx')
-rw-r--r-- | src/client/views/nodes/formattedText/FormattedTextBox.tsx | 137 |
1 files changed, 81 insertions, 56 deletions
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 |