diff options
Diffstat (limited to 'src')
52 files changed, 907 insertions, 804 deletions
diff --git a/src/ClientUtils.ts b/src/ClientUtils.ts index e7aee1c2a..baad9a06c 100644 --- a/src/ClientUtils.ts +++ b/src/ClientUtils.ts @@ -107,12 +107,12 @@ export namespace ClientUtils { default: return type.charAt(0).toUpperCase() + type.substring(1,3); } // prettier-ignore } - export function cleanDocumentType(type: DocumentType, colType: CollectionViewType) { + export function cleanDocumentType(type: DocumentType, colType?: CollectionViewType) { switch (type) { case DocumentType.PDF: return 'PDF'; case DocumentType.IMG: return 'Image'; case DocumentType.AUDIO: return 'Audio'; - case DocumentType.COL: return 'Collection:'+colType; + case DocumentType.COL: return 'Collection:'+ (colType ?? ""); case DocumentType.RTF: return 'Text'; default: return type.charAt(0).toUpperCase() + type.slice(1); } // prettier-ignore @@ -212,7 +212,7 @@ export namespace ClientUtils { return { r: r, g: g, b: b, a: a }; } - const isTransparentFunctionHack = 'isTransparent(__value__)'; + export const isTransparentFunctionHack = 'isTransparent(__value__)'; export const noRecursionHack = '__noRecursion'; // special case filters @@ -223,11 +223,6 @@ export namespace ClientUtils { export function IsRecursiveFilter(val: string) { return !val.includes(noRecursionHack); } - export function HasFunctionFilter(val: string) { - if (val.includes(isTransparentFunctionHack)) return (color: string) => color !== '' && DashColor(color).alpha() !== 1; - // add other function filters here... - return undefined; - } export function toRGBAstr(col: { r: number; g: number; b: number; a?: number }) { return 'rgba(' + col.r + ',' + col.g + ',' + col.b + (col.a !== undefined ? ',' + col.a : '') + ')'; diff --git a/src/client/documents/DocUtils.ts b/src/client/documents/DocUtils.ts index 19f3c89ef..067b9c5e0 100644 --- a/src/client/documents/DocUtils.ts +++ b/src/client/documents/DocUtils.ts @@ -3,7 +3,7 @@ import { IconProp } from '@fortawesome/fontawesome-svg-core'; import { saveAs } from 'file-saver'; import * as JSZip from 'jszip'; import { action, runInAction } from 'mobx'; -import { ClientUtils } from '../../ClientUtils'; +import { ClientUtils, DashColor } from '../../ClientUtils'; import * as JSZipUtils from '../../JSZipUtils'; import { decycle } from '../../decycler/decycler'; import { DateField } from '../../fields/DateField'; @@ -36,11 +36,16 @@ import { DocumentView } from '../views/nodes/DocumentView'; import { CollectionFreeFormView } from '../views/collections/collectionFreeForm'; export namespace DocUtils { + function HasFunctionFilter(val: string) { + if (val.includes(ClientUtils.isTransparentFunctionHack)) return (d: Doc, color: string) => !d.disableMixBlend && color !== '' && DashColor(color).alpha() !== 1; + // add other function filters here... + return undefined; + } function matchFieldValue(doc: Doc, key: string, valueIn: unknown): boolean { let value = valueIn; - const hasFunctionFilter = ClientUtils.HasFunctionFilter(value as string); + const hasFunctionFilter = HasFunctionFilter(value as string); if (hasFunctionFilter) { - return hasFunctionFilter(StrCast(doc[key])); + return hasFunctionFilter(doc, StrCast(doc[key])); } if (key === LinkedTo) { // links are not a field value, so handled here. value is an expression of form ([field=]idToDoc("...")) @@ -678,11 +683,19 @@ export namespace DocUtils { ...(defaultTextTemplate ? {} // 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, + _width: width || BoolCast(Doc.UserDoc().fitBox) ? Number(StrCast(Doc.UserDoc().fontSize).replace('px', '')) * 1.5 * 6 : 200, + _height: BoolCast(Doc.UserDoc().fitBox) ? Number(StrCast(Doc.UserDoc().fontSize).replace('px', '')) * 1.5 : 35, _layout_centered: BoolCast(Doc.UserDoc()._layout_centered), _layout_fitWidth: true, _layout_autoHeight: true, + backgroundColor: StrCast(Doc.UserDoc().textBackgroundColor), + text_fitBox: BoolCast(Doc.UserDoc().fitBox), + text_align: StrCast(Doc.UserDoc().textAlign), + text_fontColor: StrCast(Doc.UserDoc().fontColor), + text_fontFamily: StrCast(Doc.UserDoc().fontFamily), + text_fontWeight: StrCast(Doc.UserDoc().fontWeight), + text_fontStyle: StrCast(Doc.UserDoc().fontStyle), + text_fontDecoration: StrCast(Doc.UserDoc().fontDecoration), }), }); diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index 98e8bc932..efd6ce3c9 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)) @@ -925,7 +927,6 @@ export namespace Docs { I.text_align = 'center'; I.rotation = 0; I.defaultDoubleClick = 'ignore'; - I.keepZWhenDragged = true; I.author_date = new DateField(); I.acl_Guest = Doc.defaultAclPrivate ? SharingPermissions.None : SharingPermissions.View; // I.acl_Override = SharingPermissions.Unset; diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index 30c75c659..fe23fe062 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -4,7 +4,7 @@ import * as rp from 'request-promise'; import { ClientUtils, OmitKeys } from "../../ClientUtils"; import { Doc, DocListCast, DocListCastAsync, FieldType, Opt } from "../../fields/Doc"; import { DocData } from "../../fields/DocSymbols"; -import { InkTool } from "../../fields/InkField"; +import { InkEraserTool, InkInkTool, InkProperty, InkTool } from "../../fields/InkField"; import { List } from "../../fields/List"; import { PrefetchProxy } from "../../fields/Proxy"; import { RichTextField } from "../../fields/RichTextField"; @@ -39,6 +39,7 @@ import { SelectionManager } from "./SelectionManager"; import { ColorScheme } from "./SettingsManager"; import { SnappingManager } from "./SnappingManager"; import { UndoManager } from "./UndoManager"; +import { DocumentView } from "../views/nodes/DocumentView"; export interface Button { // DocumentOptions fields a button can set @@ -186,7 +187,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 +269,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 +393,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,11 +732,11 @@ 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: " 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_);}'} }, - { title: "Italic", toolTip: "Italic (Ctrl+I)", btnType: ButtonType.ToggleButton, icon: "italic", toolType:"italics", ignoreClick: true, scripts: {onClick: '{ return toggleCharStyle(this.toolType, _readOnly_);}'} }, + { title: "Italic", toolTip: "Italic (Ctrl+I)", btnType: ButtonType.ToggleButton, icon: "italic", toolType:"italic", ignoreClick: true, scripts: {onClick: '{ return toggleCharStyle(this.toolType, _readOnly_);}'} }, { title: "Under", toolTip: "Underline (Ctrl+U)", btnType: ButtonType.ToggleButton, icon: "underline", toolType:"underline",ignoreClick: true, scripts: {onClick: '{ return toggleCharStyle(this.toolType, _readOnly_);}'} }, { title: "Bullets", toolTip: "Bullet List", btnType: ButtonType.ToggleButton, icon: "list", toolType:"bullet", ignoreClick: true, scripts: {onClick: '{ return toggleCharStyle(this.toolType, _readOnly_);}'} }, { title: "#", toolTip: "Number List", btnType: ButtonType.ToggleButton, icon: "list-ol", toolType:"decimal", ignoreClick: true, scripts: {onClick: '{ return toggleCharStyle(this.toolType, _readOnly_);}'} }, @@ -746,6 +747,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()'}}, @@ -757,24 +759,27 @@ pie title Minerals in my tap water static inkTools():Button[] { return [ - { title: "Pen", toolTip: "Pen (Ctrl+P)", btnType: ButtonType.ToggleButton, icon: "pen-nib", toolType: "pen", scripts: {onClick:'{ return setActiveTool(this.toolType, false, _readOnly_);}' }}, - { title: "Highlight",toolTip: "Highlight (Ctrl+H)", btnType: ButtonType.ToggleButton, icon: "highlighter",toolType: "highlighter", scripts: {onClick:'{ return setActiveTool(this.toolType, false, _readOnly_);}' }}, - { title: "Write", toolTip: "Write (Ctrl+Shift+P)", btnType: ButtonType.ToggleButton, icon: "pen", toolType: "write", scripts: {onClick:'{ return setActiveTool(this.toolType, false, _readOnly_);}' }, funcs: {hidden:"IsNoviceMode()" }}, - { title: "Eraser", toolTip: "Eraser (Ctrl+E)", btnType: ButtonType.MultiToggleButton, toolType: InkTool.Eraser, scripts: {onClick:'{ return setActiveTool(this.toolType, false, _readOnly_);}' }, + { title: "Circle", toolTip: "Circle (double tap to lock mode)", btnType: ButtonType.ToggleButton, icon: "circle", toolType: Gestures.Circle, scripts: {onClick:`{ return setActiveTool(this.toolType, false, _readOnly_);}`, onDoubleClick:`{ return setActiveTool(this.toolType, true, _readOnly_);}`} }, + { title: "Square", toolTip: "Square (double tap to lock mode)", btnType: ButtonType.ToggleButton, icon: "square", toolType: Gestures.Rectangle, scripts: {onClick:`{ return setActiveTool(this.toolType, false, _readOnly_);}`, onDoubleClick:`{ return setActiveTool(this.toolType, true, _readOnly_);}`} }, + { title: "Line", toolTip: "Line (double tap to lock mode)", btnType: ButtonType.ToggleButton, icon: "minus", toolType: Gestures.Line, scripts: {onClick:`{ return setActiveTool(this.toolType, false, _readOnly_);}`, onDoubleClick:`{ return setActiveTool(this.toolType, true, _readOnly_);}`} }, + { title: "Ink", toolTip: "Ink", btnType: ButtonType.MultiToggleButton, toolType: InkTool.Ink, scripts: {onClick:'{ return setActiveTool(this.toolType, true, _readOnly_);}' }, + subMenu: [ + { title: "Pen", toolTip: "Pen (Ctrl+P)", btnType: ButtonType.ToggleButton, icon: "pen-nib", toolType: InkInkTool.Pen, ignoreClick: true, scripts: {onClick:'{ return setActiveTool(this.toolType, true, _readOnly_);}' }}, + { title: "Highlight",toolTip: "Highlight (Ctrl+H)", btnType: ButtonType.ToggleButton, icon: "highlighter", toolType: InkInkTool.Highlight, ignoreClick: true, scripts: {onClick:'{ return setActiveTool(this.toolType, true, _readOnly_);}' }}, + { title: "Write", toolTip: "Write (Ctrl+Shift+P)", btnType: ButtonType.ToggleButton, icon: "pen", toolType: InkInkTool.Write, ignoreClick: true, scripts: {onClick:'{ return setActiveTool(this.toolType, true, _readOnly_);}' }, funcs: {hidden:"IsNoviceMode()" }}, + ]}, + { title: "Width", toolTip: "Stroke width", btnType: ButtonType.NumberSliderButton, toolType: InkProperty.StrokeWidth,ignoreClick: true, scripts: {script: '{ return setInkProperty(this.toolType, value, _readOnly_);}'}, funcs: {hidden:"!activeInkTool()"}, numBtnMin: 1, linearBtnWidth:40}, + { title: "Color", toolTip: "Stroke color", btnType: ButtonType.ColorButton, icon: "pen", toolType: InkProperty.StrokeColor,ignoreClick: true, scripts: {script: '{ return setInkProperty(this.toolType, value, _readOnly_);}'}, funcs: {hidden:"!activeInkTool()"}}, + { title: "Eraser", toolTip: "Eraser (Ctrl+E)", btnType: ButtonType.MultiToggleButton, toolType: InkTool.Eraser, scripts: {onClick:'{ return setActiveTool(this.toolType, false, _readOnly_);}' }, subMenu: [ - { title: "Stroke", toolTip: "Stroke Erase", btnType: ButtonType.ToggleButton, icon: "eraser", toolType:InkTool.StrokeEraser, ignoreClick: true, scripts: {onClick: '{ return setActiveTool(this.toolType, false, _readOnly_);}'} }, - { title: "Segment", toolTip: "Segment Erase", btnType: ButtonType.ToggleButton, icon: "xmark", toolType:InkTool.SegmentEraser,ignoreClick: true, scripts: {onClick: '{ return setActiveTool(this.toolType, false, _readOnly_);}'} }, - { title: "Radius", toolTip: "Radius Erase", btnType: ButtonType.ToggleButton, icon: "circle-xmark",toolType:InkTool.RadiusEraser, ignoreClick: true, scripts: {onClick: '{ return setActiveTool(this.toolType, false, _readOnly_);}'} }, + { title: "Stroke", toolTip: "Eraser complete strokes",btnType: ButtonType.ToggleButton, icon: "eraser", toolType:InkEraserTool.Stroke, ignoreClick: true, scripts: {onClick: '{ return setActiveTool(this.toolType, false, _readOnly_);}'}}, + { title: "Segment", toolTip: "Erase between intersections",btnType:ButtonType.ToggleButton,icon:"xmark", toolType:InkEraserTool.Segment, ignoreClick: true, scripts: {onClick: '{ return setActiveTool(this.toolType, false, _readOnly_);}'}}, + { title: "Area", toolTip: "Erase like a pencil", btnType: ButtonType.ToggleButton, icon: "circle-xmark",toolType:InkEraserTool.Radius, ignoreClick: true, scripts: {onClick: '{ return setActiveTool(this.toolType, false, _readOnly_);}'}}, ]}, - { title: "Eraser Width", toolTip: "Eraser Width", btnType: ButtonType.NumberSliderButton, toolType: "eraserWidth", ignoreClick: true, scripts: {script: '{ return setInkProperty(this.toolType, value, _readOnly_);}'}, numBtnMin: 1, funcs: {hidden:"NotRadiusEraser()"}}, - { title: "Circle", toolTip: "Circle (double tap to lock mode)", btnType: ButtonType.ToggleButton, icon: "circle", toolType: Gestures.Circle, scripts: {onClick:`{ return setActiveTool(this.toolType, false, _readOnly_);}`, onDoubleClick:`{ return setActiveTool(this.toolType, true, _readOnly_);}`} }, - { title: "Square", toolTip: "Square (double tap to lock mode)", btnType: ButtonType.ToggleButton, icon: "square", toolType: Gestures.Rectangle, scripts: {onClick:`{ return setActiveTool(this.toolType, false, _readOnly_);}`, onDoubleClick:`{ return setActiveTool(this.toolType, true, _readOnly_);}`} }, - { title: "Line", toolTip: "Line (double tap to lock mode)", btnType: ButtonType.ToggleButton, icon: "minus", toolType: Gestures.Line, scripts: {onClick:`{ return setActiveTool(this.toolType, false, _readOnly_);}`, onDoubleClick:`{ return setActiveTool(this.toolType, true, _readOnly_);}`} }, - { title: "Mask", toolTip: "Mask", btnType: ButtonType.ToggleButton, icon: "user-circle",toolType: "inkMask", scripts: {onClick:'{ return setInkProperty(this.toolType, value, _readOnly_);}'}, funcs: {hidden:"IsNoviceMode()" } }, - { title: "Labels", toolTip: "Labels", btnType: ButtonType.ToggleButton, icon: "text-width", toolType: "labels", scripts: {onClick:'{ return setInkProperty(this.toolType, value, _readOnly_);}'}, }, - { title: "Width", toolTip: "Stroke width", btnType: ButtonType.NumberSliderButton, toolType: "strokeWidth", ignoreClick: true, scripts: {script: '{ return setInkProperty(this.toolType, value, _readOnly_);}'}, numBtnMin: 1}, - { title: "Ink", toolTip: "Stroke color", btnType: ButtonType.ColorButton, icon: "pen", toolType: "strokeColor", ignoreClick: true, scripts: {script: '{ return setInkProperty(this.toolType, value, _readOnly_);}'} }, - { title: "Smart Draw", toolTip: "Draw with GPT", btnType: ButtonType.ToggleButton, icon: "user-pen", toolType: "smartdraw", scripts: {onClick:'{ return setActiveTool(this.toolType, false, _readOnly_);}'}, funcs: {hidden: "IsNoviceMode()"}}, + { title: " Size", toolTip: "Size of area pencil eraser", btnType: ButtonType.NumberSliderButton, toolType: InkProperty.EraserWidth,ignoreClick: true, scripts: {script: '{ return setInkProperty(this.toolType, value, _readOnly_);}'}, funcs: {hidden:"NotRadiusEraser()"}, numBtnMin: 1, linearBtnWidth:40}, + { title: "Mask", toolTip: "Make Stroke a Stencil Mask", btnType: ButtonType.ToggleButton, icon: "user-circle", toolType: InkProperty.Mask, scripts: {onClick:'{ return setInkProperty(this.toolType, value, _readOnly_);}'}, funcs: {hidden:"IsNoviceMode()" } }, + { title: "Labels", toolTip: "Show Labels Inside Shapes", btnType: ButtonType.ToggleButton, icon: "text-width", toolType: InkProperty.Labels, scripts: {onClick:'{ return setInkProperty(this.toolType, value, _readOnly_);}'}}, + { title: "Smart Draw", toolTip: "Draw with GPT", btnType: ButtonType.ToggleButton, icon: "user-pen", toolType: InkTool.SmartDraw, scripts: {onClick:'{ return setActiveTool(this.toolType, false, _readOnly_);}'}, funcs: {hidden: "IsNoviceMode()"}}, ]; } @@ -813,11 +818,11 @@ pie title Minerals in my tap water { title: "Pin", icon: "map-pin", toolTip: "Pin View to Trail", btnType: ButtonType.ClickButton, expertMode: false, width: 30, scripts: { onClick: 'pinWithView(altKey)'}, funcs: {hidden: "IsNoneSelected()"}}, { title: "Header", icon: "heading", toolTip: "Doc Titlebar Color", btnType: ButtonType.ColorButton, expertMode: false, ignoreClick: true, scripts: { script: 'return setHeaderColor(value, _readOnly_)'} }, { title: "Template",icon: "scroll", toolTip: "Default Note Template",btnType: ButtonType.ToggleButton, expertMode: false, toolType:DocumentType.RTF, scripts: { onClick: '{ return setDefaultTemplate(_readOnly_); }'} }, - { title: "Fill", icon: "fill-drip", toolTip: "Fill/Background Color",btnType: ButtonType.ColorButton, expertMode: false, ignoreClick: true, width: 30, scripts: { script: 'return setBackgroundColor(value, _readOnly_)'}, funcs: {hidden: "IsNoneSelected()"}}, // Only when a document is selected - { title: "Overlay", icon: "layer-group", toolTip: "Overlay", btnType: ButtonType.ToggleButton, expertMode: true, toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectedDocType(this.toolType, this.expertMode, true)'}, scripts: { onClick: '{ return toggleOverlay(_readOnly_); }'}}, // Only when floating document is selected in freeform - { title: "Back", icon: "chevron-left", toolTip: "Prev Animation Frame", btnType: ButtonType.ClickButton, expertMode: true, toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectedDocType(this.toolType, this.expertMode)'}, width: 30, scripts: { onClick: 'prevKeyFrame(_readOnly_)'}}, + { title: "Fill", icon: "fill-drip", toolTip: "Fill/Background Color",btnType: ButtonType.ColorButton, expertMode: false, ignoreClick: true, width: 30, scripts: { script: 'return setBackgroundColor(value, _readOnly_)'} }, // Only when a document is selected + { title: "Overlay", icon: "layer-group", toolTip: "Overlay", btnType: ButtonType.ToggleButton, expertMode: true, toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectedDocType(this.toolType, this.expertMode, true)'}, scripts: { onClick: '{ return toggleOverlay(_readOnly_); }'}}, // Only when floating document is selected in freeform + { title: "Back", icon: "chevron-left", toolTip: "Prev Animation Frame", btnType: ButtonType.ClickButton, expertMode: true, toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectedDocType(this.toolType, this.expertMode)'}, width: 30, scripts: { onClick: 'prevKeyFrame(_readOnly_)'}}, { title: "Num", icon:"", toolTip: "Frame # (click to toggle edit mode)",btnType: ButtonType.TextButton, expertMode: true, toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectedDocType(this.toolType, this.expertMode)', buttonText: 'selectedDocs()?.lastElement()?.currentFrame?.toString()'}, width: 20, scripts: { onClick: '{ return curKeyFrame(_readOnly_);}'}}, - { title: "Fwd", icon: "chevron-right", toolTip: "Next Animation Frame", btnType: ButtonType.ClickButton, expertMode: true, toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectedDocType(this.toolType, this.expertMode)'}, width: 30, scripts: { onClick: 'nextKeyFrame(_readOnly_)'}}, + { title: "Fwd", icon: "chevron-right", toolTip: "Next Animation Frame", btnType: ButtonType.ClickButton, expertMode: true, toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectedDocType(this.toolType, this.expertMode)'}, width: 30, scripts: { onClick: 'nextKeyFrame(_readOnly_)'}}, { title: "Filter", icon: "=", toolTip: "Filter cards by tags", subMenu: CurrentUserUtils.tagGroupTools(),ignoreClick:true, toolType:DocumentType.COL, funcs: {hidden: '!SelectedDocType(this.toolType, this.expertMode)'}, btnType: ButtonType.MultiToggleButton, width: 30, backgroundColor: doc.userVariantColor as string}, { title: "Text", icon: "Text", toolTip: "Text functions", subMenu: CurrentUserUtils.textTools(), expertMode: false, toolType:DocumentType.RTF, funcs: { linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} }, // Always available @@ -862,7 +867,7 @@ pie title Minerals in my tap water } // linear view const reqdSubMenuOpts = { ...OmitKeys(params, ["scripts", "funcs", "subMenu"]).omit, undoIgnoreFields: new List<string>(['width', "linearView_IsOpen"]), - childDontRegisterViews: true, flexGap: 0, _height: 30, ignoreClick: !params.scripts?.onClick, + childDontRegisterViews: true, flexGap: 0, _height: 30, _width: 30, ignoreClick: !params.scripts?.onClick, linearView_SubMenu: true, linearView_Expandable: true, embedContainer: menuDoc}; const items = (menutBtn?:Doc) => !menutBtn ? [] : subMenu.map(sub => this.setupContextMenuBtn(sub, menutBtn) ); @@ -986,15 +991,23 @@ pie title Minerals in my tap water Doc.noviceMode ?? (Doc.noviceMode = true); doc._showLabel ?? (doc._showLabel = true); doc.textAlign ?? (doc.textAlign = "left"); + doc.textBackgroundColor ?? (doc.textBackgroundColor = Colors.LIGHT_GRAY); doc.activeTool = InkTool.None; doc.openInkInLightbox ?? (doc.openInkInLightbox = false); - doc.activeInkHideTextLabels ?? (doc.activeInkHideTextLabels = false); - doc.activeInkColor ?? (doc.activeInkColor = "rgb(0, 0, 0)"); - doc.activeInkWidth ?? (doc.activeInkWidth = 1); - doc.activeInkBezier ?? (doc.activeInkBezier = "0"); - doc.activeFillColor ?? (doc.activeFillColor = ""); - doc.activeArrowStart ?? (doc.activeArrowStart = ""); - doc.activeArrowEnd ?? (doc.activeArrowEnd = ""); + doc.activeHideTextLabels ?? (doc.activeHideTextLabels = false); + doc[`active${InkInkTool.Pen}Color`] ?? (doc[`active${InkInkTool.Pen}Color`] = "rgb(0, 0, 0)"); + doc[`active${InkInkTool.Pen}Width`] ?? (doc[`active${InkInkTool.Pen}Width`] = 2); + doc[`active${InkInkTool.Pen}Bezier`] ?? (doc[`active${InkInkTool.Pen}Bezier`] = "0"); + doc[`active${InkInkTool.Write}Color`] ?? (doc[`active${InkInkTool.Write}Color`] = "rgb(255, 0, 0)"); + doc[`active${InkInkTool.Write}Width`] ?? (doc[`active${InkInkTool.Write}Width`] = 1); + doc[`active${InkInkTool.Write}Bezier`] ?? (doc[`active${InkInkTool.Write}Bezier`] = "0"); + doc[`active${InkInkTool.Highlight}Color`] ?? (doc[`active${InkInkTool.Highlight}Color`] = 'transparent'); + doc[`active${InkInkTool.Highlight}Width`] ?? (doc[`active${InkInkTool.Highlight}Width`] = 20); + doc[`active${InkInkTool.Highlight}Bezier`] ?? (doc[`active${InkInkTool.Highlight}Bezier`] = "0"); + doc[`active${InkInkTool.Highlight}Fill`] ?? (doc[`active${InkInkTool.Highlight}Fill`] = "rgba(0, 255, 255, 0.4)"); + doc.activeInkTool ?? (doc.activeInkTool = InkInkTool.Pen); + doc.activeEraserTool ?? (doc.activeEraserTool = InkEraserTool.Stroke); + doc.activeEraserWidth ?? (doc.activeEraserWidth = 20); doc.activeDash ?? (doc.activeDash === "0"); doc.fontSize ?? (doc.fontSize = "12px"); doc.fontFamily ?? (doc.fontFamily = "Arial"); @@ -1129,7 +1142,9 @@ pie title Minerals in my tap water } // eslint-disable-next-line prefer-arrow-callback -ScriptingGlobals.add(function NotRadiusEraser() { return Doc.ActiveTool !== InkTool.RadiusEraser; }, "is the active tool anything but the radius eraser"); +ScriptingGlobals.add(function activeInkTool() { return Doc.ActiveTool=== InkTool.Ink || DocumentView.Selected().some(dv => dv.layoutDoc.layout_isSvg); }, "is a pen tool or an ink stroke active"); +// eslint-disable-next-line prefer-arrow-callback +ScriptingGlobals.add(function NotRadiusEraser() { return Doc.ActiveTool !== InkTool.Eraser || Doc.ActiveEraser !== InkEraserTool.Radius; }, "is the active tool anything but the radius eraser"); // eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(function MySharedDocs() { return Doc.MySharedDocs; }, "document containing all shared Docs"); // eslint-disable-next-line prefer-arrow-callback diff --git a/src/client/util/InteractionUtils.tsx b/src/client/util/InteractionUtils.tsx index 4231c2ca8..ca1cb8014 100644 --- a/src/client/util/InteractionUtils.tsx +++ b/src/client/util/InteractionUtils.tsx @@ -109,7 +109,7 @@ export namespace InteractionUtils { dash: string | undefined, scalexIn: number, scaleyIn: number, - shape: Gestures, + shape: Gestures | undefined, pevents: Property.PointerEvents, opacity: number, nodefs: boolean, @@ -136,7 +136,7 @@ export namespace InteractionUtils { const arrowLengthFactor = 5 * (markerScale || 0.5); const arrowNotchFactor = 2 * (markerScale || 0.5); return ( - <svg fill={color} style={{ transition: 'inherit' }} onPointerDown={downHdlr}> + <svg fill={color || 'transparent'} style={{ transition: 'inherit' }} onPointerDown={downHdlr}> {' '} {/* setting the svg fill sets the arrowStart fill */} {nodefs ? null : ( @@ -186,7 +186,7 @@ export namespace InteractionUtils { opacity: 1.0, // opacity: strokeWidth !== width ? 0.5 : undefined, pointerEvents: pevents === 'all' ? 'visiblePainted' : pevents, - stroke: color ?? 'rgb(0, 0, 0)', + stroke: (color ?? 'rgb(0, 0, 0)') || 'transparent', strokeWidth, strokeLinecap: strokeLineCap, strokeDasharray: dashArray, diff --git a/src/client/util/SettingsManager.tsx b/src/client/util/SettingsManager.tsx index 9200d68db..628097ca8 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; }); @@ -269,9 +268,9 @@ export class SettingsManager extends React.Component<object> { formLabelPlacement="right" toggleType={ToggleType.SWITCH} onClick={() => { - Doc.UserDoc().activeInkHideTextLabels = !Doc.UserDoc().activeInkHideTextLabels; + Doc.UserDoc().activeHideTextLabels = !Doc.UserDoc().activeHideTextLabels; }} - toggleStatus={BoolCast(Doc.UserDoc().activeInkHideTextLabels)} + toggleStatus={BoolCast(Doc.UserDoc().activeHideTextLabels)} size={Size.XSMALL} color={SettingsManager.userColor} /> @@ -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/util/SnappingManager.ts b/src/client/util/SnappingManager.ts index 5f6c7d9ac..2a150dc5a 100644 --- a/src/client/util/SnappingManager.ts +++ b/src/client/util/SnappingManager.ts @@ -1,4 +1,5 @@ import { observable, action, runInAction, makeObservable } from 'mobx'; +import { Gestures } from '../../pen-gestures/GestureTypes'; export enum freeformScrollMode { Pan = 'pan', @@ -29,6 +30,8 @@ export class SnappingManager { @observable _propertyWid: number = 0; @observable _printToConsole: boolean = false; @observable _hideDecorations: boolean = false; + @observable _keepGestureMode: boolean = false; // for whether primitive selection enters a one-shot or persistent mode + @observable _inkShape: Gestures | undefined = undefined; private constructor() { SnappingManager._manager = this; @@ -61,6 +64,8 @@ export class SnappingManager { public static get PropertiesWidth(){ return this.Instance._propertyWid; } // prettier-ignore public static get PrintToConsole() { return this.Instance._printToConsole; } // prettier-ignore public static get HideDecorations(){ return this.Instance._hideDecorations; } // prettier-ignore + public static get KeepGestureMode(){ return this.Instance._keepGestureMode; } // prettier-ignore + public static get InkShape() { return this.Instance._inkShape; } // prettier-ignore public static SetLongPress = (press: boolean) => runInAction(() => {this.Instance._longPress = press}); // prettier-ignore public static SetShiftKey = (down: boolean) => runInAction(() => {this.Instance._shiftKey = down}); // prettier-ignore @@ -78,6 +83,8 @@ export class SnappingManager { public static SetPropertiesWidth= (wid:number) =>runInAction(() => {this.Instance._propertyWid = wid}); // prettier-ignore public static SetPrintToConsole = (state:boolean) =>runInAction(() => {this.Instance._printToConsole = state}); // prettier-ignore public static SetHideDecorations= (state:boolean) =>runInAction(() => {this.Instance._hideDecorations = state}); // prettier-ignore + public static SetKeepGestureMode= (state:boolean) =>runInAction(() => {this.Instance._keepGestureMode = state}); // prettier-ignore + public static SetInkShape = (shape?:Gestures)=>runInAction(() => {this.Instance._inkShape = shape}); // prettier-ignore public static userColor: string | undefined; public static userVariantColor: string | undefined; diff --git a/src/client/views/FilterPanel.tsx b/src/client/views/FilterPanel.tsx index 11425e477..99738052d 100644 --- a/src/client/views/FilterPanel.tsx +++ b/src/client/views/FilterPanel.tsx @@ -102,7 +102,7 @@ const HotKeyIconButton: React.FC<HotKeyButtonProps> = observer(({ hotKey /*, sel return ( <div - className={`filterHotKey-button`} + className="filterHotKey-button" onClick={e => { e.stopPropagation(); state.startEditing(); diff --git a/src/client/views/GestureOverlay.tsx b/src/client/views/GestureOverlay.tsx index afeecaa63..2d6cd03e0 100644 --- a/src/client/views/GestureOverlay.tsx +++ b/src/client/views/GestureOverlay.tsx @@ -2,9 +2,9 @@ import * as fitCurve from 'fit-curve'; import { action, computed, makeObservable, observable, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; -import { returnEmptyFilter, returnEmptyString, returnFalse, setupMoveUpEvents } from '../../ClientUtils'; +import { setupMoveUpEvents } from '../../ClientUtils'; import { emptyFunction, intersectRect } from '../../Utils'; -import { Doc, Opt, returnEmptyDoclist } from '../../fields/Doc'; +import { Doc } from '../../fields/Doc'; import { InkData, InkField, InkTool } from '../../fields/InkField'; import { NumCast } from '../../fields/Types'; import { Gestures } from '../../pen-gestures/GestureTypes'; @@ -14,27 +14,25 @@ import { DocumentType } from '../documents/DocumentTypes'; import { Docs } from '../documents/Documents'; import { InteractionUtils } from '../util/InteractionUtils'; import { ScriptingGlobals } from '../util/ScriptingGlobals'; -import { Transform } from '../util/Transform'; +import { SnappingManager } from '../util/SnappingManager'; import { undoable } from '../util/UndoManager'; import './GestureOverlay.scss'; import { InkingStroke } from './InkingStroke'; import { ObservableReactComponent } from './ObservableReactComponent'; -import { returnEmptyDocViewList } from './StyleProvider'; import { CollectionFreeFormView } from './collections/collectionFreeForm'; import { - ActiveArrowEnd, - ActiveArrowScale, - ActiveArrowStart, - ActiveDash, - ActiveFillColor, - ActiveInkBezierApprox, + ActiveInkArrowEnd, + ActiveInkArrowScale, + ActiveInkArrowStart, ActiveInkColor, + ActiveInkDash, + ActiveInkFillColor, ActiveInkWidth, DocumentView, - SetActiveArrowStart, - SetActiveDash, - SetActiveFillColor, + SetActiveInkArrowStart, SetActiveInkColor, + SetActiveInkDash, + SetActiveInkFillColor, SetActiveInkWidth, } from './nodes/DocumentView'; export enum ToolglassTools { @@ -57,18 +55,14 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil // eslint-disable-next-line no-use-before-define static Instances: GestureOverlay[] = []; - @observable public InkShape: Opt<Gestures> = undefined; @observable public SavedColor?: string = undefined; @observable public SavedWidth?: number = undefined; @observable public Tool: ToolglassTools = ToolglassTools.None; - @observable public KeepPrimitiveMode = false; // for whether primitive selection enters a one-shot or persistent mode @observable private _thumbX?: number = undefined; @observable private _thumbY?: number = undefined; @observable private _pointerY?: number = undefined; @observable private _points: { X: number; Y: number }[] = []; - @observable private _strokes: InkData[] = []; - @observable private _palette?: JSX.Element = undefined; @observable private _clipboardDoc?: JSX.Element = undefined; @computed private get height(): number { @@ -95,8 +89,9 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil } @action onPointerDown = (e: React.PointerEvent) => { + (document.activeElement as HTMLElement)?.blur(); if (!(e.target as HTMLElement)?.className?.toString().startsWith('lm_')) { - if ([InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool)) { + if (Doc.ActiveTool === InkTool.Ink) { this._points.push({ X: e.clientX, Y: e.clientY }); setupMoveUpEvents(this, e, this.onPointerMove, this.onPointerUp, emptyFunction); } @@ -122,13 +117,9 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil }; @action primCreated() { - if (!this.KeepPrimitiveMode) { - this.InkShape = undefined; - // get out of ink mode after each stroke= - // if (Doc.ActiveTool === InkTool.Highlighter && GestureOverlay.Instance.SavedColor) SetActiveInkColor(GestureOverlay.Instance.SavedColor); + if (!SnappingManager.KeepGestureMode) { + SnappingManager.SetInkShape(undefined); Doc.ActiveTool = InkTool.None; - // SetActiveArrowStart('none'); - // SetActiveArrowEnd('none'); } } /** @@ -244,15 +235,16 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil this.dispatchGesture(Gestures.Stroke); }; @action - onPointerUp = () => { + onPointerUp = (e: PointerEvent) => { const ffView = DocumentView.DownDocView?.ComponentView instanceof CollectionFreeFormView && DocumentView.DownDocView.ComponentView; + const downView = DocumentView.DownDocView; DocumentView.DownDocView = undefined; if (this._points.length > 1) { const B = this.svgBounds; const points = this._points.map(p => ({ X: p.X - B.left, Y: p.Y - B.top })); const { Name, Score } = - (this.InkShape - ? new Result(this.InkShape, 1, Date.now) + (SnappingManager.InkShape + ? new Result(SnappingManager.InkShape, 1, Date.now) : Doc.UserDoc().recognizeGestures && points.length > 2 ? GestureUtils.GestureRecognizer.Recognize([points]) : undefined) ?? @@ -286,7 +278,11 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil this.dryInk(); } } + } else { + (downView?.ComponentView as CollectionFreeFormView)?._marqueeViewRef?.current?.setPreviewCursor?.(this._points[0].X, this._points[0].Y, false, false, undefined); + e.preventDefault(); } + this.primCreated(); this._points.length = 0; }; /** @@ -552,97 +548,38 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil B.bottom += width / 2; B.width += width; B.height += width; - const fillColor = ActiveFillColor(); + const fillColor = ActiveInkFillColor(); const strokeColor = fillColor && fillColor !== 'transparent' ? fillColor : ActiveInkColor(); return [ this.props.children, - this._palette, - [ - this._strokes.map((l, i) => { - const b = { left: -20000, right: 20000, top: -20000, bottom: 20000, width: 40000, height: 40000 }; // this.getBounds(l, true); - return ( - <svg key={i} width={b.width} height={b.height} style={{ top: 0, left: 0, transform: `translate(${b.left}px, ${b.top}px)`, pointerEvents: 'none', position: 'absolute', zIndex: 30000, overflow: 'visible' }}> - {InteractionUtils.CreatePolyline( - l, - b.left, - b.top, - strokeColor, - width, - width, - 'miter', - 'round', - ActiveInkBezierApprox(), - 'none' /* ActiveFillColor() */, - ActiveArrowStart(), - ActiveArrowEnd(), - ActiveArrowScale(), - ActiveDash(), - 1, - 1, - this.InkShape as Gestures, - 'none', - 1.0, - false - )} - </svg> - ); - }), - this._points.length <= 1 ? null : ( - <svg key="svg" width={B.width} height={B.height} style={{ top: 0, left: 0, transform: `translate(${B.left}px, ${B.top}px)`, pointerEvents: 'none', position: 'absolute', zIndex: 30000, overflow: 'visible' }}> - {InteractionUtils.CreatePolyline( - this._points.map(p => ({ X: p.X - (rect?.x || 0), Y: p.Y - (rect?.y || 0) })), - B.left, - B.top, - ActiveInkColor(), - width, - width, - 'miter', - 'round', - '', - 'none' /* ActiveFillColor() */, - ActiveArrowStart(), - ActiveArrowEnd(), - ActiveArrowScale(), - ActiveDash(), - 1, - 1, - this.InkShape as Gestures, - 'none', - 1.0, - false - )} - </svg> - ), - ], + this._points.length <= 1 ? null : ( + <svg key="svg" width={B.width} height={B.height} style={{ top: 0, left: 0, transform: `translate(${B.left}px, ${B.top}px)`, pointerEvents: 'none', position: 'absolute', zIndex: 30000, overflow: 'visible' }}> + {InteractionUtils.CreatePolyline( + this._points.map(p => ({ X: p.X - (rect?.x || 0), Y: p.Y - (rect?.y || 0) })), + B.left, + B.top, + strokeColor, + width, + width, + 'miter', + 'round', + '', + 'none' /* ActiveFillColor() */, + ActiveInkArrowStart(), + ActiveInkArrowEnd(), + ActiveInkArrowScale(), + ActiveInkDash(), + 1, + 1, + SnappingManager.InkShape, + 'none', + 1.0, + false + )} + </svg> + ), ]; } - screenToLocalTransform = () => new Transform(-(this._thumbX ?? 0), -(this._thumbY ?? 0) + this.height, 1); - return300 = () => 300; - @action - public openFloatingDoc = (doc: Doc) => { - this._clipboardDoc = ( - <DocumentView - Document={doc} - addDocument={undefined} - addDocTab={returnFalse} - pinToPres={emptyFunction} - removeDocument={undefined} - ScreenToLocalTransform={this.screenToLocalTransform} - PanelWidth={this.return300} - PanelHeight={this.return300} - isDocumentActive={returnFalse} - isContentActive={returnFalse} - renderDepth={0} - styleProvider={returnEmptyString} - containerViewPath={returnEmptyDocViewList} - focus={emptyFunction} - whenChildContentsActiveChanged={emptyFunction} - childFiltersByRanges={returnEmptyFilter} - childFilters={returnEmptyFilter} - searchFilterDocs={returnEmptyDoclist} - /> - ); - }; @action public closeFloatingDoc = () => { @@ -689,10 +626,10 @@ ScriptingGlobals.add(function setPen(width: string, color: string, fill: string, SetActiveInkColor(color); GestureOverlay.Instance.SavedWidth = ActiveInkWidth(); SetActiveInkWidth(width); - SetActiveFillColor(fill); - SetActiveArrowStart(arrowStart); - SetActiveArrowStart(arrowEnd); - SetActiveDash(dash); + SetActiveInkFillColor(fill); + SetActiveInkArrowStart(arrowStart); + SetActiveInkArrowStart(arrowEnd); + SetActiveInkDash(dash); }); }); // eslint-disable-next-line prefer-arrow-callback diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts index d7d8e9506..f16338361 100644 --- a/src/client/views/GlobalKeyHandler.ts +++ b/src/client/views/GlobalKeyHandler.ts @@ -2,7 +2,7 @@ import { random } from 'lodash'; import { action } from 'mobx'; import { Doc, DocListCast } from '../../fields/Doc'; import { Id } from '../../fields/FieldSymbols'; -import { InkTool } from '../../fields/InkField'; +import { InkInkTool, InkTool } from '../../fields/InkField'; import { ScriptField } from '../../fields/ScriptField'; import { Cast, PromiseValue } from '../../fields/Types'; import { GoogleAuthenticationManager } from '../apis/GoogleAuthenticationManager'; @@ -280,10 +280,10 @@ export class KeyManager { } break; case 'e': - Doc.ActiveTool = [InkTool.StrokeEraser, InkTool.SegmentEraser, InkTool.RadiusEraser].includes(Doc.ActiveTool) ? InkTool.None : InkTool.StrokeEraser; + Doc.ActiveTool = Doc.ActiveTool === InkTool.Eraser ? InkTool.None : InkTool.Eraser; break; case 'p': - Doc.ActiveTool = Doc.ActiveTool === InkTool.Pen ? InkTool.None : InkTool.Pen; + Doc.ActiveTool = Doc.ActiveTool === InkTool.Ink ? InkTool.None : InkTool.Ink; break; case 'r': preventDefault = false; @@ -378,7 +378,8 @@ export class KeyManager { UndoManager.Redo(); break; case 'p': - Doc.ActiveTool = InkTool.Write; + Doc.ActiveInk = InkInkTool.Write; + Doc.ActiveTool = InkTool.Ink; break; default: } diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx index 270266a94..5199eb02b 100644 --- a/src/client/views/InkingStroke.tsx +++ b/src/client/views/InkingStroke.tsx @@ -470,14 +470,13 @@ export class InkingStroke extends ViewBoxAnnotatableComponent<FieldViewProps>() className="inkStroke" style={{ transform: isInkMask ? `rotate(-${NumCast(this._props.LocalRotation?.() ?? 0)}deg) translate(${InkingStroke.MaskDim / 2}px, ${InkingStroke.MaskDim / 2}px)` : undefined, - // mixBlendMode: this.layoutDoc.tool === InkTool.Highlighter ? 'multiply' : 'unset', cursor: this._props.isSelected() ? 'default' : undefined, }} {...interactions}> {clickableLine(this.onPointerDown, isInkMask)} {isInkMask ? null : inkLine} </svg> - {!closed || this.dataDoc[this.fieldKey + '_showLabel'] === false || (!RTFCast(this.dataDoc.text)?.Text && !this.dataDoc[this.fieldKey + '_showLabel'] && (!this._props.isSelected() || Doc.UserDoc().activeInkHideTextLabels)) ? null : ( + {!closed || this.dataDoc[this.fieldKey + '_showLabel'] === false || (!RTFCast(this.dataDoc.text)?.Text && !this.dataDoc[this.fieldKey + '_showLabel'] && (!this._props.isSelected() || Doc.UserDoc().activeHideTextLabels)) ? null : ( <div className="inkStroke-text" style={{ diff --git a/src/client/views/LightboxView.tsx b/src/client/views/LightboxView.tsx index a543b4875..3a8a38073 100644 --- a/src/client/views/LightboxView.tsx +++ b/src/client/views/LightboxView.tsx @@ -214,7 +214,7 @@ export class LightboxView extends ObservableReactComponent<LightboxViewProps> { // if (this._showPalette === false) AnnotationPalette.Instance.resetPalette(true); }; togglePen = () => { - Doc.ActiveTool = Doc.ActiveTool === InkTool.Pen ? InkTool.None : InkTool.Pen; + Doc.ActiveTool = Doc.ActiveTool === InkTool.Ink ? InkTool.None : InkTool.Ink; }; toggleExplore = () => SnappingManager.SetExploreMode(!SnappingManager.ExploreMode); @@ -332,7 +332,7 @@ export class LightboxView extends ObservableReactComponent<LightboxViewProps> { {toggleBtn('lightboxView-navBtn', 'toggle reading view', BoolCast(this._doc?._layout_fitWidth), 'book-open', 'book', this.toggleFitWidth)} {toggleBtn('lightboxView-tabBtn', 'open document in a tab', false, 'file-export', '', this.downloadDoc)} {toggleBtn('lightboxView-paletteBtn', 'toggle annotation palette', this._showPalette === true, 'palette', '', this.togglePalette)} - {toggleBtn('lightboxView-penBtn', 'toggle pen annotation', Doc.ActiveTool === InkTool.Pen, 'pen', '', this.togglePen)} + {toggleBtn('lightboxView-penBtn', 'toggle pen annotation', Doc.ActiveTool === InkTool.Ink, 'pen', '', this.togglePen)} {toggleBtn('lightboxView-exploreBtn', 'toggle navigate only mode', SnappingManager.ExploreMode, 'globe-americas', '', this.toggleExplore)} </div> ); 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/StyleProp.ts b/src/client/views/StyleProp.ts index 44d3bf757..56367e70b 100644 --- a/src/client/views/StyleProp.ts +++ b/src/client/views/StyleProp.ts @@ -19,7 +19,9 @@ export enum StyleProp { FontColor = 'fontColor', // color o tet FontSize = 'fontSize', // size of text font FontFamily = 'fontFamily', // font family of text - FontWeight = 'fontWeight', // font weight of text + FontWeight = 'fontWeight', // font weight of text (eg bold) + FontStyle = 'fontStyle', // font style of text (eg italic) + FontDecoration = 'fontDecoration', // text decoration of text (eg underline) Highlighting = 'highlighting', // border highlighting ContextMenuItems = 'contextMenuItems', // menu items to add to context menu AnchorMenuItems = 'anchorMenuItems', diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx index 8859f6464..3a5f57908 100644 --- a/src/client/views/StyleProvider.tsx +++ b/src/client/views/StyleProvider.tsx @@ -25,6 +25,7 @@ import { styleProviderQuiz } from './StyleProviderQuiz'; import { StyleProp } from './StyleProp'; import './StyleProvider.scss'; import { TagsView } from './TagsView'; +import { InkInkTool } from '../../fields/InkField'; function toggleLockedPosition(doc: Doc) { UndoManager.RunInBatch(() => Doc.toggleLockedPosition(doc), 'toggleBackground'); @@ -171,7 +172,9 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps & case StyleProp.FontSize: return StrCast(doc?.[fieldKey + 'fontSize'], StrCast(Doc.UserDoc().fontSize)); case StyleProp.FontFamily: return StrCast(doc?.[fieldKey + 'fontFamily'], StrCast(Doc.UserDoc().fontFamily)); case StyleProp.FontWeight: return StrCast(doc?.[fieldKey + 'fontWeight'], StrCast(Doc.UserDoc().fontWeight)); - case StyleProp.FillColor: return StrCast(doc?._fillColor, StrCast(doc?.fillColor, StrCast(doc?.backgroundColor, 'transparent'))); + case StyleProp.FontStyle: return StrCast(doc?.[fieldKey + 'fontStyle'], StrCast(Doc.UserDoc().fontStyle)); + case StyleProp.FontDecoration:return StrCast(doc?.[fieldKey + 'fontDecoration'], StrCast(Doc.UserDoc().fontDecoration)); + case StyleProp.FillColor: return StrCast(doc?._fillColor, StrCast(doc?.fillColor, StrCast(doc?.backgroundColor, StrCast(Doc.UserDoc()[Doc.ActiveInk === InkInkTool.Highlight ? "inkHighlighterColor": "inkFillColor"], 'transparent')))); case StyleProp.ShowCaption: return hideCaptions || doc?._type_collection === CollectionViewType.Carousel ? undefined: StrCast(doc?._layout_showCaption); case StyleProp.TitleHeight: return Math.min(4,(docView?.().screenToViewTransform().Scale ?? 1)) * NumCast(Doc.UserDoc().headerHeight,30); case StyleProp.ShowTitle: return ( @@ -251,7 +254,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 +316,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 3a5cb5f37..5e6c40400 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/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index e1786d2c9..3e8138390 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -453,7 +453,7 @@ export class CollectionDockingView extends CollectionSubView() { } } } - if (!InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE) && !InteractionUtils.IsType(e, InteractionUtils.PENTYPE) && ![InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool)) { + if (!InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE) && !InteractionUtils.IsType(e, InteractionUtils.PENTYPE) && Doc.ActiveTool !== InkTool.Ink) { e.stopPropagation(); } }; 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/collectionFreeForm/CollectionFreeFormInfoUI.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormInfoUI.tsx index 5d8373fc7..8b9a3e0ec 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormInfoUI.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormInfoUI.tsx @@ -29,7 +29,7 @@ export class CollectionFreeFormInfoUI extends ObservableReactComponent<Collectio } _firstDocPos = { x: 0, y: 0 }; - constructor(props: any) { + constructor(props: CollectionFreeFormInfoUIProps) { super(props); makeObservable(this); this._currState = this.setupStates(); @@ -163,7 +163,7 @@ export class CollectionFreeFormInfoUI extends ObservableReactComponent<Collectio return presentDocs; }], // eslint-disable-next-line no-use-before-define - activePen: [() => activeTool() === InkTool.Pen, () => penMode], + activePen: [() => activeTool() === InkTool.Ink, () => penMode], }, 'documentation.png', () => TopBar.Instance.FlipDocumentationIcon() @@ -187,7 +187,7 @@ export class CollectionFreeFormInfoUI extends ObservableReactComponent<Collectio const penMode = InfoState('You\'re in pen mode. Click and drag to draw your first masterpiece.', { // activePen: [() => activeTool() === InkTool.Eraser, () => eraserMode], - activePen: [() => activeTool() !== InkTool.Pen, () => viewedLink], + activePen: [() => activeTool() !== InkTool.Ink, () => viewedLink], }); // prettier-ignore // const eraserMode = InfoState('You\'re in eraser mode. Say goodbye to your first masterpiece.', { diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index ff711314b..f2a5780c5 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -10,7 +10,7 @@ import { DateField } from '../../../../fields/DateField'; import { Doc, DocListCast, Field, FieldType, Opt, StrListCast } from '../../../../fields/Doc'; import { DocData, Height, Width } from '../../../../fields/DocSymbols'; import { Id } from '../../../../fields/FieldSymbols'; -import { InkData, InkField, InkTool, Segment } from '../../../../fields/InkField'; +import { InkData, InkEraserTool, InkField, InkInkTool, InkTool, Segment } from '../../../../fields/InkField'; import { List } from '../../../../fields/List'; import { RichTextField } from '../../../../fields/RichTextField'; import { listSpec } from '../../../../fields/Schema'; @@ -36,7 +36,20 @@ import { ContextMenu } from '../../ContextMenu'; import { InkingStroke } from '../../InkingStroke'; import { CollectionFreeFormDocumentView } from '../../nodes/CollectionFreeFormDocumentView'; import { SchemaCSVPopUp } from '../../nodes/DataVizBox/SchemaCSVPopUp'; -import { ActiveArrowEnd, ActiveArrowStart, ActiveDash, ActiveEraserWidth, ActiveFillColor, ActiveInkBezierApprox, ActiveInkColor, ActiveInkWidth, ActiveIsInkMask, DocumentView, SetActiveInkColor, SetActiveInkWidth } from '../../nodes/DocumentView'; +import { + ActiveInkArrowEnd, + ActiveInkArrowStart, + ActiveInkDash, + ActiveEraserWidth, + ActiveInkFillColor, + ActiveInkBezierApprox, + ActiveInkColor, + ActiveInkWidth, + ActiveIsInkMask, + DocumentView, + SetActiveInkColor, + SetActiveInkWidth, +} from '../../nodes/DocumentView'; import { FieldViewProps } from '../../nodes/FieldView'; import { FocusViewOptions } from '../../nodes/FocusViewOptions'; import { FormattedTextBox } from '../../nodes/formattedText/FormattedTextBox'; @@ -497,13 +510,9 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection // prettier-ignore const hit = this._clusters.handlePointerDown(this.screenToFreeformContentsXf.transformPoint(e.clientX, e.clientY)); switch (Doc.ActiveTool) { - case InkTool.Highlighter: - case InkTool.Write: - case InkTool.Pen: + case InkTool.Ink: break; // the GestureOverlay handles ink stroke input -- either as gestures, or drying as ink strokes that are added to document views - case InkTool.StrokeEraser: - case InkTool.SegmentEraser: - case InkTool.RadiusEraser: + case InkTool.Eraser: this._batch = UndoManager.StartBatch('collectionErase'); this._eraserPts.length = 0; setupMoveUpEvents(this, e, this.onEraserMove, this.onEraserUp, this.onEraserClick, hit !== -1); @@ -543,7 +552,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection const { points } = ge; const B = this.screenToFreeformContentsXf.transformBounds(ge.bounds.left, ge.bounds.top, ge.bounds.width, ge.bounds.height); const inkDoc = this.createInkDoc(points, B); - if (Doc.ActiveTool === InkTool.Write) { + if (Doc.ActiveInk === InkInkTool.Highlight) inkDoc[DocData].backgroundColor = 'transparent'; + if (Doc.ActiveInk === InkInkTool.Write) { this.unprocessedDocs.push(inkDoc); CollectionFreeFormView.collectionsWithUnprocessedInk.add(this); } @@ -606,7 +616,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection const currPoint = { X: e.clientX, Y: e.clientY }; this._eraserPts.push([currPoint.X, currPoint.Y]); this._eraserPts = this._eraserPts.slice(Math.max(0, this._eraserPts.length - 5)); - if (Doc.ActiveTool === InkTool.RadiusEraser) { + if (Doc.ActiveEraser === InkEraserTool.Radius) { const strokeMap: Map<DocumentView, number[]> = this.getRadiusEraserIntersections({ X: currPoint.X - delta[0], Y: currPoint.Y - delta[1] }, currPoint); strokeMap.forEach((intersects, stroke) => { if (!this._deleteList.includes(stroke)) { @@ -614,13 +624,16 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection SetActiveInkWidth(StrCast(stroke.Document.stroke_width?.toString()) || '1'); SetActiveInkColor(StrCast(stroke.Document.color?.toString()) || 'black'); const segments = this.radiusErase(stroke, intersects.sort()); - segments?.forEach(segment => - this.forceStrokeGesture( - e, - Gestures.Stroke, - segment.reduce((data, curve) => [...data, ...curve.points.map(p => stroke.ComponentView?.ptToScreen?.({ X: p.x, Y: p.y }) ?? { X: 0, Y: 0 })], [] as PointData[]) - ) - ); + segments?.forEach(segment => { + const points = segment.reduce((data, curve) => [...data, ...curve.points.map(p => stroke.ComponentView?.ptToScreen?.({ X: p.x, Y: p.y }) ?? { X: 0, Y: 0 })], [] as PointData[]); + const bounds = InkField.getBounds(points); + const B = this.screenToFreeformContentsXf.transformBounds(bounds.left, bounds.top, bounds.width, bounds.height); + const inkDoc = this.createInkDoc(points, B); + ['color', 'fillColor', 'stroke_width', 'stroke_dash', 'stroke_bezier'].forEach(field => { + inkDoc[DocData][field] = stroke.dataDoc[field]; + }); + this.addDocument(inkDoc); + }); } stroke.layoutDoc.opacity = 0; stroke.layoutDoc.dontIntersect = true; @@ -632,7 +645,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection SetActiveInkWidth(StrCast(intersect.inkView.Document.stroke_width?.toString()) || '1'); SetActiveInkColor(StrCast(intersect.inkView.Document.color?.toString()) || 'black'); // create a new curve by appending all curves of the current segment together in order to render a single new stroke. - if (Doc.ActiveTool !== InkTool.StrokeEraser) { + if (Doc.ActiveEraser !== InkEraserTool.Stroke) { // this._eraserLock++; const segments = this.segmentErase(intersect.inkView, intersect.t); // intersect.t is where the eraser intersected the ink stroke - want to remove the segment that starts at the intersection just before this t value and goes to the one just after it const newStrokes = segments?.map(segment => { @@ -1195,14 +1208,14 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection y: B.y - inkWidth / 2, _width: B.width + inkWidth, _height: B.height + inkWidth, - stroke_showLabel: BoolCast(Doc.UserDoc().activeInkHideTextLabels)}, // prettier-ignore + stroke_showLabel: !BoolCast(Doc.UserDoc().activeHideTextLabels)}, // prettier-ignore inkWidth, ActiveInkColor(), ActiveInkBezierApprox(), - ActiveFillColor(), - ActiveArrowStart(), - ActiveArrowEnd(), - ActiveDash(), + ActiveInkFillColor(), + ActiveInkArrowStart(), + ActiveInkArrowEnd(), + ActiveInkDash(), ActiveIsInkMask() ); }; @@ -1235,14 +1248,14 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection y: B.y - inkWidth / 2, _width: B.width + inkWidth, _height: B.height + inkWidth, - stroke_showLabel: BoolCast(Doc.UserDoc().activeInkHideTextLabels)}, // prettier-ignore + stroke_showLabel: BoolCast(Doc.UserDoc().activeHideTextLabels)}, // prettier-ignore inkWidth, opts.autoColor ? stroke[1] : ActiveInkColor(), ActiveInkBezierApprox(), - stroke[2] === 'none' ? ActiveFillColor() : stroke[2], - ActiveArrowStart(), - ActiveArrowEnd(), - ActiveDash(), + stroke[2] === 'none' ? ActiveInkFillColor() : stroke[2], + ActiveInkArrowStart(), + ActiveInkArrowEnd(), + ActiveInkDash(), ActiveIsInkMask() ); this._drawing.push(inkDoc); @@ -2195,7 +2208,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection width: `${100 / this.nativeDimScaling}%`, height: this._props.getScrollHeight?.() ?? `${100 / this.nativeDimScaling}%`, }}> - {Doc.ActiveTool === InkTool.RadiusEraser && this._showEraserCircle && ( + {Doc.ActiveTool === InkTool.Eraser && Doc.ActiveEraser === InkEraserTool.Radius && this._showEraserCircle && ( <div onPointerMove={this.onCursorMove} style={{ diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx index c865c681d..ddc50871d 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx @@ -690,7 +690,7 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps }} style={{ overflow: StrCast(this._props.Document._overflow), - cursor: [InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool) || this._visible ? 'crosshair' : 'pointer', + cursor: Doc.ActiveTool === InkTool.Ink || this._visible ? 'crosshair' : 'pointer', }} onDragOver={e => e.preventDefault()} onScroll={e => { 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..5a17bc8f6 100644 --- a/src/client/views/global/globalScripts.ts +++ b/src/client/views/global/globalScripts.ts @@ -3,7 +3,7 @@ import { Colors } from 'browndash-components'; import { runInAction } from 'mobx'; import { Doc, DocListCast, Opt, StrListCast } from '../../../fields/Doc'; import { DocData } from '../../../fields/DocSymbols'; -import { InkTool } from '../../../fields/InkField'; +import { InkEraserTool, InkInkTool, InkProperty, InkTool } from '../../../fields/InkField'; import { List } from '../../../fields/List'; import { BoolCast, Cast, NumCast, StrCast } from '../../../fields/Types'; import { WebField } from '../../../fields/URLField'; @@ -16,21 +16,20 @@ import { SnappingManager } from '../../util/SnappingManager'; import { UndoManager, undoable } from '../../util/UndoManager'; import { GestureOverlay } from '../GestureOverlay'; import { InkTranscription } from '../InkTranscription'; -import { InkingStroke } from '../InkingStroke'; import { PropertiesView } from '../PropertiesView'; import { CollectionFreeFormView } from '../collections/collectionFreeForm'; import { CollectionFreeFormDocumentView } from '../nodes/CollectionFreeFormDocumentView'; import { ActiveEraserWidth, - ActiveFillColor, + ActiveInkFillColor, ActiveInkColor, - ActiveInkHideTextLabels, + ActiveHideTextLabels, ActiveInkWidth, ActiveIsInkMask, DocumentView, - SetActiveFillColor, + SetActiveInkFillColor, SetActiveInkColor, - SetActiveInkHideTextLabels, + SetactiveHideTextLabels, SetActiveInkWidth, SetActiveIsInkMask, SetEraserWidth, @@ -60,20 +59,21 @@ ScriptingGlobals.add(function setView(view: string, getSelected: boolean) { // eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(function setBackgroundColor(color?: string, checkResult?: boolean) { const selectedViews = DocumentView.Selected(); + const selectedDoc = selectedViews.lastElement()?.Document; + const defaultFill = selectedDoc?._layout_isSvg ? () => StrCast(selectedDoc[DocData].fillColor) : !Doc.ActiveTool || Doc.ActiveTool === InkTool.None ? () => StrCast(Doc.UserDoc().textBackgroundColor, 'transparent') : () => ActiveInkFillColor(); + const setDefaultFill = !Doc.ActiveTool || Doc.ActiveTool === InkTool.None ? (c: string) => { Doc.UserDoc().textBackgroundColor = c; }: SetActiveInkFillColor; // prettier-ignore if (Doc.ActiveTool !== InkTool.None && !selectedViews.lastElement()?.Document._layout_isSvg) { - if (checkResult) { - return ActiveFillColor(); - } - SetActiveFillColor(color ?? 'transparent'); + if (checkResult) return defaultFill(); + setDefaultFill(color ?? 'transparent'); } else if (selectedViews.length) { if (checkResult) { const selView = selectedViews.lastElement(); const fieldKey = selView.Document._layout_isSvg ? 'fillColor' : 'backgroundColor'; const layoutFrameNumber = Cast(selView.containerViewPath?.().lastElement()?.Document?._currentFrame, 'number'); // frame number that container is at which determines layout frame values const contentFrameNumber = Cast(selView.Document?._currentFrame, 'number', layoutFrameNumber ?? null); // frame number that content is at which determines what content is displayed - return CollectionFreeFormDocumentView.getStringValues(selView?.Document, contentFrameNumber)[fieldKey] ?? 'transparent'; + return CollectionFreeFormDocumentView.getStringValues(selView?.Document, contentFrameNumber)[fieldKey] || defaultFill(); } - selectedViews.some(dv => dv.ComponentView instanceof InkingStroke) && SetActiveFillColor(color ?? 'transparent'); + setDefaultFill(color ?? 'transparent'); selectedViews.forEach(dv => { const fieldKey = dv.Document._layout_isSvg ? 'fillColor' : 'backgroundColor'; const layoutFrameNumber = Cast(dv.containerViewPath?.().lastElement()?.Document?._currentFrame, 'number'); // frame number that container is at which determines layout frame values @@ -92,10 +92,13 @@ ScriptingGlobals.add(function setBackgroundColor(color?: string, checkResult?: b } else { const selected = DocumentView.SelectedDocs().length ? DocumentView.SelectedDocs() : LinkManager.Instance.currentLink ? [LinkManager.Instance.currentLink] : []; if (checkResult) { - return selected.lastElement()?._backgroundColor ?? 'transparent'; + return selected.lastElement()?._backgroundColor ?? defaultFill(); } - SetActiveFillColor(color ?? 'transparent'); - selected.forEach(doc => { doc[DocData].backgroundColor = color; }); // prettier-ignore + if (!selected.length) setDefaultFill(color ?? 'transparent'); + else + selected.forEach(doc => { + doc[DocData][doc._layout_isSvg ? 'fillColor' : 'backgroundColor'] = color; + }); } return ''; }); @@ -295,10 +298,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 +314,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 +333,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' | 'italic' | 'elide' | 'underline' | 'left' | 'center' | 'right' | 'vcent' | 'bullet' | 'decimal'; type attrfuncs = [attrname, { checkResult: () => boolean; toggle?: () => unknown }]; // eslint-disable-next-line prefer-arrow-callback @@ -343,14 +342,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,16 +355,18 @@ 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), toggle: () => editorView && RichTextMenu.Instance?.toggleNoAutoLinkAnchor()}], ['bold', { checkResult: () => (editorView ? RichTextMenu.Instance?.bold??false : (Doc.UserDoc().fontWeight === 'bold')), toggle: editorView ? RichTextMenu.Instance?.toggleBold : () => { Doc.UserDoc().fontWeight = Doc.UserDoc().fontWeight === 'bold' ? undefined : 'bold'; }}], - ['italics', { checkResult: () => (editorView ? RichTextMenu.Instance?.italics ?? false : (Doc.UserDoc().fontStyle === 'italics')), - toggle: editorView ? RichTextMenu.Instance?.toggleItalics : () => { Doc.UserDoc().fontStyle = Doc.UserDoc().fontStyle === 'italics' ? undefined : 'italics'; }}], - ['underline', { checkResult: () => (editorView ? RichTextMenu.Instance?.underline ?? false: (Doc.UserDoc().textDecoration === 'underline')), - toggle: editorView ? RichTextMenu.Instance?.toggleUnderline : () => { Doc.UserDoc().textDecoration = Doc.UserDoc().textDecoration === 'underline' ? undefined : 'underline'; } }]] + ['italic', { checkResult: () => (editorView ? RichTextMenu.Instance?.italic ?? false : (Doc.UserDoc().fontStyle === 'italic')), + toggle: editorView ? RichTextMenu.Instance?.toggleItalic : () => { Doc.UserDoc().fontStyle = Doc.UserDoc().fontStyle === 'italic' ? undefined : 'italic'; }}], + ['underline', { checkResult: () => (editorView ? RichTextMenu.Instance?.underline ?? false: (Doc.UserDoc().fontDecoration === 'underline')), + toggle: editorView ? RichTextMenu.Instance?.toggleUnderline : () => { Doc.UserDoc().fontDecoration = Doc.UserDoc().fontDecoration === 'underline' ? undefined : 'underline'; } }]] const map = new Map(attrs.concat(alignments).concat(listings)); if (checkResult) { @@ -379,43 +376,46 @@ ScriptingGlobals.add(function toggleCharStyle(charStyle: attrname, checkResult?: return undefined; }); -function setActiveTool(toolIn: InkTool | Gestures, keepPrim: boolean, checkResult?: boolean) { +function setActiveTool(tool: InkTool | InkEraserTool | InkInkTool | Gestures, keepPrim: boolean, checkResult?: boolean) { InkTranscription.Instance?.createInkGroup(); - const tool = toolIn === InkTool.Eraser ? Doc.UserDoc().activeEraserTool : toolIn; if (checkResult) { - return ((Doc.ActiveTool === tool || (Doc.UserDoc().activeEraserTool === tool && (tool === toolIn || Doc.ActiveTool === tool))) && !GestureOverlay.Instance?.InkShape) || GestureOverlay.Instance?.InkShape === tool - ? GestureOverlay.Instance?.KeepPrimitiveMode || ![Gestures.Circle, Gestures.Line, Gestures.Rectangle].includes(tool as Gestures) + return Doc.ActiveTool === tool || Doc.ActiveEraser === tool || Doc.ActiveInk === tool || SnappingManager.InkShape === tool + ? true //SnappingManager.KeepGestureMode || ![Gestures.Circle, Gestures.Line, Gestures.Rectangle].includes(tool as Gestures) : false; } runInAction(() => { + const eraserTool = tool === InkTool.Eraser ? Doc.ActiveEraser : [InkEraserTool.Stroke, InkEraserTool.Radius, InkEraserTool.Segment].includes(tool as InkEraserTool) ? (tool as InkEraserTool) : undefined; + const inkTool = tool === InkTool.Ink ? Doc.ActiveInk : [InkInkTool.Pen, InkInkTool.Write, InkInkTool.Highlight].includes(tool as InkInkTool) ? (tool as InkInkTool) : undefined; if (GestureOverlay.Instance) { - GestureOverlay.Instance.KeepPrimitiveMode = keepPrim; + SnappingManager.SetKeepGestureMode(keepPrim); } if (Object.values(Gestures).includes(tool as Gestures)) { - if (GestureOverlay.Instance.InkShape === tool && !keepPrim) { + if (SnappingManager.InkShape === tool && !keepPrim) { Doc.ActiveTool = InkTool.None; - GestureOverlay.Instance.InkShape = undefined; + SnappingManager.SetInkShape(undefined); } else { - Doc.ActiveTool = InkTool.Pen; - GestureOverlay.Instance.InkShape = tool as Gestures; + Doc.ActiveTool = InkTool.Ink; + SnappingManager.SetInkShape(tool as Gestures); } - } else if (tool) { - if (Doc.UserDoc().ActiveTool === tool) { + } else if (eraserTool) { + if (Doc.ActiveTool === InkTool.Eraser && Doc.ActiveTool === tool) { Doc.ActiveTool = InkTool.None; } else { - if ([InkTool.StrokeEraser, InkTool.RadiusEraser, InkTool.SegmentEraser].includes(tool as InkTool)) { - Doc.UserDoc().activeEraserTool = tool; - } - // pen or eraser - if (Doc.ActiveTool === tool && !GestureOverlay.Instance.InkShape && !keepPrim) { - Doc.ActiveTool = InkTool.None; - } else { - Doc.ActiveTool = tool as InkTool; - GestureOverlay.Instance.InkShape = undefined; - } + Doc.ActiveEraser = eraserTool; + Doc.ActiveTool = InkTool.Eraser; + SnappingManager.SetInkShape(undefined); + } + } else if (inkTool) { + if (Doc.ActiveTool === InkTool.Ink && Doc.ActiveTool === tool) { + Doc.ActiveTool = InkTool.None; + } else { + Doc.ActiveInk = inkTool; + Doc.ActiveTool = InkTool.Ink; + SnappingManager.SetInkShape(undefined); } } else { - Doc.ActiveTool = InkTool.None; + if ((Doc.ActiveTool === tool || !tool) && !keepPrim) Doc.ActiveTool = InkTool.None; + else Doc.ActiveTool = tool as InkTool; } }); return undefined; @@ -425,44 +425,39 @@ ScriptingGlobals.add(setActiveTool, 'sets the active ink tool mode'); // eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(function activeEraserTool() { - return StrCast(Doc.UserDoc().activeEraserTool, InkTool.StrokeEraser); + return StrCast(Doc.UserDoc().activeEraserTool, InkEraserTool.Stroke); }, 'returns the current eraser tool'); // toggle: Set overlay status of selected document // eslint-disable-next-line prefer-arrow-callback -ScriptingGlobals.add(function setInkProperty(option: 'inkMask' | 'labels' | 'fillColor' | 'strokeWidth' | 'strokeColor' | 'eraserWidth', value: string | number, checkResult?: boolean) { - const selected = DocumentView.SelectedDocs().lastElement() ?? Doc.UserDoc(); +ScriptingGlobals.add(function setInkProperty(option: InkProperty, value: string | number, checkResult?: boolean) { + const selected = DocumentView.SelectedDocs().lastElement(); // prettier-ignore - const map: Map<'inkMask' | 'labels' | 'fillColor' | 'strokeWidth' | 'strokeColor' | 'eraserWidth', { checkResult: () => number|boolean|string|undefined; setInk: (doc: Doc) => void; setMode: () => void }> = new Map([ - ['inkMask', { + const map: Map<InkProperty, { checkResult: () => number|boolean|string|undefined; setInk: (doc: Doc) => void; setMode: () => void }> = new Map([ + [InkProperty.Mask, { checkResult: () => ((selected?._layout_isSvg ? BoolCast(selected[DocData].stroke_isInkMask) : ActiveIsInkMask())), setInk: (doc: Doc) => { doc[DocData].stroke_isInkMask = !doc.stroke_isInkMask; }, - setMode: () => selected?.type !== DocumentType.INK && SetActiveIsInkMask(!ActiveIsInkMask()), - }], - ['labels', { - checkResult: () => ((selected?._stroke_showLabel ? BoolCast(selected[DocData].stroke_showLabel) : ActiveInkHideTextLabels())), - setInk: (doc: Doc) => { doc[DocData].stroke_showLabel = !doc.stroke_showLabel; }, - setMode: () => selected?.type !== DocumentType.INK && SetActiveInkHideTextLabels(!ActiveInkHideTextLabels()), + setMode: () => SetActiveIsInkMask(value ? true : false) }], - ['fillColor', { - checkResult: () => (selected?._layout_isSvg ? StrCast(selected[DocData].fillColor) : ActiveFillColor() ?? "transparent"), - setInk: (doc: Doc) => { doc[DocData].fillColor = StrCast(value); }, - setMode: () => SetActiveFillColor(StrCast(value)), + [InkProperty.Labels, { + checkResult: () => ((selected?._layout_isSvg ? BoolCast(selected[DocData].stroke_showLabel) : !ActiveHideTextLabels())), + setInk: (doc: Doc) => { doc[DocData].stroke_showLabel = value; }, + setMode: () => SetactiveHideTextLabels(value? false : true), }], - [ 'strokeWidth', { + [ InkProperty.StrokeWidth, { checkResult: () => (selected?._layout_isSvg ? NumCast(selected[DocData].stroke_width) : ActiveInkWidth()), setInk: (doc: Doc) => { doc[DocData].stroke_width = NumCast(value); }, - setMode: () => { SetActiveInkWidth(value.toString()); selected?.type === DocumentType.INK && setActiveTool( GestureOverlay.Instance.InkShape ?? InkTool.Pen, true, false);}, + setMode: () => SetActiveInkWidth(value.toString()), }], - ['strokeColor', { + [InkProperty.StrokeColor, { checkResult: () => (selected?._layout_isSvg? StrCast(selected[DocData].color) : ActiveInkColor()), setInk: (doc: Doc) => { doc[DocData].color = String(value); }, - setMode: () => { SetActiveInkColor(StrCast(value)); selected?.type === DocumentType.INK && setActiveTool(GestureOverlay.Instance.InkShape ?? InkTool.Pen, true, false);}, + setMode: () => SetActiveInkColor(StrCast(value)) }], - [ 'eraserWidth', { + [ InkProperty.EraserWidth, { checkResult: () => ActiveEraserWidth() === 0 ? 1 : ActiveEraserWidth(), setInk: (doc: Doc) => { }, - setMode: () => { SetEraserWidth(+value);}, + setMode: () => SetEraserWidth(+value), }] ]); diff --git a/src/client/views/newlightbox/ButtonMenu/ButtonMenu.tsx b/src/client/views/newlightbox/ButtonMenu/ButtonMenu.tsx index 3eb99f47a..8115bafbf 100644 --- a/src/client/views/newlightbox/ButtonMenu/ButtonMenu.tsx +++ b/src/client/views/newlightbox/ButtonMenu/ButtonMenu.tsx @@ -1,5 +1,3 @@ -/* eslint-disable jsx-a11y/no-static-element-interactions */ -/* eslint-disable jsx-a11y/click-events-have-key-events */ import { action } from 'mobx'; import * as React from 'react'; import { Doc } from '../../../../fields/Doc'; @@ -35,10 +33,10 @@ export function ButtonMenu() { <div className="newLightboxView-penBtn" title="toggle pen annotation" - style={{ background: Doc.ActiveTool === InkTool.Pen ? 'white' : undefined }} + style={{ background: Doc.ActiveTool === InkTool.Ink ? 'white' : undefined }} onClick={e => { e.stopPropagation(); - Doc.ActiveTool = Doc.ActiveTool === InkTool.Pen ? InkTool.None : InkTool.Pen; + Doc.ActiveTool = Doc.ActiveTool === InkTool.Ink ? InkTool.None : InkTool.Ink; }} /> <div diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx index d51b1cd3a..3905403ea 100644 --- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx @@ -1,7 +1,8 @@ +import { Colors } from 'browndash-components'; import { action, makeObservable, observable } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; -import { OmitKeys } from '../../../ClientUtils'; +import { DashColor, OmitKeys } from '../../../ClientUtils'; import { numberRange } from '../../../Utils'; import { Doc, DocListCast, Opt } from '../../../fields/Doc'; import { TransitionTimer } from '../../../fields/DocSymbols'; @@ -296,12 +297,12 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF transition: this.DataTransition(), zIndex: this.ZIndex, display: this.Width ? undefined : 'none', + mixBlendMode: !this.layoutDoc.disableMixBlend && DashColor(StrCast(this.layoutDoc[this.layoutDoc._layout_isSvg ? 'fillColor' : 'backgroundColor'], Colors.WHITE)).alpha() !== 1 ? 'multiply' : undefined, }}> {this.RenderCutoffProvider(this.Document) ? ( <div style={{ position: 'absolute', width: this.PanelWidth(), height: this.PanelHeight(), background: 'lightGreen' }} /> ) : ( <DocumentView - // eslint-disable-next-line react/jsx-props-no-spreading {...OmitKeys(this._props,this.WrapperKeys.map(val => val.lower)).omit} // prettier-ignore Document={this._props.Document} renderDepth={this._props.renderDepth} diff --git a/src/client/views/nodes/DataVizBox/DataVizBox.tsx b/src/client/views/nodes/DataVizBox/DataVizBox.tsx index d79a37181..7925410e2 100644 --- a/src/client/views/nodes/DataVizBox/DataVizBox.tsx +++ b/src/client/views/nodes/DataVizBox/DataVizBox.tsx @@ -73,7 +73,7 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { return <div className="dataVizBox-annotationLayer" style={{ height: this._props.PanelHeight(), width: this._props.PanelWidth() }} ref={this._annotationLayer} />; } marqueeDown = (e: React.PointerEvent) => { - if (!e.altKey && e.button === 0 && NumCast(this.Document._freeform_scale, 1) <= NumCast(this.Document.freeform_scaleMin, 1) && this._props.isContentActive() && ![InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool)) { + if (!e.altKey && e.button === 0 && NumCast(this.Document._freeform_scale, 1) <= NumCast(this.Document.freeform_scaleMin, 1) && this._props.isContentActive() && Doc.ActiveTool !== InkTool.Ink) { setupMoveUpEvents( this, e, @@ -453,7 +453,7 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { @action onPointerDown = (e: React.PointerEvent): void => { if ((this.Document._freeform_scale || 1) !== 1) return; - if (!e.altKey && e.button === 0 && this._props.isContentActive() && ![InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool)) { + if (!e.altKey && e.button === 0 && this._props.isContentActive() && Doc.ActiveTool !== InkTool.Ink) { this._props.select(false); MarqueeAnnotator.clearAnnotations(this._savedAnnotations); this._marqueeing = [e.clientX, e.clientY]; 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..30f9e6363 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -380,7 +380,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document }; onPointerMove = (e: PointerEvent): void => { - if (e.buttons !== 1 || [InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool)) return; + if (e.buttons !== 1 || Doc.ActiveTool === InkTool.Ink) return; if (!ClientUtils.isClick(e.clientX, e.clientY, this._downX, this._downY, Date.now())) { this.cleanupPointerEvents(); @@ -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 @@ -1562,55 +1560,45 @@ export class DocumentView extends DocComponent<DocumentViewProps>() { } else func(); } } - -export function ActiveFillColor(): string { - const dv = DocumentView.Selected().lastElement() ?.Document._layout_isSvg ? DocumentView.Selected().lastElement() : undefined; - return StrCast(dv?.Document.fillColor, StrCast(ActiveInkPen()?.activeFillColor, "")); -} // prettier-ignore -export function ActiveInkPen(): Doc { return Doc.UserDoc(); } // prettier-ignore -export function ActiveInkColor(): string { return StrCast(ActiveInkPen()?.activeInkColor, 'black'); } // prettier-ignore -export function ActiveIsInkMask(): boolean { return BoolCast(ActiveInkPen()?.activeIsInkMask, false); } // prettier-ignore -export function ActiveInkHideTextLabels(): boolean { return BoolCast(ActiveInkPen().activeInkHideTextLabels, false); } // prettier-ignore -export function ActiveArrowStart(): string { return StrCast(ActiveInkPen()?.activeArrowStart, ''); } // prettier-ignore -export function ActiveArrowEnd(): string { return StrCast(ActiveInkPen()?.activeArrowEnd, ''); } // prettier-ignore -export function ActiveArrowScale(): number { return NumCast(ActiveInkPen()?.activeArrowScale, 1); } // prettier-ignore -export function ActiveDash(): string { return StrCast(ActiveInkPen()?.activeDash, '0'); } // prettier-ignore -export function ActiveInkWidth(): number { return Number(ActiveInkPen()?.activeInkWidth); } // prettier-ignore -export function ActiveInkBezierApprox(): string { return StrCast(ActiveInkPen()?.activeInkBezier); } // prettier-ignore -export function ActiveEraserWidth(): number { return Number(ActiveInkPen()?.eraserWidth ?? 25); } // prettier-ignore - +export function ActiveHideTextLabels(): boolean { return BoolCast(Doc.UserDoc().activeHideTextLabels, false); } // prettier-ignore +export function ActiveIsInkMask(): boolean { return BoolCast(Doc.UserDoc()?.activeIsInkMask, false); } // prettier-ignore +export function ActiveEraserWidth(): number { return Number(Doc.UserDoc()?.activeEraserWidth ?? 25); } // prettier-ignore + +export function ActiveInkFillColor(): string { return StrCast(Doc.UserDoc()?.[`active${Doc.ActiveInk}Fill`]); } // prettier-ignore +export function ActiveInkColor(): string { return StrCast(Doc.UserDoc()?.[`active${Doc.ActiveInk}Color`], 'black'); } // prettier-ignore +export function ActiveInkArrowStart(): string { return StrCast(Doc.UserDoc()?.[`active${Doc.ActiveInk}ArrowStart`], ''); } // prettier-ignore +export function ActiveInkArrowEnd(): string { return StrCast(Doc.UserDoc()?.[`active${Doc.ActiveInk}ArrowEnd`], ''); } // prettier-ignore +export function ActiveInkArrowScale(): number { return NumCast(Doc.UserDoc()?.[`active${Doc.ActiveInk}ArrowScale`], 1); } // prettier-ignore +export function ActiveInkDash(): string { return StrCast(Doc.UserDoc()?.[`active${Doc.ActiveInk}Dash`], '0'); } // prettier-ignore +export function ActiveInkWidth(): number { return Number(Doc.UserDoc()?.[`active${Doc.ActiveInk}Width`]); } // prettier-ignore +export function ActiveInkBezierApprox(): string { return StrCast(Doc.UserDoc()[`active${Doc.ActiveInk}Bezier`]); } // prettier-ignore + +export function SetActiveIsInkMask(value: boolean) { Doc.UserDoc() && (Doc.UserDoc().activeIsInkMask = value); } // prettier-ignore +export function SetactiveHideTextLabels(value: boolean) { Doc.UserDoc() && (Doc.UserDoc().activeHideTextLabels = value); } // prettier-ignore +export function SetEraserWidth(width: number): void { Doc.UserDoc() && (Doc.UserDoc().activeEraserWidth = width); } // prettier-ignore export function SetActiveInkWidth(width: string): void { - !isNaN(parseInt(width)) && ActiveInkPen() && (ActiveInkPen().activeInkWidth = width); + !isNaN(parseInt(width)) && Doc.UserDoc() && (Doc.UserDoc()[`active${Doc.ActiveInk}Width`] = width); } -export function SetActiveBezierApprox(bezier: string): void { - ActiveInkPen() && (ActiveInkPen().activeInkBezier = isNaN(parseInt(bezier)) ? '' : bezier); +export function SetActiveInkBezierApprox(bezier: string): void { + Doc.UserDoc() && (Doc.UserDoc()[`active${Doc.ActiveInk}Bezier`] = isNaN(parseInt(bezier)) ? '' : bezier); } export function SetActiveInkColor(value: string) { - ActiveInkPen() && (ActiveInkPen().activeInkColor = value); -} -export function SetActiveIsInkMask(value: boolean) { - ActiveInkPen() && (ActiveInkPen().activeIsInkMask = value); -} -export function SetActiveInkHideTextLabels(value: boolean) { - ActiveInkPen() && (ActiveInkPen().activeInkHideTextLabels = value); -} -export function SetActiveFillColor(value: string) { - ActiveInkPen() && (ActiveInkPen().activeFillColor = value); + Doc.UserDoc() && (Doc.UserDoc()[`active${Doc.ActiveInk}Color`] = value); } -export function SetActiveArrowStart(value: string) { - ActiveInkPen() && (ActiveInkPen().activeArrowStart = value); +export function SetActiveInkFillColor(value: string) { + Doc.UserDoc() && (Doc.UserDoc()[`active${Doc.ActiveInk}Fill`] = value); } -export function SetActiveArrowEnd(value: string) { - ActiveInkPen() && (ActiveInkPen().activeArrowEnd = value); +export function SetActiveInkArrowStart(value: string) { + Doc.UserDoc() && (Doc.UserDoc()[`active${Doc.ActiveInk}ArrowStart`] = value); } -export function SetActiveArrowScale(value: number) { - ActiveInkPen() && (ActiveInkPen().activeArrowScale = value); +export function SetActiveInkArrowEnd(value: string) { + Doc.UserDoc() && (Doc.UserDoc()[`active${Doc.ActiveInk}ArrowEnd`] = value); } -export function SetActiveDash(dash: string): void { - !isNaN(parseInt(dash)) && ActiveInkPen() && (ActiveInkPen().activeDash = dash); +export function SetActiveInkArrowScale(value: number) { + Doc.UserDoc() && (Doc.UserDoc()[`active${Doc.ActiveInk}ArrowScale`] = value); } -export function SetEraserWidth(width: number): void { - ActiveInkPen() && (ActiveInkPen().eraserWidth = width); +export function SetActiveInkDash(dash: string): void { + !isNaN(parseInt(dash)) && Doc.UserDoc() && (Doc.UserDoc()[`active${Doc.ActiveInk}`] = dash); } // eslint-disable-next-line prefer-arrow-callback 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..2405889cf 100644 --- a/src/client/views/nodes/FontIconBox/FontIconBox.scss +++ b/src/client/views/nodes/FontIconBox/FontIconBox.scss @@ -10,6 +10,13 @@ height: 3px !important; } } +.fonticonbox { + margin: auto; + width: 100%; + .formLabel { + height: 5px; + } +} .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..8c138c2ee 100644 --- a/src/client/views/nodes/FontIconBox/FontIconBox.tsx +++ b/src/client/views/nodes/FontIconBox/FontIconBox.tsx @@ -4,7 +4,7 @@ import { Button, ColorPicker, Dropdown, DropdownType, IconButton, IListItemProps import { action, computed, makeObservable, observable } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; -import { ClientUtils, returnFalse, returnTrue, setupMoveUpEvents } from '../../../../ClientUtils'; +import { ClientUtils, DashColor, returnFalse, returnTrue, setupMoveUpEvents } from '../../../../ClientUtils'; import { Doc, DocListCast, StrListCast } from '../../../../fields/Doc'; import { BoolCast, DocCast, NumCast, ScriptCast, StrCast } from '../../../../fields/Types'; import { emptyFunction } from '../../../../Utils'; @@ -21,6 +21,8 @@ import { FieldView, FieldViewProps } from '../FieldView'; import { OpenWhere } from '../OpenWhere'; import './FontIconBox.scss'; import TrailsIcon from './TrailsIcon'; +import { InkTool } from '../../../../fields/InkField'; +import { ScriptField } from '../../../../fields/ScriptField'; export enum ButtonType { TextButton = 'textBtn', @@ -126,7 +128,8 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() { background={SnappingManager.userBackgroundColor} numberDropdownType={type} showPlusMinus={false} - tooltip={this.label} + formLabel={(StrCast(this.Document.title).startsWith(' ') ? '\u00A0' : '') + StrCast(this.Document.title)} + tooltip={StrCast(this.Document.toolTip, this.label)} type={Type.PRIM} min={NumCast(this.dataDoc.numBtnMin, 0)} max={NumCast(this.dataDoc.numBtnMax, 100)} @@ -149,64 +152,79 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() { }; /** + * Displays custom dropdown menu for fonts -- this is a HACK -- fix for generality, don't copy + */ + handleFontDropdown = (script: () => string, buttonList: string[]) => { + // text = StrCast((RichTextMenu.Instance?.TextView?.EditorView ? RichTextMenu.Instance : Doc.UserDoc()).fontFamily); + return { + buttonList, + jsx: undefined, + selectedVal: script(), + getStyle: (val: string) => ({ fontFamily: val }), + }; + }; + /** + * Displays custom dropdown menu for view selection -- this is a HACK -- fix for generality, don't copy + */ + handleViewDropdown = (script: ScriptField, buttonList: string[]) => { + const selected = Array.from(script?.script.run({ _readOnly_: true }).result as Doc[]); + const noviceList = [CollectionViewType.Freeform, CollectionViewType.Schema, CollectionViewType.Card, CollectionViewType.Carousel3D, CollectionViewType.Carousel, CollectionViewType.Stacking, CollectionViewType.NoteTaking]; + return selected.length === 1 && selected[0].type === DocumentType.COL + ? { + buttonList: buttonList.filter(value => !Doc.noviceMode || !noviceList.length || noviceList.includes(value as CollectionViewType)), + getStyle: undefined, + selectedVal: StrCast(selected[0]._type_collection), + } + : { + jsx: selected.length ? ( + <Popup + icon={<FontAwesomeIcon size="1x" icon={selected.length > 1 ? 'caret-down' : (Doc.toIcon(selected.lastElement()) as IconProp)} />} + text={selected.length === 1 ? ClientUtils.cleanDocumentType(StrCast(selected[0].type) as DocumentType) : selected.length + ' selected'} + type={Type.TERT} + color={SnappingManager.userColor} + background={SnappingManager.userVariantColor} + popup={<SelectedDocView selectedDocs={selected} />} + fillWidth + /> + ) : ( + <Button + text={`${Doc.ActiveTool === InkTool.None ? 'Text box' : Doc.ActiveInk} defaults`} // + type={Type.TERT} + color={SnappingManager.userColor} + background={SnappingManager.userVariantColor} + fillWidth + inactive + /> + ), + }; + }; + + /** * Dropdown list */ @computed get dropdownListButton() { const script = ScriptCast(this.Document.script); - - let noviceList: string[] = []; - let text: string | undefined; - let getStyle: (val: string) => { [key: string]: string } = () => ({}); - let icon: IconProp = 'caret-down'; - const isViewDropdown = script?.script.originalScript.startsWith('{ return setView'); - if (isViewDropdown) { - const selected = Array.from(script?.script.run({ _readOnly_: true }).result as Doc[]); - // const selected = DocumentView.SelectedDocs(); - if (selected.lastElement()) { - if (StrCast(selected.lastElement().type) === DocumentType.COL) { - text = StrCast(selected.lastElement()._type_collection); - } else { - if (selected.length > 1) { - text = selected.length + ' selected'; - } else { - text = ClientUtils.cleanDocumentType(StrCast(selected.lastElement().type) as DocumentType, '' as CollectionViewType); - icon = Doc.toIcon(selected.lastElement()); - } - return ( - <Popup - icon={<FontAwesomeIcon size="1x" icon={icon} />} - text={text} - type={Type.TERT} - color={SnappingManager.userColor} - background={SnappingManager.userVariantColor} - popup={<SelectedDocView selectedDocs={selected} />} - fillWidth - /> - ); - } - } else { - return <Button text="None Selected" type={Type.TERT} color={SnappingManager.userColor} background={SnappingManager.userVariantColor} fillWidth inactive />; - } - noviceList = [CollectionViewType.Freeform, CollectionViewType.Schema, CollectionViewType.Card, CollectionViewType.Carousel3D, CollectionViewType.Carousel, CollectionViewType.Stacking, CollectionViewType.NoteTaking]; - } else { - text = script?.script.run({ this: this.Document, value: '', _readOnly_: true }).result as string; - // text = StrCast((RichTextMenu.Instance?.TextView?.EditorView ? RichTextMenu.Instance : Doc.UserDoc()).fontFamily); - if (this.Document.title === 'Font') getStyle = (val: string) => ({ fontFamily: val }); // bcz: major hack to style the font dropdown items --- needs to become part of the dropdown's metadata - } + const selectedFunc = () => script?.script.run({ this: this.Document, value: '', _readOnly_: true }).result as string; + const { buttonList, selectedVal, getStyle, jsx } = (() => { + switch (this.Document.title) { + case 'Font': return this.handleFontDropdown(selectedFunc, this.buttonList); + case 'Perspective': return this.handleViewDropdown(script, this.buttonList); + default: return { buttonList: this.buttonList, selectedVal: selectedFunc(), jsx: undefined, getStyle: undefined }; + } // prettier-ignore + })(); + if (jsx) return jsx; // Get items to place into the list - const list: IListItemProps[] = this.buttonList - .filter(value => !Doc.noviceMode || !noviceList.length || noviceList.includes(value)) - .map(value => ({ - text: typeof value === 'string' ? value.charAt(0).toUpperCase() + value.slice(1) : StrCast(DocCast(value)?.title), - val: value, - style: getStyle(value), - // shortcut: '#', - })); + const list: IListItemProps[] = buttonList.map(value => ({ + text: typeof value === 'string' ? value.charAt(0).toUpperCase() + value.slice(1) : StrCast(DocCast(value)?.title), + val: value, + style: getStyle?.(value), + // shortcut: '#', + })); return ( <Dropdown - selectedVal={text} + selectedVal={selectedVal} setSelectedVal={undoable(value => script.script.run({ this: this.Document, value }), `dropdown select ${this.label}`)} color={SnappingManager.userColor} background={SnappingManager.userVariantColor} @@ -215,7 +233,7 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() { dropdownType={DropdownType.SELECT} onItemDown={this.dropdownItemDown} items={list} - tooltip={this.label} + tooltip={StrCast(this.Document.toolTip, this.label)} fillWidth /> ); @@ -235,49 +253,53 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() { const tooltip: string = StrCast(this.Document.toolTip); return ( - <ColorPicker - setSelectedColor={value => { - if (!this.colorBatch) this.colorBatch = UndoManager.StartBatch(`Set ${tooltip} color`); - this.colorScript?.script.run({ this: this.Document, value: value, _readOnly_: false }); - }} - setFinalColor={value => { - this.colorScript?.script.run({ this: this.Document, value: value, _readOnly_: false }); - this.colorBatch?.end(); - this.colorBatch = undefined; - }} - defaultPickerType="Classic" - selectedColor={curColor} - type={Type.PRIM} - color={color} - background={SnappingManager.userBackgroundColor} - icon={this.Icon(color) ?? undefined} - tooltip={tooltip} - label={this.label} - /> + <div + onPointerDown={e => { + e.stopPropagation(); + }}> + <ColorPicker + setSelectedColor={value => { + if (!this.colorBatch) this.colorBatch = UndoManager.StartBatch(`Set ${tooltip} color`); + this.colorScript?.script.run({ this: this.Document, value: value, _readOnly_: false }); + }} + setFinalColor={value => { + this.colorScript?.script.run({ this: this.Document, value: value, _readOnly_: false }); + this.colorBatch?.end(); + this.colorBatch = undefined; + }} + defaultPickerType="Classic" + selectedColor={curColor} + type={Type.PRIM} + color={color} + background={SnappingManager.userBackgroundColor} + icon={this.Icon(color) ?? undefined} + tooltip={tooltip} + label={this.label} + /> + </div> ); } @computed get multiToggleButton() { - // Determine the type of toggle button - const tooltip: string = StrCast(this.Document.toolTip); + const tooltip = StrCast(this.Document.toolTip); 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}`} + tooltip={`Click to Toggle ${tooltip} or select new option`} type={Type.PRIM} color={color} - background={background === SnappingManager.userBackgroundColor ? undefined : background} + background={undefined} multiSelect={true} onPointerDown={e => script && !toggleStatus && setupMoveUpEvents(this, e, returnFalse, emptyFunction, () => script.run({ this: this.Document, value: undefined, _readOnly_: false }))} isToggle={false} toggleStatus={toggleStatus} - label={this.label} + label={selectedItems.length === 1 ? selectedItems[0] : this.label} items={items.map(item => ({ icon: <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={StrCast(item.icon) as IconProp} color={color} />, tooltip: StrCast(item.toolTip), @@ -290,7 +312,7 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() { // it would be better to pas the 'added' flag to the callback script, but our script generator from currentUserUtils makes it hard to define // arbitrary parameter variables (but it could be done as a special case or with additional effort when creating the sript) const itemsChanged = items.filter(item => (val instanceof Array ? val.includes(item.toolType as string | number) : item.toolType === val)); - itemsChanged.forEach(itemDoc => ScriptCast(itemDoc.onClick).script.run({ this: itemDoc, _added_: added, itemDoc, _readOnly_: false })); + itemsChanged.forEach(itemDoc => ScriptCast(itemDoc.onClick).script.run({ this: itemDoc, _added_: added, value: toggleStatus, itemDoc, _readOnly_: false })); }} /> ); @@ -308,17 +330,19 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() { const toggleStatus = (script?.script.run({ this: this.Document, value: undefined, _readOnly_: true }).result as boolean) ?? false; // Colors const color = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Color) as string; - // const backgroundColor = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BackgroundColor); + // bcz: ink shapes are tri-state - off, one-shot, and on. Need to update Toggle buttons to allow this and update currentUserUtils to set the tri-state on the Doc + // in the meantime, if the button matches a tool type that is not locked, we want to set the background color to something distinct. + const inkShapeHack = ((this.Document.toolType && this.Document.toolType === SnappingManager.InkShape) || this.Document.toolType === Doc.ActiveTool) && !SnappingManager.KeepGestureMode; return ( <Toggle tooltip={`Toggle ${tooltip}`} toggleType={ToggleType.BUTTON} - type={Type.PRIM} + type={inkShapeHack ? Type.TERT : Type.PRIM} toggleStatus={toggleStatus} text={buttonText} color={color} - // background={SnappingManager.userBackgroundColor} + background={inkShapeHack ? DashColor(SnappingManager.userBackgroundColor).darken(0.05).toString() : undefined} icon={this.Icon(color)!} label={this.label} onPointerDown={e => @@ -392,7 +416,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/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index 0dfc0ec28..ff879a2ab 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -338,7 +338,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { @computed get nativeSize() { TraceMobx(); - if (this.paths.length && this.paths[0].includes('icon-hi')) return { nativeWidth: NumCast(this.layoutDoc._width), nativeHeight: NumCast(this.layoutDoc._height), nativeOrientation: 0} + if (this.paths.length && this.paths[0].includes('icon-hi')) return { nativeWidth: NumCast(this.layoutDoc._width), nativeHeight: NumCast(this.layoutDoc._height), nativeOrientation: 0 }; const nativeWidth = NumCast(this.dataDoc[this.fieldKey + '_nativeWidth'], NumCast(this.layoutDoc[this.fieldKey + '_nativeWidth'], 500)); const nativeHeight = NumCast(this.dataDoc[this.fieldKey + '_nativeHeight'], NumCast(this.layoutDoc[this.fieldKey + '_nativeHeight'], 500)); const nativeOrientation = NumCast(this.dataDoc[this.fieldKey + '_nativeOrientation'], 1); @@ -465,13 +465,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { marqueeDown = (e: React.PointerEvent) => { if (!this.dataDoc[this.fieldKey]) { this.chooseImage(); - } else if ( - !e.altKey && - e.button === 0 && - NumCast(this.layoutDoc._freeform_scale, 1) <= NumCast(this.dataDoc.freeform_scaleMin, 1) && - this._props.isContentActive() && - ![InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool) - ) { + } else if (!e.altKey && e.button === 0 && NumCast(this.layoutDoc._freeform_scale, 1) <= NumCast(this.dataDoc.freeform_scaleMin, 1) && this._props.isContentActive() && Doc.ActiveTool !== InkTool.Ink) { setupMoveUpEvents( this, e, 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/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx index de51f6447..3bf7de2fe 100644 --- a/src/client/views/nodes/VideoBox.tsx +++ b/src/client/views/nodes/VideoBox.tsx @@ -776,7 +776,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { // starts marquee selection marqueeDown = (e: React.PointerEvent) => { - if (!e.altKey && e.button === 0 && NumCast(this.layoutDoc._freeform_scale, 1) === 1 && this._props.isContentActive() && ![InkTool.Highlighter, InkTool.Pen].includes(Doc.ActiveTool)) { + if (!e.altKey && e.button === 0 && NumCast(this.layoutDoc._freeform_scale, 1) === 1 && this._props.isContentActive() && Doc.ActiveTool !== InkTool.Ink) { setupMoveUpEvents( this, e, diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx index a5788d02a..9ba4f8ead 100644 --- a/src/client/views/nodes/WebBox.tsx +++ b/src/client/views/nodes/WebBox.tsx @@ -44,6 +44,7 @@ import { LinkInfo } from './LinkDocPreview'; import { OpenWhere } from './OpenWhere'; import './WebBox.scss'; +// eslint-disable-next-line @typescript-eslint/no-require-imports const { CreateImage } = require('./WebBoxRenderer'); @observer @@ -335,7 +336,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { ele = document.createElement('div'); ele.append(contents); } - } catch (e) { + } catch { /* empty */ } const visibleAnchor = this._getAnchor(this._savedAnnotations, true); @@ -506,7 +507,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { let href: Opt<string>; try { href = iframe?.contentWindow?.location.href; - } catch (e) { + } catch { runInAction(() => this._warning++); href = undefined; } @@ -713,7 +714,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { this._webUrl = this._url; } } - } catch (e) { + } catch { console.log('WebBox URL error:' + this._url); } return true; @@ -805,7 +806,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { sel.empty(); // Chrome else if (sel?.removeAllRanges) sel.removeAllRanges(); // Firefox this.marqueeing = [e.clientX, e.clientY]; - if (!e.altKey && e.button === 0 && this._props.isContentActive() && ![InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool)) { + if (!e.altKey && e.button === 0 && this._props.isContentActive() && Doc.ActiveTool !== InkTool.Ink) { setupMoveUpEvents( this, e, @@ -855,7 +856,6 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { })} contentEditable onPointerDown={this.webClipDown} - // eslint-disable-next-line react/no-danger dangerouslySetInnerHTML={{ __html: field.html }} /> ); @@ -1031,7 +1031,6 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { {this.inlineTextAnnotations .sort((a, b) => NumCast(a.y) - NumCast(b.y)) .map(anno => ( - // eslint-disable-next-line react/jsx-props-no-spreading <Annotation {...this._props} fieldKey={this.annotationKey} pointerEvents={this.pointerEvents} containerDataDoc={this.dataDoc} annoDoc={anno} key={`${anno[Id]}-annotation`} /> ))} </div> @@ -1042,7 +1041,6 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { } renderAnnotations = (childFilters: () => string[]) => ( <CollectionFreeFormView - // eslint-disable-next-line react/jsx-props-no-spreading {...this._props} setContentViewBox={this.setInnerContent} NativeWidth={returnZero} @@ -1217,7 +1215,6 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { <div style={{ position: 'absolute', height: '100%', right: 0, top: 0, width: `calc(100 * ${this.sidebarWidth() / this._props.PanelWidth()}%` }}> <SidebarAnnos ref={this._sidebarRef} - // eslint-disable-next-line react/jsx-props-no-spreading {...this._props} whenChildContentsActiveChanged={this.whenChildContentsActiveChanged} fieldKey={this.fieldKey + '_' + this._urlHash} diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index 2aee7b6b3..905c69bb8 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 { @@ -134,10 +135,12 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB @observable _showSidebar = false; - @computed get fontColor() { return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.FontColor) as string; } // prettier-ignore - @computed get fontSize() { return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.FontSize) as string; } // prettier-ignore - @computed get fontFamily() { return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.FontFamily) as string; } // prettier-ignore - @computed get fontWeight() { return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.FontWeight) as string; } // prettier-ignore + @computed get fontColor() { return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.FontColor) as string; } // prettier-ignore + @computed get fontSize() { return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.FontSize) as string; } // prettier-ignore + @computed get fontFamily() { return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.FontFamily) as string; } // prettier-ignore + @computed get fontWeight() { return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.FontWeight) as string; } // prettier-ignore + @computed get fontStyle() { return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.FontStyle) as string; } // prettier-ignore + @computed get fontDecoration() { return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.FontDecoration) as string; } // prettier-ignore set _recordingDictation(value) { !this.dataDoc[`${this.fieldKey}_recordingSource`] && (this.dataDoc.mediaState = value ? mediaState.Recording : undefined); @@ -158,6 +161,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 +169,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 +179,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 +192,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 +201,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 +267,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 +296,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 +313,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 +324,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 +376,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 +391,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 +411,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 +420,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 +435,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 +454,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 +481,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 +557,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 +620,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 +814,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 +1000,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 +1024,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 +1037,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 +1059,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 +1084,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 +1097,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 +1145,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 +1179,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 +1188,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 +1210,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 +1221,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 +1309,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 +1335,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 +1345,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 +1391,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 +1438,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 +1489,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 +1521,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 +1541,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 +1605,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 +1684,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 +1729,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 +1775,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 +1834,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 +1860,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 +1875,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 +2096,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 => { @@ -2116,6 +2121,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB fontSize: this.fontSize, fontFamily: this.fontFamily, fontWeight: this.fontWeight, + fontStyle: this.fontStyle, + textDecoration: this.fontDecoration, ...styleFromLayout, }}> <div @@ -2156,8 +2163,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..62fbd23ea 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; @@ -41,7 +41,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { @observable private collapsed: boolean = false; @observable private _noLinkActive: boolean = false; @observable private _boldActive: boolean = false; - @observable private _italicsActive: boolean = false; + @observable private _italicActive: boolean = false; @observable private _underlineActive: boolean = false; @observable private _strikethroughActive: boolean = false; @observable private _subscriptActive: boolean = false; @@ -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; }); @@ -89,8 +89,8 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { @computed get underline() { return this._underlineActive; } - @computed get italics() { - return this._italicsActive; + @computed get italic() { + return this._italicActive; } @computed get strikeThrough() { return this._strikethroughActive; @@ -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 @@ -285,7 +280,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { this._noLinkActive = false; this._boldActive = false; - this._italicsActive = false; + this._italicActive = false; this._underlineActive = false; this._strikethroughActive = false; this._subscriptActive = false; @@ -295,7 +290,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { switch (mark.name) { case 'noAutoLinkAnchor': this._noLinkActive = true; break; case 'strong': this._boldActive = true; break; - case 'em': this._italicsActive = true; break; + case 'em': this._italicActive = true; break; case 'underline': this._underlineActive = true; break; case 'strikethrough': this._strikethroughActive = true; break; case 'subscript': this._subscriptActive = true; break; @@ -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); @@ -346,7 +352,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { } }; - toggleItalics = () => { + toggleItalic = () => { if (this.view) { const mark = this.view.state.schema.mark(this.view.state.schema.marks.em); this.setMark(mark, this.view.state, this.view.dispatch, false); @@ -354,8 +360,8 @@ 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.TextView && this.view && fontField !== 'fitBox') { 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)) { @@ -367,9 +373,12 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { const fmark = this.view?.state.schema.marks['pF' + fontField.substring(1)].create(attrs); this.setMark(fmark, this.view.state, (tx: Transaction) => this.view!.dispatch(tx.addStoredMark(fmark)), true); this.view.focus(); + } else if (this.dataDoc) { + this.dataDoc[`text_${fontField}`] = value; + this.updateMenu(undefined, undefined, undefined, this.dataDoc); } else { Doc.UserDoc()[fontField] = value; - // this.updateMenu(this.view, undefined, this.props, this.layoutDoc); + this.updateMenu(undefined, undefined, undefined, this.dataDoc); } }; @@ -395,7 +404,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 +418,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 +434,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 +716,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/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx index 358557ad7..920c9ea8b 100644 --- a/src/client/views/pdf/PDFViewer.tsx +++ b/src/client/views/pdf/PDFViewer.tsx @@ -377,7 +377,7 @@ export class PDFViewer extends ObservableReactComponent<IViewerProps> { if ((e.button !== 0 || e.altKey) && this._props.isContentActive()) { this._setPreviewCursor?.(e.clientX, e.clientY, true, false, this._props.Document); } - if (!e.altKey && e.button === 0 && this._props.isContentActive() && ![InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool)) { + if (!e.altKey && e.button === 0 && this._props.isContentActive() && Doc.ActiveTool !== InkTool.Ink) { this._props.select(false); MarqueeAnnotator.clearAnnotations(this._savedAnnotations); this.isAnnotating = true; diff --git a/src/client/views/smartdraw/SmartDrawHandler.tsx b/src/client/views/smartdraw/SmartDrawHandler.tsx index b4635673c..d0f6566a5 100644 --- a/src/client/views/smartdraw/SmartDrawHandler.tsx +++ b/src/client/views/smartdraw/SmartDrawHandler.tsx @@ -21,7 +21,7 @@ import { SVGToBezier, SVGType } from '../../util/bezierFit'; import { InkingStroke } from '../InkingStroke'; import { ObservableReactComponent } from '../ObservableReactComponent'; import { MarqueeView } from '../collections/collectionFreeForm'; -import { ActiveArrowEnd, ActiveArrowStart, ActiveDash, ActiveFillColor, ActiveInkBezierApprox, ActiveInkColor, ActiveInkWidth, ActiveIsInkMask, DocumentView } from '../nodes/DocumentView'; +import { ActiveInkArrowEnd, ActiveInkArrowStart, ActiveInkDash, ActiveInkFillColor, ActiveInkBezierApprox, ActiveInkColor, ActiveInkWidth, ActiveIsInkMask, DocumentView } from '../nodes/DocumentView'; import './SmartDrawHandler.scss'; export interface DrawingOptions { @@ -105,14 +105,14 @@ export class SmartDrawHandler extends ObservableReactComponent<object> { y: bounds.top - inkWidth / 2, _width: bounds.width + inkWidth, _height: bounds.height + inkWidth, - stroke_showLabel: BoolCast(Doc.UserDoc().activeInkHideTextLabels)}, // prettier-ignore + stroke_showLabel: !BoolCast(Doc.UserDoc().activeHideTextLabels)}, // prettier-ignore inkWidth, opts.autoColor ? stroke[1] : ActiveInkColor(), ActiveInkBezierApprox(), - stroke[2] === 'none' ? ActiveFillColor() : stroke[2], - ActiveArrowStart(), - ActiveArrowEnd(), - ActiveDash(), + stroke[2] === 'none' ? ActiveInkFillColor() : stroke[2], + ActiveInkArrowStart(), + ActiveInkArrowEnd(), + ActiveInkDash(), ActiveIsInkMask() ); drawing.push(inkDoc); diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts index 6ec195910..aef7bf330 100644 --- a/src/fields/Doc.ts +++ b/src/fields/Doc.ts @@ -14,7 +14,7 @@ import { Initializing, Self, SelfProxy, TransitionTimer, UpdatingFromServer, Width } from './DocSymbols'; // prettier-ignore import { Copy, FieldChanged, HandleUpdate, Id, Parent, ToJavascriptString, ToScriptString, ToString } from './FieldSymbols'; -import { InkTool } from './InkField'; +import { InkEraserTool, InkInkTool, InkTool } from './InkField'; import { List } from './List'; import { ObjectField, serverOpType } from './ObjectField'; import { PrefetchProxy, ProxyField } from './Proxy'; @@ -253,6 +253,10 @@ export class Doc extends RefField { public static set ActivePage(val) { Doc.UserDoc().activePage = val; } // prettier-ignore public static get ActiveTool(): InkTool { return StrCast(Doc.UserDoc().activeTool, InkTool.None) as InkTool; } // prettier-ignore public static set ActiveTool(tool:InkTool){ Doc.UserDoc().activeTool = tool; } // prettier-ignore + public static get ActiveInk(): InkInkTool { return StrCast(Doc.UserDoc().activeInkTool, InkTool.None) as InkInkTool; } // prettier-ignore + public static set ActiveInk(tool:InkInkTool){ Doc.UserDoc().activeInkTool = tool; } // prettier-ignore + public static get ActiveEraser(): InkEraserTool { return StrCast(Doc.UserDoc().activeEraserTool, InkTool.None) as InkEraserTool; } // prettier-ignore + public static set ActiveEraser(tool:InkEraserTool){ Doc.UserDoc().activeEraserTool = tool; } // prettier-ignore public static get ActivePresentation() { return DocCast(Doc.ActiveDashboard?.activePresentation) as Opt<Doc>; } // prettier-ignore public static set ActivePresentation(val) { Doc.ActiveDashboard && (Doc.ActiveDashboard.activePresentation = val) } // prettier-ignore public static get ActiveDashboard() { return DocCast(Doc.UserDoc().activeDashboard); } // prettier-ignore diff --git a/src/fields/InkField.ts b/src/fields/InkField.ts index 17b99b033..936e45eb2 100644 --- a/src/fields/InkField.ts +++ b/src/fields/InkField.ts @@ -8,17 +8,30 @@ import { ObjectField } from './ObjectField'; // Helps keep track of the current ink tool in use. export enum InkTool { - None = 'none', - Pen = 'pen', - Highlighter = 'highlighter', - StrokeEraser = 'strokeeraser', - SegmentEraser = 'segmenteraser', - RadiusEraser = 'radiuseraser', - Eraser = 'eraser', // not a real tool, but a class of tools - Stamp = 'stamp', - Write = 'write', - PresentationPin = 'presentationpin', - SmartDraw = 'smartdraw', + None = 'None', + Ink = 'Ink', + Eraser = 'Eraser', // not a real tool, but a class of tools + SmartDraw = 'Smartdraw', +} + +export enum InkInkTool { + Pen = 'Pen', + Highlight = 'Highlight', + Write = 'Write', +} + +export enum InkEraserTool { + Stroke = 'Stroke', + Segment = 'Segment', + Radius = 'Radius', +} + +export enum InkProperty { + Mask = 'inkMask', + Labels = 'labels', + StrokeWidth = 'strokeWidth', + StrokeColor = 'strokeColor', + EraserWidth = ' eraserWidth', } export type Segment = Array<Bezier>; 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); diff --git a/src/pen-gestures/GestureTypes.ts b/src/pen-gestures/GestureTypes.ts index 5a8e9bd97..10f9ba6d0 100644 --- a/src/pen-gestures/GestureTypes.ts +++ b/src/pen-gestures/GestureTypes.ts @@ -8,7 +8,6 @@ export enum Gestures { Arrow = 'arrow', RightAngle = 'rightangle', } - // Defines a point in an ink as a pair of x- and y-coordinates. export interface PointData { X: number; diff --git a/src/pen-gestures/ndollar.ts b/src/pen-gestures/ndollar.ts index 04262b61f..f6e1c87fa 100644 --- a/src/pen-gestures/ndollar.ts +++ b/src/pen-gestures/ndollar.ts @@ -1,4 +1,5 @@ /* eslint-disable no-use-before-define */ +import { numberRange } from '../Utils'; import { Gestures } from './GestureTypes'; /** @@ -193,77 +194,38 @@ export class NDollarRecognizer { constructor( useBoundedRotationInvariance: boolean // constructor ) { + const rectMaker = (width: number, height1: number, height2: number) => [ + new Point(0, 0), // + new Point(0, height1), + new Point(width, height2), + new Point(width, 0), + new Point(0, 0), + ]; + + const arect = rectMaker(100, 100, 50); + const aorect = rectMaker(300, 100, 50); + const brect = rectMaker(100, 100, 200); + const borect = rectMaker(300, 100, 200); + const rect = rectMaker(100, 100, 100); + const orect = rectMaker(300, 100, 100); + const equilateral = [new Point(50, 100), new Point(100, 0), new Point(0, 0), new Point(50, 100)]; + const aequilateral = [new Point(20, 100), new Point(200, 0), new Point(0, 0), new Point(20, 100)]; + const bequilateral = [new Point(180, 100), new Point(200, 0), new Point(0, 0), new Point(180, 100)]; + const circle = numberRange(11).map(i => new Point(100 + 100 * Math.cos((i / 10) * Math.PI * 2), 100 + 100 * Math.sin((i / 10) * Math.PI * 2))); + const rightAngle = [new Point(0, 0), new Point(0, 100), new Point(200, 100)]; // - // one predefined multistroke for each multistroke type + // one predefined multistroke (plus its counterclockwise reversal for closed shapes) for each multistroke type // this.Multistrokes.push( - new Multistroke( - Gestures.Rectangle, - useBoundedRotationInvariance, - new Array([ - new Point(30, 146), // new Point(29, 160), new Point(30, 180), new Point(31, 200), - new Point(30, 222), // new Point(50, 219), new Point(70, 225), new Point(90, 230), - new Point(106, 225), // new Point(100, 200), new Point(106, 180), new Point(110, 160), - new Point(106, 146), // new Point(80, 150), new Point(50, 146), - new Point(30, 143), - ]) - ) - ); - this.Multistrokes.push(new Multistroke(Gestures.Rectangle, useBoundedRotationInvariance, new Array([new Point(30, 143), new Point(106, 146), new Point(106, 225), new Point(30, 222), new Point(30, 146)]))); - this.Multistrokes.push(new Multistroke(Gestures.Line, useBoundedRotationInvariance, [[new Point(12, 347), new Point(119, 347)]])); - this.Multistrokes.push( - new Multistroke( - Gestures.Triangle, // equilateral - useBoundedRotationInvariance, - new Array([new Point(40, 100), new Point(100, 200), new Point(140, 102), new Point(42, 100)]) - ) - ); - this.Multistrokes.push( - new Multistroke( - Gestures.Triangle, // equilateral - useBoundedRotationInvariance, - new Array([new Point(42, 100), new Point(140, 102), new Point(100, 200), new Point(40, 100)]) - ) - ); - this.Multistrokes.push( - new Multistroke( - Gestures.Circle, - useBoundedRotationInvariance, - new Array([ - new Point(200, 250), - new Point(240, 230), - new Point(248, 210), - new Point(248, 190), - new Point(240, 170), - new Point(200, 150), - new Point(160, 170), - new Point(151, 190), - new Point(151, 210), - new Point(160, 230), - new Point(201, 250), - ]) - ) - ); - this.Multistrokes.push( - new Multistroke( - Gestures.Circle, - useBoundedRotationInvariance, - new Array([ - new Point(201, 250), - new Point(160, 230), - new Point(151, 210), - new Point(151, 190), - new Point(160, 170), - new Point(200, 150), - new Point(240, 170), - new Point(248, 190), - new Point(248, 210), - new Point(240, 230), - new Point(200, 250), - ]) - ) + ...[arect, aorect, brect, borect, rect, orect].map(s => new Multistroke(Gestures.Rectangle, useBoundedRotationInvariance, [s])), + ...[arect, aorect, brect, borect, rect, orect].map(s => new Multistroke(Gestures.Rectangle, useBoundedRotationInvariance, [s.reverse()])), + ...[aequilateral, bequilateral, equilateral].map(s => new Multistroke(Gestures.Triangle, useBoundedRotationInvariance, [s])), + ...[aequilateral, bequilateral, equilateral].map(s => new Multistroke(Gestures.Triangle, useBoundedRotationInvariance, [s.reverse()])), + new Multistroke(Gestures.Circle, useBoundedRotationInvariance, [circle]), + new Multistroke(Gestures.Circle, useBoundedRotationInvariance, [circle.reverse()]), + new Multistroke(Gestures.RightAngle, useBoundedRotationInvariance, [rightAngle]), + new Multistroke(Gestures.Line, useBoundedRotationInvariance, [[new Point(12, 347), new Point(119, 347)]]) ); - this.Multistrokes.push(new Multistroke(Gestures.RightAngle, useBoundedRotationInvariance, new Array([new Point(0, 0), new Point(0, 100), new Point(200, 100)]))); NumMultistrokes = this.Multistrokes.length; // NumMultistrokes flags the end of the non user-defined gstures strokes // // PREDEFINED STROKES |