diff options
author | bobzel <zzzman@gmail.com> | 2024-10-28 20:07:51 -0400 |
---|---|---|
committer | bobzel <zzzman@gmail.com> | 2024-10-28 20:07:51 -0400 |
commit | 63f9f9573dab9745d2f570e1917b325a474fdd93 (patch) | |
tree | 51083d4087aa2b86f8e8d18523e82fb41ce51a3f /src | |
parent | 050fe4fe0d9f9722a7a2a48ae3b446049f93775f (diff) |
reworked LabelBox's to be fully editable. gor rid of singleLine for label boxes. made a text_fitBox field to render text with a labelBox. fixed styleprovider in freeformview clusters to be untracked() to avoid invalidations. added a default text color to Settings panel. fixed clicking text buttons to not lose focus in labelBox.
Diffstat (limited to 'src')
27 files changed, 421 insertions, 287 deletions
diff --git a/src/client/documents/DocUtils.ts b/src/client/documents/DocUtils.ts index 19f3c89ef..e3cb5e72c 100644 --- a/src/client/documents/DocUtils.ts +++ b/src/client/documents/DocUtils.ts @@ -679,10 +679,13 @@ export namespace DocUtils { ? {} // if the new doc will inherit from a template, don't set any layout fields since that would block the inheritance : { _width: width || 200, - _height: 35, + _height: BoolCast(Doc.UserDoc().fitBox) ? 70 : 35, _layout_centered: BoolCast(Doc.UserDoc()._layout_centered), _layout_fitWidth: true, _layout_autoHeight: true, + text_fitBox: BoolCast(Doc.UserDoc().fitBox), + text_align: StrCast(Doc.UserDoc().textAlign), + text_fontColor: StrCast(Doc.UserDoc().fontColor), }), }); diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index 98e8bc932..b0e1a7545 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -294,7 +294,6 @@ export class DocumentOptions { _yMargin?: NUMt = new NumInfo('gap between top edge of dcoument and start of masonry/stacking layouts', false); _xPadding?: NUMt = new NumInfo('x padding', false); _yPadding?: NUMt = new NumInfo('y padding', false); - _singleLine?: boolean; // whether label box is restricted to one line of text _createDocOnCR?: boolean; // whether carriage returns and tabs create new text documents _columnWidth?: NUMt = new NumInfo('width of table column', false); _columnsHideIfEmpty?: BOOLt = new BoolInfo('whether stacking view column headings should be hidden'); @@ -302,10 +301,14 @@ export class DocumentOptions { _caption_yMargin?: NUMt = new NumInfo('y margin of caption inside of a carousel collection', false, true); icon_nativeWidth?: NUMt = new NumInfo('native width of icon view', false, true); icon_nativeHeight?: NUMt = new NumInfo('native height of icon view', false, true); - _text_fontSize?: string; - _text_fontFamily?: string; - _text_fontWeight?: string; - text_align?: STRt = new StrInfo('horizontal text alignment default'); + text_fontSize?: string; + text_fontFamily?: string; + text_fontWeight?: string; + text_fitBox?: BOOLt = new BoolInfo("whether text box should be scaled to fit it's containing render box"); + text_align?: STRt = new StrInfo('horizontal text alignment default', undefined, undefined, ['left', 'center', 'right']); + title_align?: STRt = new StrInfo('horizontal title alignment in label box', undefined, undefined, ['left', 'center', 'right']); + title_transform?: STRt = new StrInfo('transformation to apply to title in label box (eg., uppercase)', undefined, undefined, ['uppercase', 'lowercase', 'capitalize']); + text_transform?: STRt = new StrInfo('transformation to apply to text in text box (eg., uppercase)', undefined, undefined, ['uppercase', 'lowercase', 'capitalize']); text_placeholder?: BOOLt = new BoolInfo('makes the text act like a placeholder and automatically select when the text box is selected'); fontSize?: string; _pivotField?: string; // field key used to determine headings for sections in stacking, masonry, pivot views @@ -496,7 +499,6 @@ export class DocumentOptions { sidebar_type_collection?: string; // collection type of text sidebar data_dashboards?: List<FieldType>; // list of dashboards used in shareddocs; - textTransform?: string; letterSpacing?: string; iconTemplate?: string; // name of icon template style icon_fieldKey?: string; // specifies the icon template to use (e.g., icon_fieldKey='george', then the icon template's name is icon_george; otherwise, the template's name would be icon_<type> where type is the Doc's type(pdf,rich text, etc)) diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index 30c75c659..b3aa5e145 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -186,7 +186,7 @@ export class CurrentUserUtils { const templateIconsDoc = DocUtils.AssignOpts(DocCast(doc[field]), reqdOpts) ?? (doc[field] = Docs.Create.TreeDocument([], reqdOpts)); const labelBox = (opts: DocumentOptions, fieldKey:string) => Docs.Create.LabelDocument({ - layout: LabelBox.LayoutString(fieldKey), textTransform: "unset", letterSpacing: "unset", _singleLine: false, _label_minFontSize: 14, _label_maxFontSize: 14, layout_borderRounding: "5px", _width: 150, _height: 70, _xPadding: 10, _yPadding: 10, ...opts + layout: LabelBox.LayoutString(fieldKey), letterSpacing: "unset", _label_minFontSize: 14, _label_maxFontSize: 14, layout_borderRounding: "5px", _width: 150, _height: 70, _xPadding: 10, _yPadding: 10, ...opts }); const imageBox = (opts: DocumentOptions, fieldKey:string) => Docs.Create.ImageDocument( "http://www.cs.brown.edu/~bcz/noImage.png", { layout:ImageBox.LayoutString(fieldKey), "icon_nativeWidth": 360 / 4, "icon_nativeHeight": 270 / 4, iconTemplate:DocumentType.IMG, _width: 360 / 4, _height: 270 / 4, _layout_showTitle: "title", ...opts }); const fontBox = (opts:DocumentOptions, fieldKey:string) => Docs.Create.FontIconDocument({ layout:FontIconBox.LayoutString(fieldKey), _nativeHeight: 30, _nativeWidth: 30, _width: 30, _height: 30, ...opts }); @@ -268,7 +268,7 @@ export class CurrentUserUtils { MakeTemplate(Docs.Create.MultirowDocument( [ Docs.Create.MulticolumnDocument([], { title: "hero", _height: 200, isSystem: true }), - Docs.Create.TextDocument("", { title: "text", _layout_fitWidth:true, _height: 100, isSystem: true, _text_fontFamily: StrCast(Doc.UserDoc().fontFamily), _text_fontSize: StrCast(Doc.UserDoc().fontSize) }) + Docs.Create.TextDocument("", { title: "text", _layout_fitWidth:true, _height: 100, isSystem: true, text_fontFamily: StrCast(Doc.UserDoc().fontFamily), text_fontSize: StrCast(Doc.UserDoc().fontSize) }) ], {...opts, title: "Slide View Template"})); const plotlyApi = () => { let plotly = Doc.MyPublishedDocs.find(fdoc => fdoc.title === "@plotly"); @@ -392,7 +392,7 @@ pie title Minerals in my tap water {key: "Trail", creator: Docs.Create.PresDocument, opts: { _width: 400, _height: 30, _type_collection: CollectionViewType.Stacking, _layout_dontCenter:'xy', dropAction: dropActionType.embed, treeView_HideTitle: true, _layout_fitWidth:true, layout_boxShadow: "0 0" }}, {key: "Tab", creator: opts => Docs.Create.FreeformDocument([], opts), opts: { _width: 500, _height: 800, _layout_fitWidth: true, _freeform_backgroundGrid: true, }}, {key: "Slide", creator: opts => Docs.Create.TreeDocument([], opts), opts: { _width: 300, _height: 200, _type_collection: CollectionViewType.Tree, - treeView_HasOverlay: true, _text_fontSize: "20px", _layout_autoHeight: true, + treeView_HasOverlay: true, text_fontSize: "20px", _layout_autoHeight: true, dropAction:dropActionType.move, treeView_Type: TreeViewType.outline, backgroundColor: "white", _xMargin: 0, _yMargin: 0, _createDocOnCR: true }, funcs: {title: 'this.text?.Text'}}, @@ -731,7 +731,7 @@ pie title Minerals in my tap water return [ { title: "Font", toolTip: "Font", width: 100, btnType: ButtonType.DropdownList, toolType:"font", ignoreClick: true, scripts: {script: '{ return setFontAttr(this.toolType, value, _readOnly_);}'}, btnList: new List<string>(["Roboto", "Roboto Mono", "Nunito", "Times New Roman", "Arial", "Georgia", "Comic Sans MS", "Tahoma", "Impact", "Crimson Text"]) }, - { title: "Font Size",toolTip: "Font size (%size)", btnType: ButtonType.NumberDropdownButton, toolType:"fontSize", ignoreClick: true, scripts: {script: '{ return setFontAttr(this.toolType, value, _readOnly_);}'}, numBtnMax: 200, numBtnMin: 6 }, + { title: "Font Size",toolTip: "Font size (%size)", btnType: ButtonType.NumberDropdownButton, toolType:"fontSize", ignoreClick: true, scripts: {script: '{ return setFontAttr(this.toolType, value, _readOnly_);}'}, numBtnMax: 200, numBtnMin: 9 }, { title: "Color", toolTip: "Font color (%color)", btnType: ButtonType.ColorButton, icon: "font", toolType:"fontColor",ignoreClick: true, scripts: {script: '{ return setFontAttr(this.toolType, value, _readOnly_);}'} }, { title: "Highlight",toolTip: "Font highlight", btnType: ButtonType.ColorButton, icon: "highlighter", toolType:"highlight",ignoreClick: true, scripts: {script: '{ return setFontAttr(this.toolType, value, _readOnly_);}'} }, { title: "Bold", toolTip: "Bold (Ctrl+B)", btnType: ButtonType.ToggleButton, icon: "bold", toolType:"bold", ignoreClick: true, scripts: {onClick: '{ return toggleCharStyle(this.toolType, _readOnly_);}'} }, @@ -746,6 +746,7 @@ pie title Minerals in my tap water { title: "Center", toolTip: "Center align (Cmd-\\)",btnType: ButtonType.ToggleButton, icon: "align-center",toolType:"center",ignoreClick: true, scripts: {onClick: '{ return toggleCharStyle(this.toolType, _readOnly_);}'} }, { title: "Right", toolTip: "Right align (Cmd-])", btnType: ButtonType.ToggleButton, icon: "align-right", toolType:"right", ignoreClick: true, scripts: {onClick: '{ return toggleCharStyle(this.toolType, _readOnly_);}'} }, ]}, + { title: "Fit Box", toolTip: "Fit text to box", btnType: ButtonType.ToggleButton, icon: "object-group",toolType:"fitBox", ignoreClick: true, scripts: {onClick: '{ return toggleCharStyle(this.toolType, _readOnly_);}'} }, { title: "Elide", toolTip: "Elide selection", btnType: ButtonType.ToggleButton, icon: "eye", toolType:"elide", ignoreClick: true, scripts: {onClick: '{ return toggleCharStyle(this.toolType, _readOnly_);}'} }, { title: "Dictate", toolTip: "Dictate", btnType: ButtonType.ToggleButton, icon: "microphone", toolType:"dictation", ignoreClick: true, scripts: {onClick: '{ return toggleCharStyle(this.toolType, _readOnly_);}'} }, { title: "NoLink", toolTip: "Auto Link", btnType: ButtonType.ToggleButton, icon: "link", toolType:"noAutoLink", expertMode:true, scripts: {onClick: '{ return toggleCharStyle(this.toolType, _readOnly_);}'}, funcs: {hidden: 'IsNoviceMode()'}}, diff --git a/src/client/util/SettingsManager.tsx b/src/client/util/SettingsManager.tsx index 9200d68db..33d7d90a8 100644 --- a/src/client/util/SettingsManager.tsx +++ b/src/client/util/SettingsManager.tsx @@ -1,5 +1,5 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { Button, ColorPicker, Dropdown, DropdownType, EditableText, Group, NumberDropdown, Size, Toggle, ToggleType, Type } from 'browndash-components'; +import { Button, ColorPicker, Colors, Dropdown, DropdownType, EditableText, Group, NumberDropdown, Size, Toggle, ToggleType, Type } from 'browndash-components'; import { action, computed, makeObservable, observable, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; @@ -45,7 +45,6 @@ export class SettingsManager extends React.Component<object> { public closeMgr = action(() => { this._isOpen = false; }); - // eslint-disable-next-line react/no-unused-class-component-methods public openMgr = action(() => { this._isOpen = true; }); @@ -354,6 +353,27 @@ export class SettingsManager extends React.Component<object> { Doc.UserDoc().fontSize = val + 'px'; }} /> + <ColorPicker + color={SettingsManager.userColor} + type={Type.PRIM} + defaultPickerType="Classic" + selectedColor={StrCast(Doc.UserDoc().textBackgroundColor, Colors.LIGHT_GRAY)} + background={SnappingManager.userBackgroundColor} + icon={<FontAwesomeIcon icon="palette" size="lg" />} + tooltip="default text background color" + label="background" + setSelectedColor={value => { + Doc.UserDoc().textBackgroundColor = value; + // if (!this.colorBatch) this.colorBatch = UndoManager.StartBatch(`Set ${tooltip} color`); + // this.colorScript?.script.run({ this: this.Document, value: value, _readOnly_: false }); + }} + setFinalColor={value => { + Doc.UserDoc().textBackgroundColor = value; + // this.colorScript?.script.run({ this: this.Document, value: value, _readOnly_: false }); + // this.colorBatch?.end(); + // this.colorBatch = undefined; + }} + /> <Dropdown items={fontFamilies.map(val => ({ text: val, diff --git a/src/client/views/MarqueeAnnotator.tsx b/src/client/views/MarqueeAnnotator.tsx index 7266875c5..fa1123a2d 100644 --- a/src/client/views/MarqueeAnnotator.tsx +++ b/src/client/views/MarqueeAnnotator.tsx @@ -34,7 +34,7 @@ export interface MarqueeAnnotatorProps { getPageFromScroll?: (top: number) => number; finishMarquee: (x?: number, y?: number) => void; anchorMenuClick?: () => undefined | ((anchor: Doc) => void); - anchorMenuFlashcard?: () => Promise<String>; + anchorMenuFlashcard?: () => Promise<string>; anchorMenuCrop?: (anchor: Doc | undefined, addCrop: boolean) => Doc | undefined; highlightDragSrcColor?: string; } @@ -220,7 +220,6 @@ export class MarqueeAnnotator extends ObservableReactComponent<MarqueeAnnotatorP e.preventDefault(); e.stopPropagation(); let cropRegion: Doc | undefined; - // eslint-disable-next-line no-return-assign const sourceAnchorCreator = () => (cropRegion = this.highlight('', true, undefined, true)); // hyperlink color const targetCreator = (/* annotationOn: Doc | undefined */) => this.props.anchorMenuCrop!(cropRegion, false)!; DragManager.StartAnchorAnnoDrag([ele], new DragManager.AnchorAnnoDragData(this.props.docView(), sourceAnchorCreator, targetCreator), e.pageX, e.pageY, { diff --git a/src/client/views/PinFuncs.ts b/src/client/views/PinFuncs.ts index bed473eec..ab02c2d07 100644 --- a/src/client/views/PinFuncs.ts +++ b/src/client/views/PinFuncs.ts @@ -1,4 +1,4 @@ -import { Doc, DocListCast } from '../../fields/Doc'; +import { Doc, DocListCast, Field } from '../../fields/Doc'; import { DocData } from '../../fields/DocSymbols'; import { Copy, Id } from '../../fields/FieldSymbols'; import { List } from '../../fields/List'; @@ -74,7 +74,7 @@ export function PinDocView(pinDocIn: Doc, pinProps: PinProps, targetDoc: Doc) { const fkey = Doc.LayoutFieldKey(targetDoc); if (pinProps.pinData.dataview) { pinDoc.config_usePath = targetDoc[fkey + '_usePath']; - pinDoc.config_data = targetDoc[fkey] instanceof ObjectField ? (targetDoc[fkey] as ObjectField)[Copy]() : targetDoc.data; + pinDoc.config_data = Field.Copy(targetDoc[fkey]); } if (pinProps.pinData.dataannos) { const fieldKey = Doc.LayoutFieldKey(targetDoc); diff --git a/src/client/views/SidebarAnnos.tsx b/src/client/views/SidebarAnnos.tsx index 1f3ad8444..87076bf65 100644 --- a/src/client/views/SidebarAnnos.tsx +++ b/src/client/views/SidebarAnnos.tsx @@ -78,8 +78,8 @@ export class SidebarAnnos extends ObservableReactComponent<FieldViewProps & Extr _height: 50, _layout_fitWidth: true, _layout_autoHeight: true, - _text_fontSize: StrCast(Doc.UserDoc().fontSize), - _text_fontFamily: StrCast(Doc.UserDoc().fontFamily), + text_fontSize: StrCast(Doc.UserDoc().fontSize), + text_fontFamily: StrCast(Doc.UserDoc().fontFamily), }); Doc.SetSelectOnLoad(target); FormattedTextBox.DontSelectInitialText = true; diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx index 8859f6464..12c032964 100644 --- a/src/client/views/StyleProvider.tsx +++ b/src/client/views/StyleProvider.tsx @@ -251,7 +251,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps & case DocumentType.PRESELEMENT: docColor = docColor || ""; break; case DocumentType.PRES: docColor = docColor || 'transparent'; break; case DocumentType.FONTICON: docColor = boxBackground ? undefined : docColor || Colors.DARK_GRAY; break; - case DocumentType.RTF: docColor = docColor || Colors.LIGHT_GRAY; break; + case DocumentType.RTF: docColor = docColor || StrCast(Doc.UserDoc().textBackgroundColor, Colors.LIGHT_GRAY); break; case DocumentType.LINK: docColor = (isAnchor ? docColor : undefined); break; case DocumentType.INK: docColor = doc?.stroke_isInkMask ? 'rgba(0,0,0,0.7)' : undefined; break; case DocumentType.EQUATION: docColor = docColor || 'transparent'; break; @@ -313,7 +313,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps & ? undefined // if it's a background & has a cluster color, make the shadow spread really big : fieldKey.includes('_inline') // if doc is an inline document in a text box ? `${Colors.DARK_GRAY} ${StrCast(doc.layout_boxShadow, '0vw 0vw 0.1vw')}` - : DocCast(doc.embedContainer)?.type === DocumentType.RTF && !isInk() // if doc is embedded in a text document (but not an inline) + :doc.rootDocument !== doc.embedContainer && DocCast(doc.embedContainer)?.type === DocumentType.RTF && !isInk() // if doc is embedded in a text document (but not an inline) and this isn't a simple text template (where the layoutDoc's rootDocument is its embed container) ? `${Colors.DARK_GRAY} ${StrCast(doc.layout_boxShadow, '0.2vw 0.2vw 0.8vw')}` : StrCast(doc.layout_boxShadow, ''); } diff --git a/src/client/views/StyleProviderQuiz.tsx b/src/client/views/StyleProviderQuiz.tsx index 1f2ad1485..f07a2c803 100644 --- a/src/client/views/StyleProviderQuiz.tsx +++ b/src/client/views/StyleProviderQuiz.tsx @@ -66,7 +66,7 @@ export namespace styleProviderQuiz { newCol.zIndex = 1000; newCol.forceActive = true; newCol.quiz = text; - newCol[DocData].textTransform = 'none'; + newCol[DocData][Doc.LayoutFieldKey(newCol) + '_transform'] = 'none'; Doc.AddDocToList(img.Document, '_quizBoxes', newCol); img.addDocument(newCol); // img._loading = false; diff --git a/src/client/views/collections/CollectionCardDeckView.tsx b/src/client/views/collections/CollectionCardDeckView.tsx index beb45f326..0ba6b679c 100644 --- a/src/client/views/collections/CollectionCardDeckView.tsx +++ b/src/client/views/collections/CollectionCardDeckView.tsx @@ -620,7 +620,6 @@ export class CollectionCardView extends CollectionSubView() { * Actually renders all the cards */ @computed get renderCards() { - console.log('CHILDPw = ' + this.childPanelWidth()); // Map sorted documents to their rendered components return this.sortedDocs.map((doc, index) => { const cardsInRow = this.cardsInRowThatIncludesCardIndex(index); diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormClusters.ts b/src/client/views/collections/collectionFreeForm/CollectionFreeFormClusters.ts index 6ad67a864..8530f7a23 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormClusters.ts +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormClusters.ts @@ -1,4 +1,4 @@ -import { action, observable } from 'mobx'; +import { action, observable, untracked } from 'mobx'; import { CollectionFreeFormView } from '.'; import { intersectRect } from '../../../../Utils'; import { Doc, Opt } from '../../../../fields/Doc'; @@ -179,7 +179,9 @@ export class CollectionFreeFormClusters { }; styleProvider = (doc: Opt<Doc>, props: Opt<FieldViewProps>, property: string) => { - if (doc && this.childDocs?.includes(doc)) + // without untracked, every inquired style property for any Doc will be invalidated if a change is made to the collection's childDocs. + // this prevents that by assuming that a Doc is generally always (or never) a member of childDocs - if it's removed or added, then all of its properties get updated anyway. + if (doc && untracked(() => this.childDocs)?.includes(doc)) switch (property.split(':')[0]) { case StyleProp.BackgroundColor: { diff --git a/src/client/views/collections/collectionSchema/SchemaRowBox.tsx b/src/client/views/collections/collectionSchema/SchemaRowBox.tsx index 6ffb0865a..7f519b065 100644 --- a/src/client/views/collections/collectionSchema/SchemaRowBox.tsx +++ b/src/client/views/collections/collectionSchema/SchemaRowBox.tsx @@ -188,7 +188,7 @@ export class SchemaRowBox extends ViewBoxBaseComponent<SchemaRowBoxProps>() { selectedCells={this.selectedCells} selectedCol={this.selectedCol} setColumnValues={this.setColumnValues} - oneLine={BoolCast(this.schemaDoc?._singleLine)} + oneLine={BoolCast(this.schemaDoc?._schema_singleLine)} menuTarget={this.schemaView.MenuTarget} transform={() => { const ind = index === this.schemaView.columnKeys.length - 1 ? this.schemaView.columnKeys.length - 3 : index; diff --git a/src/client/views/global/globalScripts.ts b/src/client/views/global/globalScripts.ts index 954c79f7d..013b18a97 100644 --- a/src/client/views/global/globalScripts.ts +++ b/src/client/views/global/globalScripts.ts @@ -295,10 +295,10 @@ ScriptingGlobals.add(function setTagFilter(tag: string, added: boolean, checkRes }, ''); // eslint-disable-next-line prefer-arrow-callback -ScriptingGlobals.add(function setFontAttr(attr: 'font' | 'fontColor' | 'highlight' | 'fontSize' | 'alignment', value: string | number, checkResult?: boolean) { +ScriptingGlobals.add(function setFontAttr(attr: 'font' | 'fontColor' | 'highlight' | 'fontSize', value: string | number, checkResult?: boolean) { const editorView = RichTextMenu.Instance?.TextView?.EditorView; // prettier-ignore - const map: Map<'font'|'fontColor'|'highlight'|'fontSize'|'alignment', { checkResult: () => string | undefined; setDoc: () => void;}> = new Map([ + const map: Map<'font'|'fontColor'|'highlight'|'fontSize', { checkResult: () => string | undefined; setDoc: () => void;}> = new Map([ ['font', { checkResult: () => RichTextMenu.Instance?.fontFamily, setDoc: () => value && RichTextMenu.Instance?.setFontField(value.toString(), 'fontFamily'), @@ -311,10 +311,6 @@ ScriptingGlobals.add(function setFontAttr(attr: 'font' | 'fontColor' | 'highligh checkResult: () => RichTextMenu.Instance?.fontColor, setDoc: () => value && RichTextMenu.Instance?.setFontField(value.toString(), 'fontColor'), }], - ['alignment', { - checkResult: () => RichTextMenu.Instance?.textAlign, - setDoc: () => { value && editorView?.state ? RichTextMenu.Instance?.align(editorView, editorView.dispatch, value.toString() as "center"|"left"|"right"):(Doc.UserDoc().textAlign = value); }, - }], ['fontSize', { checkResult: () => RichTextMenu.Instance?.fontSize.replace('px', ''), setDoc: () => { @@ -334,7 +330,7 @@ ScriptingGlobals.add(function setFontAttr(attr: 'font' | 'fontColor' | 'highligh return undefined; }); -type attrname = 'noAutoLink' | 'dictation' | 'bold' | 'italics' | 'elide' | 'underline' | 'left' | 'center' | 'right' | 'vcent' | 'bullet' | 'decimal'; +type attrname = 'noAutoLink' | 'dictation' | 'fitBox' | 'bold' | 'italics' | 'elide' | 'underline' | 'left' | 'center' | 'right' | 'vcent' | 'bullet' | 'decimal'; type attrfuncs = [attrname, { checkResult: () => boolean; toggle?: () => unknown }]; // eslint-disable-next-line prefer-arrow-callback @@ -343,14 +339,10 @@ ScriptingGlobals.add(function toggleCharStyle(charStyle: attrname, checkResult?: const editorView = textView?.EditorView; // prettier-ignore const alignments:attrfuncs[] = (['left','right','center','vcent'] as ("left"|"center"|"right"|"vcent")[]).map((where) => - [ where, { checkResult: () => editorView ? (where === 'vcent' ? RichTextMenu.Instance?.textVcenter ?? false: - (RichTextMenu.Instance?.textAlign === where)): - where === 'vcent' ? BoolCast(Doc.UserDoc()._layout_centered): - (Doc.UserDoc().textAlign === where), - toggle: () => { editorView?.state ? (where === 'vcent' ? RichTextMenu.Instance?.vcenterToggle(): - RichTextMenu.Instance?.align(editorView, editorView.dispatch, where)): - where === 'vcent' ? Doc.UserDoc()._layout_centered = !Doc.UserDoc()._layout_centered: - (Doc.UserDoc().textAlign = where); } + [ where, { checkResult: () => (where === 'vcent' ? RichTextMenu.Instance?.textVcenter ?? false: + (RichTextMenu.Instance?.textAlign === where)), + toggle: () => { (where === 'vcent' ? RichTextMenu.Instance?.vcenterToggle(): + RichTextMenu.Instance?.align(editorView, editorView?.dispatch, where)); } }]); // prettier-ignore // prettier-ignore const listings:attrfuncs[] = (['bullet','decimal'] as attrname[]).map(list => @@ -360,6 +352,8 @@ ScriptingGlobals.add(function toggleCharStyle(charStyle: attrname, checkResult?: const attrs:attrfuncs[] = [ ['dictation', { checkResult: () => !!textView?._recordingDictation, toggle: () => textView && runInAction(() => { textView._recordingDictation = !textView._recordingDictation;} ) }], + ['fitBox', { checkResult: () => RichTextMenu.Instance?.fitBox ?? false, + toggle: () => RichTextMenu.Instance?.toggleFitBox()}], ['elide', { checkResult: () => false, toggle: () => editorView ? RichTextMenu.Instance?.elideSelection(): 0}], ['noAutoLink',{ checkResult: () => ((editorView && RichTextMenu.Instance?.noAutoLink) ?? false), diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu.tsx b/src/client/views/nodes/DataVizBox/DocCreatorMenu.tsx index 16c016d6c..7c601185e 100644 --- a/src/client/views/nodes/DataVizBox/DocCreatorMenu.tsx +++ b/src/client/views/nodes/DataVizBox/DocCreatorMenu.tsx @@ -245,7 +245,7 @@ export class DocCreatorMenu extends ObservableReactComponent<FieldViewProps> { } updateIcons = (docs: Doc[]) => { - console.log('called') + console.log('called'); docs.map(this.getIcon); }; @@ -416,7 +416,7 @@ export class DocCreatorMenu extends ObservableReactComponent<FieldViewProps> { const height = bottom - top; const width = right - left; const doc = !field.title.includes('$$') - ? Docs.Create.TextDocument('', { _height: height, _width: width, title: field.title, x: left, y: top, _text_fontSize: `${height / 2}` }) + ? Docs.Create.TextDocument('', { _height: height, _width: width, title: field.title, x: left, y: top, text_fontSize: `${height / 2}` }) : Docs.Create.ImageDocument('', { _height: height, _width: width, title: field.title.replace(/\$\$/g, ''), x: left, y: top }); return doc; }); @@ -919,17 +919,17 @@ export class DocCreatorMenu extends ObservableReactComponent<FieldViewProps> { @action setExpandedView = (info: { icon: ImageField; doc: Doc } | undefined) => { if (info) { const doc = info.doc; - const wrapper: Doc = Docs.Create.FreeformDocument([info.doc], { _height: NumListCast(doc._height)[0], _width: NumListCast(doc._width)[0], title: ''}); - const newInfo = {icon: new ImageField(''), doc: wrapper} + const wrapper: Doc = Docs.Create.FreeformDocument([info.doc], { _height: NumListCast(doc._height)[0], _width: NumListCast(doc._width)[0], title: '' }); + const newInfo = { icon: new ImageField(''), doc: wrapper }; this._expandedPreview = newInfo; } else { this._expandedPreview = info; } }; - get editingWindow(){ + get editingWindow() { const doc = this._expandedPreview?.doc ?? new Doc(); - const rendered = + const rendered = ( <div className="docCreatorMenu-expanded-template-preview"> <CollectionFreeFormView Document={this._expandedPreview!.doc} @@ -945,7 +945,7 @@ export class DocCreatorMenu extends ObservableReactComponent<FieldViewProps> { removeDocument={returnFalse} PanelWidth={() => this._menuDimensions.width - 10} PanelHeight={() => this._menuDimensions.height - 60} - ScreenToLocalTransform={() => new Transform(-this._pageX,-this._pageY, 1)} + ScreenToLocalTransform={() => new Transform(-this._pageX, -this._pageY, 1)} renderDepth={5} whenChildContentsActiveChanged={emptyFunction} focus={emptyFunction} @@ -961,14 +961,21 @@ export class DocCreatorMenu extends ObservableReactComponent<FieldViewProps> { yPadding={0} /> </div> - + ); return ( <div className="docCreatorMenu-expanded-template-preview"> - <div className="top-panel"/> + <div className="top-panel" /> {rendered} <div className="right-buttons-panel"> - <button className="docCreatorMenu-menu-button section-reveal-options top-right" onPointerDown={e => this.setUpButtonClick(e, () => {this._expandedPreview && this.updateIcons(this._suggestedTemplates.slice()); this.setExpandedView(undefined)})}> + <button + className="docCreatorMenu-menu-button section-reveal-options top-right" + onPointerDown={e => + this.setUpButtonClick(e, () => { + this._expandedPreview && this.updateIcons(this._suggestedTemplates.slice()); + this.setExpandedView(undefined); + }) + }> <FontAwesomeIcon icon="minimize" /> </button> <button className="docCreatorMenu-menu-button section-reveal-options top-right-lower" onPointerDown={e => this.setUpButtonClick(e, () => this._expandedPreview && this._templateDocs.push(this._expandedPreview.doc))}> @@ -976,7 +983,6 @@ export class DocCreatorMenu extends ObservableReactComponent<FieldViewProps> { </button> </div> </div> - ); } @@ -2260,11 +2266,11 @@ export class FieldUtils { title: title, x: coord.x, y: coord.y, - _text_fontSize: `${FieldUtils.calculateFontSize(width, height, content, true)}`, + text_fontSize: `${FieldUtils.calculateFontSize(width, height, content, true)}`, backgroundColor: opts.backgroundColor ?? '', text_fontColor: opts.color, contentBold: opts.fontBold, - textTransform: opts.fontTransform, + text_transform: opts.fontTransform, color: opts.color, _layout_borderRounding: `${opts.cornerRounding ?? 0}px`, borderColor: opts.borderColor, @@ -2306,7 +2312,7 @@ export class FieldUtils { public static CarouselField = (coords: { tl: [number, number]; br: [number, number] }, parentWidth: number, parentHeight: number, title: string, fields: Doc[]) => { const { width, height, coord } = FieldUtils.getDimensions(coords, parentWidth, parentHeight); - const doc = Docs.Create.Carousel3DDocument(fields, { _height: height, _width: width, title: title, x: coord.x, y: coord.y, _text_fontSize: `${height / 2}` }); + const doc = Docs.Create.Carousel3DDocument(fields, { _height: height, _width: width, title: title, x: coord.x, y: coord.y, text_fontSize: `${height / 2}` }); return doc; }; @@ -2359,4 +2365,3 @@ export class FieldUtils { // }] // }; // } - diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index a343b9a39..eb7f333b9 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -880,7 +880,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document background: this.backgroundBoxColor, opacity: this.opacity, cursor: Doc.ActiveTool === InkTool.None ? 'grab' : 'crosshair', - color: StrCast(this.layoutDoc.color, 'inherit'), + color: StrCast(this.Document._color, 'inherit'), fontFamily: StrCast(this.Document._text_fontFamily, 'inherit'), fontSize: Cast(this.Document._text_fontSize, 'string', null), transform: this._animateScalingTo ? `scale(${this._animateScalingTo})` : undefined, @@ -981,12 +981,10 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document type: SpringType.GENTLE, ...springMappings.gentle, }; - switch (StrCast(presEffectDoc?.presentation_effect, StrCast(presEffectDoc?.followLinkAnimEffect))) { - case PresEffect.Expand: return <SpringAnimation doc={root} startOpacity={0} dir={dir} presEffect={PresEffect.Expand} springSettings={timingConfig}>{renderDoc}</SpringAnimation> - case PresEffect.Flip: return <SpringAnimation doc={root} startOpacity={0} dir={dir} presEffect={PresEffect.Flip} springSettings={timingConfig}>{renderDoc}</SpringAnimation> - case PresEffect.Rotate: return <SpringAnimation doc={root} startOpacity={0} dir={dir} presEffect={PresEffect.Rotate} springSettings={timingConfig}>{renderDoc}</SpringAnimation> - case PresEffect.Bounce: return <SpringAnimation doc={root} startOpacity={0} dir={dir} presEffect={PresEffect.Bounce} springSettings={timingConfig}>{renderDoc}</SpringAnimation> - case PresEffect.Roll: return <SpringAnimation doc={root} startOpacity={0} dir={dir} presEffect={PresEffect.Roll} springSettings={timingConfig}>{renderDoc}</SpringAnimation> + const presEffect = StrCast(presEffectDoc?.presentation_effect, StrCast(presEffectDoc?.followLinkAnimEffect)); + switch (presEffect) { + case PresEffect.Expand: case PresEffect.Flip: case PresEffect.Rotate: case PresEffect.Bounce: + case PresEffect.Roll: return <SpringAnimation doc={root} startOpacity={0} dir={dir} presEffect={presEffect} springSettings={timingConfig}>{renderDoc}</SpringAnimation> // case PresEffect.Fade: return <SlideEffect doc={root} dir={dir} presEffect={PresEffect.Fade} tension={timingConfig.stiffness} friction={timingConfig.damping} mass={timingConfig.mass}>{renderDoc}</SlideEffect> case PresEffect.Fade: return <Fade {...effectProps}>{renderDoc}</Fade> // keep as preset, doesn't really make sense with spring config diff --git a/src/client/views/nodes/EquationBox.tsx b/src/client/views/nodes/EquationBox.tsx index fefe25764..290c90d6e 100644 --- a/src/client/views/nodes/EquationBox.tsx +++ b/src/client/views/nodes/EquationBox.tsx @@ -122,7 +122,7 @@ export class EquationBox extends ViewBoxBaseComponent<FieldViewProps>() { width: 'fit-content', // `${100 / scale}%`, height: `${100 / scale}%`, pointerEvents: !this._props.isSelected() ? 'none' : undefined, - fontSize: StrCast(this.layoutDoc._text_fontSize), + fontSize: StrCast(this.Document._text_fontSize), }} onKeyDown={e => e.stopPropagation()}> <EquationEditor ref={this._ref} value={StrCast(this.dataDoc.text, 'x')} spaceBehavesLikeTab onChange={this.onChange} autoCommands="pi theta sqrt sum prod alpha beta gamma rho" autoOperatorNames="sin cos tan" /> diff --git a/src/client/views/nodes/FontIconBox/FontIconBox.scss b/src/client/views/nodes/FontIconBox/FontIconBox.scss index 2db285910..ab03a2318 100644 --- a/src/client/views/nodes/FontIconBox/FontIconBox.scss +++ b/src/client/views/nodes/FontIconBox/FontIconBox.scss @@ -10,6 +10,10 @@ height: 3px !important; } } +.fonticonbox { + margin: auto; + width: 100%; +} .menuButton { height: 100%; display: flex; diff --git a/src/client/views/nodes/FontIconBox/FontIconBox.tsx b/src/client/views/nodes/FontIconBox/FontIconBox.tsx index d4898eb3c..b45774a75 100644 --- a/src/client/views/nodes/FontIconBox/FontIconBox.tsx +++ b/src/client/views/nodes/FontIconBox/FontIconBox.tsx @@ -262,11 +262,13 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() { const script = ScriptCast(this.Document.onClick)?.script; const toggleStatus = script?.run({ this: this.Document, value: undefined, _readOnly_: true }).result as boolean; + // Colors const color = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Color) as string; const background = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BackgroundColor) as string; const items = DocListCast(this.dataDoc.data); const selectedItems = items.filter(itemDoc => ScriptCast(itemDoc.onClick).script.run({ this: itemDoc, value: undefined, _readOnly_: true }).result).map(item => StrCast(item.toolType)); + return ( <MultiToggle tooltip={`Toggle ${tooltip}`} @@ -392,7 +394,7 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() { render() { return ( - <div style={{ margin: 'auto', width: '100%' }} onContextMenu={this.specificContextMenu}> + <div className="fonticonbox" onContextMenu={this.specificContextMenu}> {this.renderButton()} </div> ); diff --git a/src/client/views/nodes/LabelBox.scss b/src/client/views/nodes/LabelBox.scss index ca4b3d467..889cdc0ca 100644 --- a/src/client/views/nodes/LabelBox.scss +++ b/src/client/views/nodes/LabelBox.scss @@ -13,7 +13,6 @@ height: 100%; border-radius: inherit; //letter-spacing: 2px; // bcz: doesn't work with LabelBigText - text-transform: uppercase; overflow: hidden; display: inline-block; margin: auto; diff --git a/src/client/views/nodes/LabelBox.tsx b/src/client/views/nodes/LabelBox.tsx index 94a9541f2..dcf9e1fed 100644 --- a/src/client/views/nodes/LabelBox.tsx +++ b/src/client/views/nodes/LabelBox.tsx @@ -1,19 +1,22 @@ import { Property } from 'csstype'; -import { action, computed, makeObservable } from 'mobx'; +import { action, computed, IReactionDisposer, makeObservable, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import * as textfit from 'textfit'; -import { Field, FieldType } from '../../../fields/Doc'; -import { BoolCast, NumCast, StrCast } from '../../../fields/Types'; +import { Doc, Field } from '../../../fields/Doc'; +import { NumCast, StrCast } from '../../../fields/Types'; import { TraceMobx } from '../../../fields/util'; import { DocumentType } from '../../documents/DocumentTypes'; import { Docs } from '../../documents/Documents'; import { DragManager } from '../../util/DragManager'; +import { undoable } from '../../util/UndoManager'; import { ViewBoxBaseComponent } from '../DocComponent'; import { PinDocView, PinProps } from '../PinFuncs'; import { StyleProp } from '../StyleProp'; import { FieldView, FieldViewProps } from './FieldView'; import './LabelBox.scss'; +import { FormattedTextBox } from './formattedText/FormattedTextBox'; +import { RichTextMenu } from './formattedText/RichTextMenu'; @observer export class LabelBox extends ViewBoxBaseComponent<FieldViewProps>() { @@ -22,7 +25,8 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps>() { } private dropDisposer?: DragManager.DragDropDisposer; private _timeout: NodeJS.Timeout | undefined; - _divRef: HTMLDivElement | null = null; + private _divRef: HTMLDivElement | null = null; + private _reaction: IReactionDisposer | undefined; constructor(props: FieldViewProps) { super(props); @@ -36,21 +40,29 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps>() { } }; - @computed get Title() { - return Field.toString(this.dataDoc[this.fieldKey] as FieldType) || StrCast(this.Document.title); - } - - @computed get backgroundColor() { - return this._props.styleProvider?.(this.Document, this._props, StyleProp.BackgroundColor) as string; - } - componentDidMount() { this._props.setContentViewBox?.(this); + this._reaction = reaction( + () => this.Title, + () => document.activeElement !== this._divRef && this._forceRerender++ + ); } componentWillUnMount() { this._timeout && clearTimeout(this._timeout); + this.setText(this._divRef?.innerText ?? ''); + this._reaction?.(); } + @observable _forceRerender = 0; + + @computed get Title() { return Field.toString(this.dataDoc[this.fieldKey]); } // prettier-ignore + @computed get backgroundColor() { return this._props.styleProvider?.(this.Document, this._props, StyleProp.BackgroundColor) as string; } // prettier-ignore + @computed get boxShadow() { return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BoxShadow) as string; } // prettier-ignore + + setText = undoable((text: string) => { + this.dataDoc[this.fieldKey] = text; + }, 'set label text'); + drop = (/* e: Event, de: DragManager.DropEvent */) => { return false; }; @@ -82,10 +94,11 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps>() { const textfitParams = { minFontSize: NumCast(this.layoutDoc._label_minFontSize, 1), maxFontSize: NumCast(this.layoutDoc._label_maxFontSize, 100), - multiLine: BoolCast(this.layoutDoc._singleLine, true) ? false : true, - alignHoriz: true, + multiLine: r?.textContent?.includes('\n') ? true : false, + // hack because tetFit doesn't support align 'right', but we need mobx to invalidate, so treat null as false and set to right inline + alignHoriz: StrCast(this.layoutDoc[this.fieldKey + '_align']) === 'center' ? true : StrCast(this.layoutDoc[this.fieldKey + '_align']) === 'right' ? (null as unknown as boolean) : false, alignVert: true, - detectMultiLine: true, + detectMultiLine: false, }; if (r) { if (!r.offsetHeight || !r.offsetWidth) { @@ -94,65 +107,140 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps>() { this._timeout = setTimeout(() => this.fitTextToBox(r)); return textfitParams; } + r.style.whiteSpace = ''; // textfit sets to nowrap if not multiline, but doesn't reeset if it becomes multiline + r.style.textAlign = StrCast(this.layoutDoc[this.fieldKey + '_align']); // textfit doesn't reset textAlign if it has been set to center, so we just set it to what we want + r.firstChild instanceof HTMLElement && (r.firstChild.style.textAlign = StrCast(this.layoutDoc[this.fieldKey + '_align'])); textfit(r, textfitParams); } return textfitParams; }; + resetCursor = (cranchor?: number) => { + if (this._divRef && (cranchor || this._divRef === document.activeElement)) { + const range = document.createRange(); + const anchor = cranchor ?? this._divRef.childNodes.length; + const container = cranchor === undefined ? this._divRef : (this._divRef.firstChild?.firstChild ?? this._divRef); + range.setStart(container, anchor); + range.setEnd(container, anchor); + const sel = window.getSelection(); + sel?.removeAllRanges(); + sel?.addRange(range); + } + }; + + beforeInput = action((event: InputEvent) => { + const spanChild = this._divRef?.firstChild?.firstChild; + if (spanChild?.nodeName === '#text' && ['insertLineBreak', 'insertParagraph'].includes(event.inputType)) { + event.preventDefault(); + event.stopPropagation(); + + const selection = document.getSelection(); + if (selection && document.activeElement === event.target) { + const text = spanChild.textContent ?? ''; + const cranchor = selection.anchorNode === this._divRef ? (selection.anchorOffset ? text.length : 0) : selection.anchorOffset; + const addReturnHack = text.length <= cranchor && text[text.length - 1] !== '\n' ? '\n\n' : '\n'; // not sure why, but need to add a second carriage return if typing enter at the end of the text + const splitText = text.substring(0, cranchor) + addReturnHack + text.substring(cranchor); + spanChild.textContent = splitText; + this.resetCursor(cranchor + addReturnHack.length); + } + // const span = document.createElement('span'); + // span.innerHTML = '​'; + // this._divRef!.append(span); + } + }); + // .labelBox-mainButton > div > span:nth-child(2) { + + /** + * When an IconButton is clicked, it will receive focus. However, we don't want that since we want or need that since we really want + * to maintain focus in the label's editing div (and cursor position). so this relies on IconButton's having a tabindex set to -1 so that + * we can march up the tree from the 'relatedTarget' to determine if the loss of focus was caused by a fonticonbox. If it is, we then + * restore focus + * @param e focusout event on the editing div + */ + keepFocus = (e: FocusEvent) => { + if (e.relatedTarget instanceof HTMLElement && e.relatedTarget.tabIndex === -1) { + for (let ele: HTMLElement | null = e.relatedTarget; ele; ele = (ele as HTMLElement)?.parentElement) { + if ((ele as HTMLElement)?.className === 'fonticonbox') { + setTimeout(() => this._divRef?.focus()); + break; + } + } + } + }; + render() { TraceMobx(); const boxParams = this.fitTextToBox(undefined); // this causes mobx to trigger re-render when data changes - const label = this.Title.startsWith('#') ? null : this.Title; return ( - <div key={label?.length} className="labelBox-outerDiv" ref={this.createDropTarget} style={{ boxShadow: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BoxShadow) as string }}> + <div className="labelBox-outerDiv" ref={this.createDropTarget} style={{ boxShadow: this.boxShadow }}> <div className="labelBox-mainButton" style={{ backgroundColor: this.backgroundColor, - // fontSize: StrCast(this.layoutDoc._text_fontSize), - color: StrCast(this.layoutDoc._color), - fontFamily: StrCast(this.layoutDoc._text_fontFamily) || 'inherit', + color: StrCast(this.layoutDoc._text_fontColor, StrCast(this.layoutDoc._color)), + fontFamily: StrCast(this.layoutDoc._text_fontFamily, StrCast(Doc.UserDoc().fontFamily)) || 'inherit', letterSpacing: StrCast(this.layoutDoc.letterSpacing), - textTransform: StrCast(this.layoutDoc.textTransform) as Property.TextTransform, + textTransform: StrCast(this.layoutDoc[this.fieldKey + '_transform']) as Property.TextTransform, paddingLeft: NumCast(this.layoutDoc._xPadding), paddingRight: NumCast(this.layoutDoc._xPadding), paddingTop: NumCast(this.layoutDoc._yPadding), paddingBottom: NumCast(this.layoutDoc._yPadding), width: this._props.PanelWidth(), height: this._props.PanelHeight(), - whiteSpace: 'multiLine' in boxParams && boxParams.multiLine ? 'pre-wrap' : 'pre', + whiteSpace: boxParams.multiLine ? 'pre-wrap' : 'pre', }}> <div + key={this._forceRerender} style={{ width: this._props.PanelWidth() - 2 * NumCast(this.layoutDoc._xPadding), height: this._props.PanelHeight() - 2 * NumCast(this.layoutDoc._yPadding), outline: 'unset !important', }} - onKeyDown={action(e => { + onKeyDown={e => { e.stopPropagation(); - })} + }} onKeyUp={action(e => { e.stopPropagation(); - this.dataDoc[this.fieldKey] = this._divRef?.innerText ?? ''; - setTimeout(() => this._props.select(false)); + const text = this._divRef?.firstChild; + if (text && (text as HTMLElement)?.nodeType === 3) { + this._divRef?.removeChild(text); + this._divRef?.firstChild?.appendChild(text); + this.resetCursor(); + } + this.fitTextToBox(this._divRef); })} + onFocus={() => { + RichTextMenu.Instance?.updateMenu(undefined, undefined, undefined, this.dataDoc); + this._divRef?.removeEventListener('focusout', this.keepFocus); + this._divRef?.addEventListener('focusout', this.keepFocus); + }} onBlur={() => { - this.dataDoc[this.fieldKey] = this._divRef?.innerText ?? ''; + this._divRef?.removeEventListener('focusout', this.keepFocus); + this.setText(this._divRef?.innerText ?? ''); + RichTextMenu.Instance?.updateMenu(undefined, undefined, undefined, undefined); + FormattedTextBox.LiveTextUndo?.end(); + FormattedTextBox.LiveTextUndo = undefined; + }} + dangerouslySetInnerHTML={{ + __html: `<span class="textFitted textFitAlignVert" style="display: inline-block; text-align: center; font-size: 100px; height: 0px;">${this.Title.startsWith('#') ? null : (this.Title ?? '')}</span>`, }} contentEditable={this._props.onClickScript?.() ? undefined : true} ref={r => { + this._divRef?.removeEventListener('beforeinput', this.beforeInput); this._divRef = r; - this.fitTextToBox(r); - if (this._props.isSelected() && this._divRef) { - const range = document.createRange(); - range.setStart(this._divRef, this._divRef.childNodes.length); - range.setEnd(this._divRef, this._divRef.childNodes.length); - const sel = window.getSelection(); - sel?.removeAllRanges(); - sel?.addRange(range); + if (this._divRef) { + this._divRef.addEventListener('beforeinput', this.beforeInput); + + if (Doc.SelectOnLoad === this.Document) { + Doc.SelectOnLoad = undefined; + this._divRef.focus(); + } + this.fitTextToBox(this._divRef); + if (this.Title) { + this.resetCursor(); + } } - }}> - {label} - </div> + }} + /> </div> </div> ); @@ -161,9 +249,9 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps>() { Docs.Prototypes.TemplateMap.set(DocumentType.LABEL, { layout: { view: LabelBox, dataField: 'title' }, - options: { acl: '', _singleLine: true, _layout_nativeDimEditable: true, _layout_reflowHorizontal: true, _layout_reflowVertical: true }, + options: { acl: '', _layout_nativeDimEditable: true, _layout_reflowHorizontal: true, _layout_reflowVertical: true, title_align: 'center', title_transform: 'uppercase' }, }); Docs.Prototypes.TemplateMap.set(DocumentType.BUTTON, { layout: { view: LabelBox, dataField: 'title' }, - options: { acl: '', _layout_nativeDimEditable: true, _layout_reflowHorizontal: true, _layout_reflowVertical: true }, + options: { acl: '', _layout_nativeDimEditable: true, _layout_reflowHorizontal: true, _layout_reflowVertical: true, title_align: 'center', title_transform: 'uppercase' }, }); diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index 2aee7b6b3..659cda9dd 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -65,6 +65,7 @@ import { RichTextMenu, RichTextMenuPlugin } from './RichTextMenu'; import { RichTextRules } from './RichTextRules'; import { schema } from './schema_rts'; import { Property } from 'csstype'; +import { LabelBox } from '../LabelBox'; // import * as applyDevTools from 'prosemirror-dev-tools'; export interface FormattedTextBoxProps extends FieldViewProps { @@ -158,6 +159,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB @computed get titleHeight() { return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.HeaderMargin) as number || 0; } // prettier-ignore @computed get layout_autoHeightMargins() { return this.titleHeight + NumCast(this.layoutDoc._layout_autoHeightMargins); } // prettier-ignore @computed get sidebarKey() { return this.fieldKey + '_sidebar'; } // prettier-ignore + @computed get isLabel() { return this.dataDoc[this.fieldKey+"_fitBox"]; } // prettier-ignore constructor(props: FormattedTextBoxProps) { super(props); @@ -165,7 +167,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB this._recordingStart = Date.now(); } - public get EditorView() { return this._editorView; } // prettier-ignore + public get EditorView() { return this.isLabel ? undefined : this._editorView; } // prettier-ignore // public makeAIFlashcards: () => void = unimplementedFunction; public addToCollection: ((doc: Doc | Doc[], annotationKey?: string | undefined) => boolean) | undefined; @@ -175,10 +177,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB // but since removing one anchor from the list of attr anchors isn't implemented, this will end up removing nothing. public RemoveLinkFromDoc(linkDoc?: Doc) { this.unhighlightSearchTerms(); - const state = this._editorView?.state; + const state = this.EditorView?.state; const a1 = DocCast(linkDoc?.link_anchor_1); const a2 = DocCast(linkDoc?.link_anchor_2); - if (state && a1 && a2 && this._editorView) { + if (state && a1 && a2 && this.EditorView) { this.removeDocument(a1); this.removeDocument(a2); let allFoundLinkAnchors: { href: string; title: string; anchorId: string }[] = []; @@ -188,7 +190,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB return true; }); if (allFoundLinkAnchors.length) { - this._editorView.dispatch(removeMarkWithAttrs(state.tr, 0, state.doc.nodeSize - 2, state.schema.marks.linkAnchor, { allAnchors: allFoundLinkAnchors })); + this.EditorView.dispatch(removeMarkWithAttrs(state.tr, 0, state.doc.nodeSize - 2, state.schema.marks.linkAnchor, { allAnchors: allFoundLinkAnchors })); this.setupEditor(this.config, this.fieldKey); } @@ -197,16 +199,16 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB // removes all the specified link references from the selection. // NOTE: as above, this won't work correctly if there are marks with overlapping but not exact sets of link references. public RemoveAnchorFromSelection(allAnchors: { href: string; title: string; linkId: string; targetId: string }[]) { - const state = this._editorView?.state; - if (state && this._editorView) { - this._editorView.dispatch(removeMarkWithAttrs(state.tr, state.selection.from, state.selection.to, state.schema.marks.link, { allAnchors })); + const state = this.EditorView?.state; + if (state && this.EditorView) { + this.EditorView.dispatch(removeMarkWithAttrs(state.tr, state.selection.from, state.selection.to, state.schema.marks.link, { allAnchors })); this.setupEditor(this.config, this.fieldKey); } } getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => { const rootDoc: Doc = Doc.isTemplateDoc(this._props.docViewPath().lastElement()?.Document) ? this.Document : DocCast(this.Document.rootDocument, this.Document); - if (!pinProps && this._editorView?.state.selection.empty) return rootDoc; + if (!pinProps && this.EditorView?.state.selection.empty) return rootDoc; const anchor = Docs.Create.ConfigDocument({ title: StrCast(rootDoc.title), annotationOn: rootDoc }); this.addDocument(anchor); this._finishingLink = true; @@ -263,7 +265,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB } }); }; - AnchorMenu.Instance.Highlight = undoable((color: string) => this._editorView?.state && RichTextMenu.Instance?.setFontField(color, 'fontHighlight'), 'highlght text'); + AnchorMenu.Instance.Highlight = undoable((color: string) => this.EditorView?.state && RichTextMenu.Instance?.setFontField(color, 'fontHighlight'), 'highlght text'); AnchorMenu.Instance.onMakeAnchor = () => this.getAnchor(true); AnchorMenu.Instance.StartCropDrag = unimplementedFunction; /** @@ -292,7 +294,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB }; AnchorMenu.Instance.setSelectedText(window.getSelection()?.toString() ?? ''); - const coordsB = this._editorView!.coordsAtPos(this._editorView!.state.selection.to); + const coordsB = this.EditorView!.coordsAtPos(this.EditorView!.state.selection.to); this._props.rootSelected?.() && AnchorMenu.Instance.jumpTo(coordsB.left, coordsB.bottom); let ele: Opt<HTMLDivElement>; try { @@ -309,7 +311,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB }; leafText = (node: Node) => { - if (node.type === this._editorView?.state.schema.nodes.dashField) { + if (node.type === this.EditorView?.state.schema.nodes.dashField) { const refDoc = !node.attrs.docId ? DocCast(this.Document.rootDocument, this.Document) : (DocServer.GetCachedRefField(node.attrs.docId as string) as Doc); const fieldKey = StrCast(node.attrs.fieldKey); return ( @@ -320,16 +322,16 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB return ''; }; dispatchTransaction = (tx: Transaction) => { - if (this._editorView && !this._editorView.isDestroyed) { - const state = this._editorView.state.apply(tx); - this._editorView.updateState(state); + if (this.EditorView && !this.EditorView.isDestroyed) { + const state = this.EditorView.state.apply(tx); + this.EditorView.updateState(state); this.tryUpdateDoc(false); } }; tryUpdateDoc = (force: boolean) => { - if (this._editorView) { - const { state } = this._editorView; + if (this.EditorView) { + const { state } = this.EditorView; const { dataDoc } = this; const newText = state.doc.textBetween(0, state.doc.content.size, ' \n', this.leafText); const newJson = JSON.stringify(state.toJSON()); @@ -372,7 +374,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB textChange && (dataDoc[this.fieldKey + '_modificationDate'] = new DateField(new Date(Date.now()))); // if we've deleted all the text in a note driven by a template, then restore the template data dataDoc[this.fieldKey] = undefined; - this._editorView.updateState(EditorState.fromJSON(this.config, JSON.parse(rtField.Data))); + this.EditorView.updateState(EditorState.fromJSON(this.config, JSON.parse(rtField.Data))); ScriptCast(this.layoutDoc.onTextChanged, null)?.script.run({ this: this.layoutDoc, text: newText }); unchanged = false; } @@ -387,7 +389,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB if (jsonstring) { const json = JSON.parse(jsonstring); json.selection = state.toJSON().selection; - this._editorView.updateState(EditorState.fromJSON(this.config, json)); + this.EditorView.updateState(EditorState.fromJSON(this.config, json)); } } if (window.getSelection()?.isCollapsed && this._props.rootSelected?.()) { @@ -407,8 +409,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB linkAnchor = anchor; } }); - if (this._editorView && linkTime) { - const { state } = this._editorView; + if (this.EditorView && linkTime) { + const { state } = this.EditorView; const node = state.selection.$from.node(); if (linkAnchor && node.type !== state.schema.nodes.code_block) { const time = linkTime + Date.now() / 1000 - this._recordingStart / 1000; @@ -416,7 +418,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB const { from } = state.selection; const value = state.schema.nodes.audiotag.create({ timeCode: time, audioId: linkAnchor[Id] }); const replaced = state.tr.insert(from - 1, value); - this._editorView.dispatch(replaced.setSelection(new TextSelection(replaced.doc.resolve(from + 1)))); + this.EditorView.dispatch(replaced.setSelection(new TextSelection(replaced.doc.resolve(from + 1)))); } } }; @@ -431,16 +433,16 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB (Doc.isTemplateForField(this.Document) && (link.link_anchor_1 === this.Document || link.link_anchor_2 === this.Document))) && link.link_relationship === LinkManager.AutoKeywords ); // prettier-ignore - if (this._editorView?.state.doc.textContent) { - let { tr } = this._editorView.state; - const { from, to } = this._editorView.state.selection; - const { autoLinkAnchor } = this._editorView.state.schema.marks; + if (this.EditorView?.state.doc.textContent) { + let { tr } = this.EditorView.state; + const { from, to } = this.EditorView.state.selection; + const { autoLinkAnchor } = this.EditorView.state.schema.marks; tr = tr.removeMark(0, tr.doc.content.size, autoLinkAnchor); Doc.MyPublishedDocs.filter(term => term.title).forEach(term => { tr = this.hyperlinkTerm(tr, term, newAutoLinks); }); tr = tr.setSelection(new TextSelection(tr.doc.resolve(from), tr.doc.resolve(to))); - this._editorView?.dispatch(tr); + this.EditorView?.dispatch(tr); } oldAutoLinks.filter(oldLink => !newAutoLinks.has(oldLink) && oldLink.link_anchor_2 !== this.Document).forEach(doc => Doc.DeleteLink?.(doc)); }; @@ -450,11 +452,11 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB if ( !this._props.dontRegisterView && // (this.Document.isTemplateForField === "text" || !this.Document.isTemplateForField) && // only update the title if the data document's data field is changing title.startsWith('-') && - this._editorView && + this.EditorView && !this.dataDoc.title_custom && (Doc.LayoutFieldKey(this.Document) === this.fieldKey || this.fieldKey === 'text') ) { - let node = this._editorView.state.doc; + let node = this.EditorView.state.doc; while (node.firstChild && node.firstChild.type.name !== 'text') node = node.firstChild; const str = node.textContent; const prefix = '-'; @@ -477,7 +479,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB */ hyperlinkTerm = (trIn: Transaction, target: Doc, newAutoLinks: Set<Doc>) => { let tr = trIn; - const editorView = this._editorView; + const editorView = this.EditorView; if (editorView && !Doc.AreProtosEqual(target, this.Document)) { const autoLinkTerm = Field.toString(target.title as FieldType).replace(/^@/, ''); let alink: Doc | undefined; @@ -553,18 +555,18 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB }; unhighlightSearchTerms = () => { - if (this._editorView) { - const { state } = this._editorView; + if (this.EditorView) { + const { state } = this.EditorView; if (state) { const mark = state.schema.mark(state.schema.marks.search_highlight); const activeMark = state.schema.mark(state.schema.marks.search_highlight, { selected: true }); const end = state.doc.nodeSize - 2; - this._editorView.dispatch(state.tr.removeMark(0, end, mark).removeMark(0, end, activeMark)); + this.EditorView.dispatch(state.tr.removeMark(0, end, mark).removeMark(0, end, activeMark)); } } }; adoptAnnotation = (start: number, end: number, mark: Mark) => { - const view = this._editorView!; + const view = this.EditorView!; const nmark = view.state.schema.marks.user_mark.create({ ...mark.attrs, userid: ClientUtils.CurrentUserEmail() }); view.dispatch(view.state.tr.removeMark(start, end, nmark).addMark(start, end, nmark)); }; @@ -616,7 +618,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB if (added) { draggedDoc._freeform_fitContentsToBox = true; Doc.SetContainer(draggedDoc, this.Document); - const view = this._editorView!; + const view = this.EditorView!; try { this._inDrop = true; const pos = view.posAtCoords({ left: de.x, top: de.y })?.pos; @@ -810,7 +812,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB let target: Element | HTMLElement | null = e.target as HTMLElement; // hrefs are stored on the database of the <a> node that wraps the hyerlink <span> while (target && (!(target instanceof HTMLElement) || !target.dataset?.targethrefs)) target = target.parentElement; - const editor = this._editorView; + const editor = this.EditorView; if (editor && target && !(e.nativeEvent instanceof simMouseEvent ? e.nativeEvent.dash : false)) { const hrefs = (target.dataset?.targethrefs as string) ?.trim() @@ -996,8 +998,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB const c = this.ProseRef?.getElementsByTagName('img'); if (c) { for (const i of c) { - console.log(i); - // console.log(canvas.toDataURL()); // canvas.style.zIndex = '2000000'; // document.body.appendChild(canvas); @@ -1022,8 +1022,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB animateRes = (resIndex: number, newText: string) => { if (resIndex < newText.length) { - const marks = this._editorView?.state.storedMarks ?? []; - this._editorView?.dispatch(this._editorView?.state.tr.insertText(newText[resIndex]).setStoredMarks(marks)); + const marks = this.EditorView?.state.storedMarks ?? []; + this.EditorView?.dispatch(this.EditorView?.state.tr.insertText(newText[resIndex]).setStoredMarks(marks)); setTimeout(() => this.animateRes(resIndex + 1, newText), 20); } }; @@ -1035,8 +1035,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB const res = await gptAPICall((this.dataDoc.text as RichTextField)?.Text, GPTCallType.COMPLETION); if (!res) { this.animateRes(0, 'Something went wrong.'); - } else if (this._editorView) { - const { dispatch, state } = this._editorView; + } else if (this.EditorView) { + const { dispatch, state } = this.EditorView; // for no animation, use: dispatch(state.tr.insertText(res)); // for animted response starting at end of text, use: dispatch(state.tr.setSelection(Selection.atEnd(state.doc))); @@ -1057,13 +1057,13 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB }; breakupDictation = () => { - if (this._editorView && this._recordingDictation) { + if (this.EditorView && this._recordingDictation) { this.stopDictation(/* true */); this._break = true; - const { state } = this._editorView; + const { state } = this.EditorView; const { to } = state.selection; const updated = TextSelection.create(state.doc, to, to); - this._editorView.dispatch(state.tr.setSelection(updated).insert(to, state.schema.nodes.paragraph.create({}))); + this.EditorView.dispatch(state.tr.setSelection(updated).insert(to, state.schema.nodes.paragraph.create({}))); if (this._recordingDictation) { this.recordDictation(); } @@ -1082,7 +1082,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB stopDictation = (/* abort: boolean */) => DictationManager.Controls.stop(/* !abort */); setDictationContent = (value: string) => { - if (this._editorView && this._recordingStart) { + if (this.EditorView && this._recordingStart) { if (this._break) { const textanchorFunc = () => { const tanch = Docs.Create.ConfigDocument({ title: 'dictation anchor' }); @@ -1095,22 +1095,22 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB const textanchor = Cast(link.link_anchor_1, Doc, null); if (audioanchor) { audioanchor.backgroundColor = 'tan'; - const audiotag = this._editorView.state.schema.nodes.audiotag.create({ + const audiotag = this.EditorView.state.schema.nodes.audiotag.create({ timeCode: NumCast(audioanchor._timecodeToShow), audioId: audioanchor[Id], textId: textanchor[Id], }); textanchor[DocData].title = 'dictation:' + audiotag.attrs.timeCode; - const tr = this._editorView.state.tr.insert(this._editorView.state.doc.content.size, audiotag); + const tr = this.EditorView.state.tr.insert(this.EditorView.state.doc.content.size, audiotag); const tr2 = tr.setSelection(TextSelection.create(tr.doc, tr.doc.content.size)); - this._editorView.dispatch(tr.setSelection(TextSelection.create(tr2.doc, tr2.doc.content.size))); + this.EditorView.dispatch(tr.setSelection(TextSelection.create(tr2.doc, tr2.doc.content.size))); } } } - const { from } = this._editorView.state.selection; + const { from } = this.EditorView.state.selection; this._break = false; - const tr = this._editorView.state.tr.insertText(value); - this._editorView.dispatch(tr.setSelection(TextSelection.create(tr.doc, from, tr.doc.content.size)).scrollIntoView()); + const tr = this.EditorView.state.tr.insertText(value); + this.EditorView.dispatch(tr.setSelection(TextSelection.create(tr.doc, from, tr.doc.content.size)).scrollIntoView()); } }; @@ -1143,7 +1143,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB } }); this.dataDoc[ForceServerWrite] = this.dataDoc[UpdatingFromServer] = true; // need to allow permissions for adding links to readonly/augment only documents - this._editorView!.dispatch(tr.removeMark(selection.from, selection.to, splitter)); + this.EditorView!.dispatch(tr.removeMark(selection.from, selection.to, splitter)); this.dataDoc[UpdatingFromServer] = this.dataDoc[ForceServerWrite] = false; anchor.text = selectedText; anchor.text_html = this._selectionHTML ?? selectedText; @@ -1177,7 +1177,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB let hadStart = start !== 0; frag.forEach((node, index) => { const examinedNode = findAnchorNode(node, editor); - if (examinedNode?.node && (examinedNode.node.textContent || examinedNode.node.type === this._editorView?.state.schema.nodes.dashDoc || examinedNode.node.type === this._editorView?.state.schema.nodes.audiotag)) { + if (examinedNode?.node && (examinedNode.node.textContent || examinedNode.node.type === this.EditorView?.state.schema.nodes.dashDoc || examinedNode.node.type === this.EditorView?.state.schema.nodes.audiotag)) { nodes.push(examinedNode.node); !hadStart && (start = index + examinedNode.start); hadStart = true; @@ -1186,13 +1186,13 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB return { frag: Fragment.fromArray(nodes), start }; }; const findAnchorNode = (node: Node, editor: EditorView) => { - if (node.type === this._editorView?.state.schema.nodes.audiotag) { + if (node.type === this.EditorView?.state.schema.nodes.audiotag) { if (node.attrs.textId === textAnchorId) { return { node, start: 0 }; } return undefined; } - if (node.type === this._editorView?.state.schema.nodes.dashDoc) { + if (node.type === this.EditorView?.state.schema.nodes.dashDoc) { if (node.attrs.docId === textAnchorId) { return { node, start: 0 }; } @@ -1208,9 +1208,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB }; this._didScroll = false; // assume we don't need to scroll. if we do, this will get set to true in handleScrollToSelextion when we dispatch the setSelection below - if (this._editorView && textAnchorId) { - const { state } = this._editorView; - const ret = findAnchorFrag(state.doc.content, this._editorView); + if (this.EditorView && textAnchorId) { + const { state } = this.EditorView; + const ret = findAnchorFrag(state.doc.content, this.EditorView); const firstChild = ret.frag.childCount ? ret.frag.child(0) : undefined; if (ret.start >= 0 && (ret.frag.size || (firstChild && [state.schema.nodes.dashDoc, state.schema.nodes.audioTag].includes(firstChild.type)))) { @@ -1219,7 +1219,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB if (ret.frag.firstChild) { selection = TextSelection.between(state.doc.resolve(ret.start), state.doc.resolve(ret.start + ret.frag.firstChild.nodeSize)); // bcz: looks better to not have the target selected } - this._editorView.dispatch(state.tr.setSelection(new TextSelection(selection.$from, selection.$from)).scrollIntoView()); + this.EditorView.dispatch(state.tr.setSelection(new TextSelection(selection.$from, selection.$from)).scrollIntoView()); const escAnchorId = textAnchorId[0] >= '0' && textAnchorId[0] <= '9' ? `\\3${textAnchorId[0]} ${textAnchorId.substr(1)}` : textAnchorId; addStyleSheetRule(FormattedTextBox._highlightStyleSheet, `${escAnchorId}`, { background: 'yellow', transform: 'scale(3)', 'transform-origin': 'left bottom' }); setTimeout(() => { @@ -1307,15 +1307,15 @@ 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)) { - this._editorView.updateState(EditorState.fromJSON(this.config, updatedState)); + if (JSON.stringify(this.EditorView.state.toJSON()) !== JSON.stringify(updatedState)) { + this.EditorView.updateState(EditorState.fromJSON(this.config, updatedState)); this.tryUpdateScrollHeight(); } - } else if (this._editorView.state.doc.textContent !== incomingValue?.str) { - selectAll(this._editorView.state, tx => this._editorView?.dispatch(tx.insertText(incomingValue?.str ?? ''))); + } else if (this.EditorView.state.doc.textContent !== (incomingValue?.str ?? '')) { + selectAll(this.EditorView.state, tx => this.EditorView?.dispatch(tx.insertText(incomingValue?.str ?? ''))); } } }, @@ -1333,9 +1333,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB action(selected => { if (selected && this.dataDoc[this.fieldKey + '_placeholder']) { setTimeout(() => { - selectAll(this._editorView!.state, (tx: Transaction) => { - this._editorView?.dispatch(tx); - this._editorView!.focus(); + selectAll(this.EditorView!.state, (tx: Transaction) => { + this.EditorView?.dispatch(tx); + this.EditorView!.focus(); }); }); } @@ -1343,12 +1343,12 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB if (FormattedTextBox._globalHighlights.has('Bold Text')) { this.layoutDoc[DocCss] = this.layoutDoc[DocCss] + 1; // css change happens outside of mobx/react, so this will notify anyone interested in the layout that it has changed } - if (RichTextMenu.Instance?.view === this._editorView && !selected) { + if (((RichTextMenu.Instance?.view === this.EditorView && this.EditorView) || this.isLabel) && !selected) { RichTextMenu.Instance?.updateMenu(undefined, undefined, undefined, undefined); } - if (this._editorView && selected) { - RichTextMenu.Instance?.updateMenu(this._editorView, undefined, this._props, this.layoutDoc); - setTimeout(this.autoLink, 20); + if (selected) { + RichTextMenu.Instance?.updateMenu(this.EditorView, undefined, this._props, this.dataDoc); + this.EditorView && setTimeout(this.autoLink, 20); } }), { fireImmediately: true } @@ -1389,7 +1389,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB // DocCast(this.Document.image)._freeform_fitContentsToBox = true; // Doc.SetContainer(DocCast(this.Document.image), this.Document); - // const view = this._editorView!; + // const view = this.EditorView!; // try { // this._inDrop = true; // const pos = view.posAtCoords({ left: 0, top: 0 })?.pos; @@ -1436,7 +1436,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB }; addPdfReference = (pdfAnchorId: string) => { - const view = this._editorView!; + const view = this.EditorView!; if (pdfAnchorId) { DocServer.GetRefField(pdfAnchorId).then(pdfAnchor => { if (pdfAnchor instanceof Doc) { @@ -1487,7 +1487,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB const curText = Cast(this.dataDoc[this.fieldKey], RichTextField, null) || StrCast(this.dataDoc[this.fieldKey]); const rtfField = Cast((!curText && this.layoutDoc[this.fieldKey]) || this.dataDoc[fieldKey], RichTextField); if (this.ProseRef) { - this._editorView?.destroy(); + this.EditorView?.destroy(); this._editorView = new EditorView(this.ProseRef, { state: rtfField?.Data ? EditorState.fromJSON(config, JSON.parse(rtfField.Data)) : EditorState.create(config), handleScrollToSelection: editorView => { @@ -1519,14 +1519,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB if (!rtfField) { const dataDoc = Doc.IsDelegateField(DocCast(this.layoutDoc.proto), this.fieldKey) ? DocCast(this.layoutDoc.proto) : this.dataDoc; const startupText = Field.toString(dataDoc[fieldKey] as FieldType); - const textAlign = StrCast(this.dataDoc.text_align, StrCast(Doc.UserDoc().textAlign)) || 'left'; + const textAlign = StrCast(this.dataDoc[this.fieldKey + '_align'], StrCast(Doc.UserDoc().textAlign)) || 'left'; if (textAlign !== 'left') { selectAll(this._editorView.state, tr => { - this._editorView?.dispatch(tr.replaceSelectionWith(state.schema.nodes.paragraph.create({ align: textAlign }))); + this.EditorView?.dispatch(tr.replaceSelectionWith(state.schema.nodes.paragraph.create({ align: textAlign }))); }); } if (startupText) { - this._editorView?.dispatch(this._editorView.state.tr.insertText(startupText)); + this.EditorView?.dispatch(this.EditorView.state.tr.insertText(startupText)); } this.tryUpdateDoc(true); } @@ -1539,56 +1539,57 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB Doc.SetSelectOnLoad(undefined); FormattedTextBox.SelectOnLoadChar = ''; } - if (this._editorView && selectOnLoad && !this._props.dontRegisterView && !this._props.dontSelectOnLoad && this.isActiveTab(this.ProseRef)) { + if (this.EditorView && selectOnLoad && !this._props.dontRegisterView && !this._props.dontSelectOnLoad && this.isActiveTab(this.ProseRef)) { this._props.select(false); if (selLoadChar) { - const $from = this._editorView.state.selection.anchor ? this._editorView.state.doc.resolve(this._editorView.state.selection.anchor - 1) : undefined; + 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 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 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.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)); + 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 $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 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)); + 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 } } if (selectOnLoad) { FormattedTextBox.DontSelectInitialText = false; - this._editorView!.focus(); + this.EditorView!.focus(); } if (this._props.isContentActive()) this.prepareForTyping(); - if (this._editorView && FormattedTextBox.PasteOnLoad) { + if (this.EditorView && FormattedTextBox.PasteOnLoad) { const pdfAnchorId = FormattedTextBox.PasteOnLoad.clipboardData?.getData('dash/pdfAnchor'); FormattedTextBox.PasteOnLoad = undefined; pdfAnchorId && this.addPdfReference(pdfAnchorId); } - if (this._props.autoFocus) setTimeout(() => this._editorView!.focus()); // not sure why setTimeout is needed but editing dashFieldView's doesn't work without it. + if (this._props.autoFocus) setTimeout(() => this.EditorView!.focus()); // not sure why setTimeout is needed but editing dashFieldView's doesn't work without it. } // add user mark for any first character that was typed since the user mark that gets set in KeyPress won't have been called yet. prepareForTyping = () => { - if (this._editorView) { + if (this.EditorView) { const { text, paragraph } = schema.nodes; - const selNode = this._editorView.state.selection.$anchor.node(); - if (this._editorView.state.selection.from === 1 && this._editorView.state.selection.empty && [undefined, text, paragraph].includes(selNode?.type)) { + const selNode = this.EditorView.state.selection.$anchor.node(); + if (this.EditorView.state.selection.from === 1 && this.EditorView.state.selection.empty && [undefined, text, paragraph].includes(selNode?.type)) { const docDefaultMarks = [schema.marks.user_mark.create({ userid: ClientUtils.CurrentUserEmail(), modified: Math.floor(Date.now() / 1000) })]; - this._editorView.state.selection.empty && this._editorView.state.selection.from === 1 && this._editorView?.dispatch(this._editorView?.state.tr.setStoredMarks(docDefaultMarks).removeStoredMark(schema.marks.pFontColor)); + this.EditorView.state.selection.empty && this.EditorView.state.selection.from === 1 && this.EditorView?.dispatch(this.EditorView?.state.tr.setStoredMarks(docDefaultMarks).removeStoredMark(schema.marks.pFontColor)); } } }; @@ -1602,7 +1603,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB FormattedTextBox.LiveTextUndo?.end(); FormattedTextBox.LiveTextUndo = undefined; this.unhighlightSearchTerms(); - this._editorView?.destroy(); + this.EditorView?.destroy(); RichTextMenu.Instance?.TextView === this && RichTextMenu.Instance.updateMenu(undefined, undefined, undefined, undefined); FormattedTextBoxComment.tooltip && (FormattedTextBoxComment.tooltip.style.display = 'none'); } @@ -1681,26 +1682,26 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB } }; setFocus = (ipos?: number) => { - const pos = ipos ?? (this._editorView?.state.selection.$from.pos || 1); - setTimeout(() => this._editorView?.dispatch(this._editorView.state.tr.setSelection(TextSelection.near(this._editorView.state.doc.resolve(pos)))), 100); + const pos = ipos ?? (this.EditorView?.state.selection.$from.pos || 1); + 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); + // applyDevTools.applyDevTools(this.EditorView); e.stopPropagation(); }; onClick = (e: React.MouseEvent): void => { if (!this._props.isContentActive()) return; - const editorView = this._editorView; + const editorView = this.EditorView; const editorRoot = editorView?.root instanceof Document ? editorView.root : undefined; if (editorView && (!this._forceUncollapse || editorRoot?.getSelection()?.isCollapsed)) { // this is a hack to allow the cursor to be placed at the end of a document when the document ends in an inline dash comment. Apparently Chrome on Windows has a bug/feature which breaks this when clicking after the end of the text. const pcords = editorView.posAtCoords({ left: e.clientX, top: e.clientY }); const node = pcords && editorView.state.doc.nodeAt(pcords.pos); // get what prosemirror thinks the clicked node is (if it's null, then we didn't click on any text) if (pcords && node?.type === editorView.state.schema.nodes.dashComment) { - this._editorView!.dispatch(editorView.state.tr.setSelection(TextSelection.create(editorView.state.doc, pcords.pos + 2))); + this.EditorView!.dispatch(editorView.state.tr.setSelection(TextSelection.create(editorView.state.doc, pcords.pos + 2))); e.preventDefault(); } if (!node && this.ProseRef) { @@ -1726,33 +1727,33 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB hitBulletTargets(x: number, y: number, collapse: boolean, highlightOnly: boolean, selectOrderedList: boolean = false) { this._forceUncollapse = false; clearStyleSheetRules(FormattedTextBox._bulletStyleSheet); - const clickPos = this._editorView!.posAtCoords({ left: x, top: y }); + const clickPos = this.EditorView!.posAtCoords({ left: x, top: y }); const clickPosVal = clickPos?.pos || 1; let olistPos = clickPosVal; if (clickPos && olistPos && this._props.rootSelected?.()) { - const clickNode = this._editorView?.state.doc.resolve(olistPos).node(); - const nodeBef = this._editorView?.state.doc.resolve(Math.max(0, olistPos - 1)).node(); - olistPos = nodeBef?.type === this._editorView?.state.schema.nodes.ordered_list ? olistPos - 1 : olistPos; - let $olistPos = this._editorView?.state.doc.resolve(olistPos); - let olistNode = (nodeBef !== null || clickNode?.type === this._editorView?.state.schema.nodes.list_item) && olistPos === clickPos?.pos ? clickNode : nodeBef; - if (olistNode?.type === this._editorView?.state.schema.nodes.list_item) { + const clickNode = this.EditorView?.state.doc.resolve(olistPos).node(); + const nodeBef = this.EditorView?.state.doc.resolve(Math.max(0, olistPos - 1)).node(); + olistPos = nodeBef?.type === this.EditorView?.state.schema.nodes.ordered_list ? olistPos - 1 : olistPos; + let $olistPos = this.EditorView?.state.doc.resolve(olistPos); + let olistNode = (nodeBef !== null || clickNode?.type === this.EditorView?.state.schema.nodes.list_item) && olistPos === clickPos?.pos ? clickNode : nodeBef; + if (olistNode?.type === this.EditorView?.state.schema.nodes.list_item) { if ($olistPos && $olistPos.depth) { olistNode = $olistPos.parent; - $olistPos = this._editorView?.state.doc.resolve($olistPos.start($olistPos.depth - 1)); + $olistPos = this.EditorView?.state.doc.resolve($olistPos.start($olistPos.depth - 1)); } } - const maxSize = this._editorView?.state.doc.content.size ?? 0; - const listPos = this._editorView?.state.doc.resolve(Math.min(maxSize, clickPosVal === olistPos ? clickPosVal + 1 : clickPosVal)); + const maxSize = this.EditorView?.state.doc.content.size ?? 0; + const listPos = this.EditorView?.state.doc.resolve(Math.min(maxSize, clickPosVal === olistPos ? clickPosVal + 1 : clickPosVal)); const listNode = listPos?.node(); - if (olistNode && olistNode.type === this._editorView?.state.schema.nodes.ordered_list && listNode) { + if (olistNode && olistNode.type === this.EditorView?.state.schema.nodes.ordered_list && listNode) { if (!highlightOnly) { if (selectOrderedList) { - this._editorView.dispatch(this._editorView.state.tr.setSelection(new NodeSelection(selectOrderedList ? $olistPos! : listPos!))); + this.EditorView.dispatch(this.EditorView.state.tr.setSelection(new NodeSelection(selectOrderedList ? $olistPos! : listPos!))); } else { const nodePos = clickPosVal - (olistPos === clickPosVal ? 0 : 1); - if (this._editorView.state.doc.nodeAt(nodePos)) { - const tr = this._editorView.state.tr.setNodeMarkup(nodePos, listNode.type, { ...listNode.attrs, visibility: !listNode.attrs.visibility }); - this._editorView.dispatch(tr.setSelection(TextSelection.create(tr.doc, nodePos))); + if (this.EditorView.state.doc.nodeAt(nodePos)) { + const tr = this.EditorView.state.tr.setNodeMarkup(nodePos, listNode.type, { ...listNode.attrs, visibility: !listNode.attrs.visibility }); + this.EditorView.dispatch(tr.setSelection(TextSelection.create(tr.doc, nodePos))); } } } @@ -1772,19 +1773,19 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB onBlur = (e: React.FocusEvent) => { 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(); + const stordMarks = this.EditorView?.state.storedMarks?.slice(); if (!(this.EditorView?.state.selection instanceof NodeSelection)) { this.autoLink(); - if (this._editorView?.state.tr) { + if (this.EditorView?.state.tr) { const tr = stordMarks?.reduce((tr2, m) => { tr2.addStoredMark(m); return tr2; - }, this._editorView.state.tr); - tr && this._editorView.dispatch(tr); + }, this.EditorView.state.tr); + tr && this.EditorView.dispatch(tr); } } } - if (RichTextMenu.Instance?.view === this._editorView && !(this._props.isContentActive() || this._props.rootSelected?.())) { + if (RichTextMenu.Instance?.view === this.EditorView && !(this._props.isContentActive() || this._props.rootSelected?.())) { RichTextMenu.Instance?.updateMenu(undefined, undefined, undefined, undefined); } @@ -1831,7 +1832,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB } switch (e.key) { case 'Escape': - this._editorView!.dispatch(state.tr.setSelection(TextSelection.create(state.doc, state.selection.from, state.selection.from))); + this.EditorView!.dispatch(state.tr.setSelection(TextSelection.create(state.doc, state.selection.from, state.selection.from))); (document.activeElement as HTMLElement).blur?.(); DocumentView.DeselectAll(); RichTextMenu.Instance?.updateMenu(undefined, undefined, undefined, undefined); @@ -1857,7 +1858,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB this.startUndoTypingBatch(); }; ondrop = (e: React.DragEvent) => { - this._editorView!.dispatch(updateBullets(this._editorView!.state.tr, this._editorView!.state.schema)); + this.EditorView?.dispatch(updateBullets(this.EditorView.state.tr, this.EditorView.state.schema)); e.stopPropagation(); // drag n drop of text within text note will generate a new note if not caughst, as will dragging in from outside of Dash. }; onScroll = (e: React.UIEvent) => { @@ -1872,7 +1873,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB tryUpdateScrollHeight = () => { const margins = 2 * NumCast(this.layoutDoc._yMargin, this._props.yPadding || 0); const children = this.ProseRef?.children.length ? Array.from(this.ProseRef.children[0].children) : undefined; - if (children && !SnappingManager.IsDragging) { + if (this.EditorView && children && !SnappingManager.IsDragging) { const getChildrenHeights = (kids: Element[] | undefined) => kids?.reduce((p, child) => p + toHgt(child), margins) ?? 0; const toNum = (val: string) => Number(val.replace('px', '')); const toHgt = (node: Element): number => { @@ -2093,7 +2094,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB const paddingX = Math.max(NumCast(this.layoutDoc._xMargin), this._props.xPadding ?? 0, 0, ((this._props.screenXPadding?.() ?? 0) - scrMargin[0]) * this.ScreenToLocalBoxXf().Scale); const paddingY = Math.max(NumCast(this.layoutDoc._yMargin), 0, ((this._props.yPadding ?? 0) - scrMargin[1]) * this.ScreenToLocalBoxXf().Scale); const styleFromLayout = styleFromLayoutString(this.Document, this._props, scale); // this converts any expressions in the format string to style props. e.g., <FormattedTextBox height='{this._header_height}px' > - return styleFromLayout?.height === '0px' ? null : ( + return this.isLabel ? ( + <LabelBox {...this._props} /> + ) : styleFromLayout?.height === '0px' ? null : ( <div className="formattedTextBox" ref={r => { @@ -2156,8 +2159,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB paddingTop: StrCast(this.layoutDoc._textBoxPaddingY, `${paddingY}px`), paddingBottom: StrCast(this.layoutDoc._textBoxPaddingY, `${paddingY}px`), color: StrCast(this.layoutDoc.text_fontColor), - fontWeight: `${this.layoutDoc.contentBold ? 'bold' : ''}`, - textTransform: `${this.layoutDoc.textTransform}` as Property.TextTransform, + fontWeight: this.layoutDoc.contentBold ? 'bold' : '', + textTransform: StrCast(this.dataDoc[this.fieldKey + '_transform']) as Property.TextTransform, }} /> </div> diff --git a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts index 7a8b72be0..3c84e5a10 100644 --- a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts +++ b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts @@ -349,7 +349,9 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any): KeyMa dispatch(tx4); } - if (view.state.selection.$anchor.nodeAfter?.type === schema.nodes.text && once) { + if (view.state.selection.$anchor.depth > 0 && + view.state.selection.$anchor.node(view.state.selection.$anchor.depth-1).type === schema.nodes.list_item && + view.state.selection.$anchor.nodeAfter?.type === schema.nodes.text && once) { // if text is selected across list items, then we need to forcibly insert a new line since the splitBlock code joins the two list items. enter(view.state, dispatch, view, false); } diff --git a/src/client/views/nodes/formattedText/RichTextMenu.tsx b/src/client/views/nodes/formattedText/RichTextMenu.tsx index 55e6a3a5b..65a415a23 100644 --- a/src/client/views/nodes/formattedText/RichTextMenu.tsx +++ b/src/client/views/nodes/formattedText/RichTextMenu.tsx @@ -1,6 +1,6 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Tooltip } from '@mui/material'; -import { action, computed, IReactionDisposer, makeObservable, observable, runInAction } from 'mobx'; +import { action, computed, makeObservable, observable, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import { lift, toggleMark, wrapIn } from 'prosemirror-commands'; import { Mark, MarkType } from 'prosemirror-model'; @@ -32,7 +32,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { public overMenu: boolean = false; // kind of hacky way to prevent selects not being selectable private _linkToRef = React.createRef<HTMLInputElement>(); - layoutDoc: Doc | undefined; + dataDoc: Doc | undefined; @observable public view?: EditorView & { TextView?: FormattedTextBox } = undefined; public editorProps: FieldViewProps | AntimodeMenuProps | undefined; @@ -49,6 +49,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { @observable private _activeFontSize: string = '13px'; @observable private _activeFontFamily: string = ''; + @observable private _activeFitBox: boolean = false; @observable private _activeListType: string = ''; @observable private _activeAlignment: string = 'left'; @@ -64,13 +65,12 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { @observable private currentLink: string | undefined = ''; @observable private showLinkDropdown: boolean = false; - _reaction: IReactionDisposer | undefined; constructor(props: AntimodeMenuProps) { super(props); makeObservable(this); runInAction(() => { RichTextMenu._instance.menu = this; - this.updateMenu(undefined, undefined, props, this.layoutDoc); + this.updateMenu(undefined, undefined, props, this.dataDoc); this._canFade = false; this.Pinned = true; }); @@ -101,6 +101,9 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { @computed get fontHighlight() { return this._activeHighlightColor; } + @computed get fitBox() { + return this._activeFitBox; + } @computed get fontFamily() { return this._activeFontFamily; } @@ -114,26 +117,16 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { return this._activeAlignment; } @computed get textVcenter() { - return BoolCast(this.layoutDoc?._layout_centered); - } - _disposer: IReactionDisposer | undefined; - componentDidMount() { - // this._disposer = reaction( - // () => DocumentView.Selected().slice(), - // () => this.updateMenu(undefined, undefined, undefined, undefined) - // ); - } - componentWillUnmount() { - this._disposer?.(); + return BoolCast(this.dataDoc?._layout_centered, BoolCast(Doc.UserDoc().layout_centered)); } @action - public updateMenu(view: EditorView | undefined, lastState: EditorState | undefined, props: FormattedTextBoxProps | AntimodeMenuProps | undefined, layoutDoc: Doc | undefined) { + public updateMenu(view: EditorView | undefined, lastState: EditorState | undefined, props: FormattedTextBoxProps | AntimodeMenuProps | undefined, dataDoc: Doc | undefined) { if (this._linkToRef.current?.getBoundingClientRect().width) { return; } this.view = view; - this.layoutDoc = layoutDoc; + this.dataDoc = dataDoc; props && (this.editorProps = props); // Don't do anything if the document/selection didn't change @@ -147,12 +140,13 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { const { activeSizes } = active; const { activeColors } = active; const { activeHighlights } = active; - const refDoc = DocumentView.Selected().lastElement()?.layoutDoc ?? Doc.UserDoc(); + const refDoc = DocumentView.Selected().lastElement()?.dataDoc ?? Doc.UserDoc(); const refField = (pfx => (pfx ? pfx + '_' : ''))(DocumentView.Selected().lastElement()?.LayoutFieldKey); const refVal = (field: string, dflt: string) => StrCast(refDoc[refField + field], StrCast(Doc.UserDoc()[field], dflt)); 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._activeFontColor = !activeColors.length ? StrCast(this.TextView?.Document.fontColor, refVal('fontColor', 'black')) : activeColors.length > 0 ? String(activeColors[0]) : '...'; @@ -181,7 +175,6 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { toggleMark(mark.type, mark.attrs)(state, dispatch); } } - // this.updateMenu(this.view, undefined, undefined, this.layoutDoc); } }; @@ -195,8 +188,10 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { return node.attrs.align || 'left'; } } + } else if (this.dataDoc) { + return StrCast(this.dataDoc.text_align) || 'left'; } - return 'left'; + return StrCast(Doc.UserDoc().textAlign) || 'left'; }; // finds font sizes and families in selection @@ -330,6 +325,17 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { this.view.focus(); } }; + toggleFitBox = () => { + if (this.dataDoc) { + const doc = this.dataDoc; + (document.activeElement as HTMLElement)?.blur(); + doc.text_fitBox = !doc.text_fitBox; + } else { + Doc.UserDoc().fitBox = !Doc.UserDoc().fitBox; + Doc.UserDoc().textAlign = Doc.UserDoc().fitBox ? 'center' : undefined; + } + this.updateMenu(undefined, undefined, undefined, this.dataDoc); + }; toggleBold = () => { if (this.view) { const mark = this.view.state.schema.mark(this.view.state.schema.marks.strong); @@ -354,11 +360,14 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { } }; - setFontField = (value: string, fontField: 'fontSize' | 'fontFamily' | 'fontColor' | 'fontHighlight') => { - if (this.TextView && this.view) { + 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 (this.view.state.selection.from === 1 && this.view.state.selection.empty && [undefined, text, paragraph].includes(selNode?.type)) { + 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(); } @@ -369,7 +378,6 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { this.view.focus(); } else { Doc.UserDoc()[fontField] = value; - // this.updateMenu(this.view, undefined, this.props, this.layoutDoc); } }; @@ -395,7 +403,6 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { this.view!.dispatch(tx3); }); this.view.focus(); - // this.updateMenu(this.view, undefined, this.props, this.layoutDoc); }; insertSummarizer(state: EditorState, dispatch: (tr: Transaction) => void) { @@ -410,10 +417,11 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { } vcenterToggle = () => { - this.layoutDoc && (this.layoutDoc._layout_centered = !this.layoutDoc._layout_centered); + if (this.dataDoc) this.dataDoc._layout_centered = !this.dataDoc._layout_centered; + else Doc.UserDoc()._layout_centered = !Doc.UserDoc()._layout_centered; }; - align = (view: EditorView, dispatch: (tr: Transaction) => void, alignment: 'left' | 'right' | 'center') => { - if (this.RootSelected) { + align = (view: EditorView | undefined, dispatch: undefined | ((tr: Transaction) => void), alignment: 'left' | 'right' | 'center') => { + if (view && dispatch && this.RootSelected) { let { tr } = view.state; view.state.doc.nodesBetween(view.state.selection.from, view.state.selection.to, (node, pos) => { if ([schema.nodes.paragraph, schema.nodes.heading].includes(node.type)) { @@ -425,6 +433,11 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { }); view.focus(); dispatch?.(tr); + } else { + if (this.dataDoc) { + this.dataDoc.text_align = alignment; + } else Doc.UserDoc().textAlign = alignment; + this.updateMenu(undefined, undefined, undefined, this.dataDoc); } }; @@ -702,7 +715,7 @@ interface RichTextMenuPluginProps { } export class RichTextMenuPlugin extends React.Component<RichTextMenuPluginProps> { update(view: EditorView & { TextView?: FormattedTextBox }, lastState: EditorState | undefined) { - RichTextMenu.Instance?.updateMenu(view, lastState, this.props.editorProps, view.TextView?.layoutDoc); + RichTextMenu.Instance?.updateMenu(view, lastState, this.props.editorProps, view.TextView?.dataDoc); } render() { return null; diff --git a/src/client/views/nodes/formattedText/RichTextRules.ts b/src/client/views/nodes/formattedText/RichTextRules.ts index f58434906..3d14ce4c0 100644 --- a/src/client/views/nodes/formattedText/RichTextRules.ts +++ b/src/client/views/nodes/formattedText/RichTextRules.ts @@ -121,7 +121,7 @@ export class RichTextRules { annotationOn: textDoc, _layout_fitWidth: true, _layout_autoHeight: true, - _text_fontSize: '9px', + text_fontSize: '9px', title: 'inline comment', }); textDocInline.title = inlineFieldKey; // give the annotation its own title diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx index 5d3672aee..06869717a 100644 --- a/src/client/views/nodes/trails/PresBox.tsx +++ b/src/client/views/nodes/trails/PresBox.tsx @@ -2616,13 +2616,13 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { createTemplate = (layout: string, input?: string) => { const x = this.activeItem && this.targetDoc ? NumCast(this.targetDoc.x) : 0; const y = this.activeItem && this.targetDoc ? NumCast(this.targetDoc.y) + NumCast(this.targetDoc._height) + 20 : 0; - const title = () => Docs.Create.TextDocument('Click to change title', { title: 'Slide title', _width: 380, _height: 60, x: 10, y: 58, _text_fontSize: '24pt' }); - const subtitle = () => Docs.Create.TextDocument('Click to change subtitle', { title: 'Slide subtitle', _width: 380, _height: 50, x: 10, y: 118, _text_fontSize: '16pt' }); - const header = () => Docs.Create.TextDocument('Click to change header', { title: 'Slide header', _width: 380, _height: 65, x: 10, y: 80, _text_fontSize: '20pt' }); - const contentTitle = () => Docs.Create.TextDocument('Click to change title', { title: 'Slide title', _width: 380, _height: 60, x: 10, y: 10, _text_fontSize: '24pt' }); - const content = () => Docs.Create.TextDocument('Click to change text', { title: 'Slide text', _width: 380, _height: 145, x: 10, y: 70, _text_fontSize: '14pt' }); - const content1 = () => Docs.Create.TextDocument('Click to change text', { title: 'Column 1', _width: 185, _height: 140, x: 10, y: 80, _text_fontSize: '14pt' }); - const content2 = () => Docs.Create.TextDocument('Click to change text', { title: 'Column 2', _width: 185, _height: 140, x: 205, y: 80, _text_fontSize: '14pt' }); + const title = () => Docs.Create.TextDocument('Click to change title', { title: 'Slide title', _width: 380, _height: 60, x: 10, y: 58, text_fontSize: '24pt' }); + const subtitle = () => Docs.Create.TextDocument('Click to change subtitle', { title: 'Slide subtitle', _width: 380, _height: 50, x: 10, y: 118, text_fontSize: '16pt' }); + const header = () => Docs.Create.TextDocument('Click to change header', { title: 'Slide header', _width: 380, _height: 65, x: 10, y: 80, text_fontSize: '20pt' }); + const contentTitle = () => Docs.Create.TextDocument('Click to change title', { title: 'Slide title', _width: 380, _height: 60, x: 10, y: 10, text_fontSize: '24pt' }); + const content = () => Docs.Create.TextDocument('Click to change text', { title: 'Slide text', _width: 380, _height: 145, x: 10, y: 70, text_fontSize: '14pt' }); + const content1 = () => Docs.Create.TextDocument('Click to change text', { title: 'Column 1', _width: 185, _height: 140, x: 10, y: 80, text_fontSize: '14pt' }); + const content2 = () => Docs.Create.TextDocument('Click to change text', { title: 'Column 2', _width: 185, _height: 140, x: 205, y: 80, text_fontSize: '14pt' }); // prettier-ignore switch (layout) { case 'blank': return Docs.Create.FreeformDocument([], { title: input || 'Blank slide', _width: 400, _height: 225, x, y }); diff --git a/src/client/views/pdf/GPTPopup/GPTPopup.tsx b/src/client/views/pdf/GPTPopup/GPTPopup.tsx index d5f5f620c..a7e78bcea 100644 --- a/src/client/views/pdf/GPTPopup/GPTPopup.tsx +++ b/src/client/views/pdf/GPTPopup/GPTPopup.tsx @@ -572,18 +572,19 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> { <div style={{ height: '80%' }}> {this.heading(this.mode === GPTPopupMode.SORT ? 'SORTING' : 'QUIZ')} <> - {!this.cardsDoneLoading ? ( - <div className="content-wrapper"> - <div className="loading-spinner"> - <ReactLoading type="spin" color={StrCast(Doc.UserDoc().userVariantColor)} height={30} width={30} /> - {this.loading ? <span>Loading...</span> : <span>Reading Cards...</span>} + { + !this.cardsDoneLoading ? ( + <div className="content-wrapper"> + <div className="loading-spinner"> + <ReactLoading type="spin" color={StrCast(Doc.UserDoc().userVariantColor)} height={30} width={30} /> + {this.loading ? <span>Loading...</span> : <span>Reading Cards...</span>} + </div> </div> - </div> - ) : this.mode === GPTPopupMode.CARD ? ( - this.cardMenu() - ) : ( - this.cardActual(this.mode) - ) // Call the functions to render JSX + ) : this.mode === GPTPopupMode.CARD ? ( + this.cardMenu() + ) : ( + this.cardActual(this.mode) + ) // Call the functions to render JSX } </> </div> diff --git a/src/fields/documentSchemas.ts b/src/fields/documentSchemas.ts index 335683270..b27816f55 100644 --- a/src/fields/documentSchemas.ts +++ b/src/fields/documentSchemas.ts @@ -48,8 +48,8 @@ export const documentSchema = createSchema({ _columnsHideIfEmpty: 'boolean', // whether empty stacking view column headings should be hidden // _columnHeaders: listSpec(SchemaHeaderField), // header descriptions for stacking/masonry // _schemaHeaders: listSpec(SchemaHeaderField), // header descriptions for schema views - _text_fontSize: 'string', - _text_fontFamily: 'string', + text_fontSize: 'string', + text_fontFamily: 'string', _layout_sidebarWidthPercent: 'string', // percent of text window width taken up by sidebar // appearance properties on the data document @@ -70,7 +70,7 @@ export const documentSchema = createSchema({ stroke_startMarker: 'string', stroke_endMarker: 'string', stroke_dash: 'string', - textTransform: 'string', + text_transform: 'string', treeView_Open: 'boolean', // flag denoting whether the documents sub-tree (contents) is visible or hidden treeView_ExpandedView: 'string', // name of field whose contents are being displayed as the document's subtree treeView_ExpandedViewLock: 'boolean', // whether the expanded view can be changed @@ -112,5 +112,4 @@ export const collectionSchema = createSchema({ }); export type Document = makeInterface<[typeof documentSchema]>; -// eslint-disable-next-line no-redeclare export const Document = makeInterface(documentSchema); |