From c02b622fc3fa5a897549b483acef06d54858624b Mon Sep 17 00:00:00 2001 From: alyssaf16 Date: Thu, 20 Jun 2024 13:12:26 -0400 Subject: select working --- src/client/documents/DocUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/client/documents/DocUtils.ts') diff --git a/src/client/documents/DocUtils.ts b/src/client/documents/DocUtils.ts index 0c9fe0315..327bb1f8a 100644 --- a/src/client/documents/DocUtils.ts +++ b/src/client/documents/DocUtils.ts @@ -37,7 +37,7 @@ import { Docs, DocumentOptions } from './Documents'; const { DFLT_IMAGE_NATIVE_DIM } = require('../views/global/globalCssVariables.module.scss'); // prettier-ignore -const defaultNativeImageDim = Number(DFLT_IMAGE_NATIVE_DIM.replace('px', '')); +const defaultNativeImageDim = 10000000; //Number(DFLT_IMAGE_NATIVE_DIM.replace('px', '')); export namespace DocUtils { function matchFieldValue(doc: Doc, key: string, valueIn: any): boolean { -- cgit v1.2.3-70-g09d2 From 588951d4e7e392ca1ce3beacded7d01b6fbd480f Mon Sep 17 00:00:00 2001 From: bobzel Date: Mon, 30 Sep 2024 15:09:46 -0400 Subject: don't cap the resolution of an image anymore -- let the user do it explicitly through the Pixels menu option. --- src/client/documents/DocUtils.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) (limited to 'src/client/documents/DocUtils.ts') diff --git a/src/client/documents/DocUtils.ts b/src/client/documents/DocUtils.ts index 1696de7ad..3a0167cb6 100644 --- a/src/client/documents/DocUtils.ts +++ b/src/client/documents/DocUtils.ts @@ -35,11 +35,6 @@ import { TaskCompletionBox } from '../views/nodes/TaskCompletedBox'; import { DocumentType } from './DocumentTypes'; import { Docs, DocumentOptions } from './Documents'; -// eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-require-imports -const { DFLT_IMAGE_NATIVE_DIM } = require('../views/global/globalCssVariables.module.scss'); // prettier-ignore - -const defaultNativeImageDim = 10000000; //Number(DFLT_IMAGE_NATIVE_DIM.replace('px', '')); - export namespace DocUtils { function matchFieldValue(doc: Doc, key: string, valueIn: unknown): boolean { let value = valueIn; @@ -624,7 +619,7 @@ export namespace DocUtils { export function assignImageInfo(result: Upload.FileInformation, protoIn: Doc) { const proto = protoIn; if (Upload.isImageInformation(result)) { - const maxNativeDim = Math.min(Math.max(result.nativeHeight, result.nativeWidth), defaultNativeImageDim); + const maxNativeDim = Math.max(result.nativeHeight, result.nativeWidth); const exifRotation = StrCast(result.exifData?.data?.Orientation).toLowerCase(); proto.data_nativeOrientation = result.exifData?.data?.image?.Orientation ?? (exifRotation.includes('rotate 90') || exifRotation.includes('rotate 270') ? 5 : undefined); proto.data_nativeWidth = result.nativeWidth < result.nativeHeight ? (maxNativeDim * result.nativeWidth) / result.nativeHeight : maxNativeDim; -- cgit v1.2.3-70-g09d2 From 09b722789afa5d7c5edfc701978f17e4745113bc Mon Sep 17 00:00:00 2001 From: bobzel Date: Mon, 7 Oct 2024 14:25:35 -0400 Subject: more lint --- src/client/documents/DocUtils.ts | 2 -- src/client/views/TagsView.tsx | 4 +--- 2 files changed, 1 insertion(+), 5 deletions(-) (limited to 'src/client/documents/DocUtils.ts') diff --git a/src/client/documents/DocUtils.ts b/src/client/documents/DocUtils.ts index 0699ea09f..1130a9ae8 100644 --- a/src/client/documents/DocUtils.ts +++ b/src/client/documents/DocUtils.ts @@ -1,5 +1,3 @@ -/* eslint-disable prefer-destructuring */ -/* eslint-disable default-param-last */ /* eslint-disable no-use-before-define */ import { IconProp } from '@fortawesome/fontawesome-svg-core'; import { saveAs } from 'file-saver'; diff --git a/src/client/views/TagsView.tsx b/src/client/views/TagsView.tsx index 9858e7b61..072cae3af 100644 --- a/src/client/views/TagsView.tsx +++ b/src/client/views/TagsView.tsx @@ -84,9 +84,7 @@ export class TagItem extends ObservableReactComponent { */ public static allDocsWithTag = (tag: string) => DocListCast(TagItem.findTagCollectionDoc(tag)?.[DocData].docs); - public static docHasTag = (doc: Doc, tag: string) => { - return StrListCast(doc?.tags).includes(tag); - }; + public static docHasTag = (doc: Doc, tag: string) => StrListCast(doc?.tags).includes(tag); /** * Adds a tag to the metadata of this document and adds the Doc to the corresponding tag collection Doc (or creates it) * @param tag tag string -- cgit v1.2.3-70-g09d2 From 29b83f023442c313ca5cf95f70f6430f101060e6 Mon Sep 17 00:00:00 2001 From: bobzel Date: Mon, 14 Oct 2024 19:55:32 -0400 Subject: reorganized comparisonBox related components -- moved stuff down into Docs.Crete and CurrentUserUtils. changed Doc.Copy to copy Doc's in fields tagged with cloneOnCopy. Changed ComparisonBox to support hover for slide or flip views. Fixed pointerEfvents for hover in comparisonBox --- src/client/documents/DocUtils.ts | 2 - src/client/documents/Documents.ts | 43 ++++- src/client/util/CurrentUserUtils.ts | 4 +- .../views/collections/CollectionCarousel3DView.tsx | 33 ++-- .../views/collections/CollectionCarouselView.tsx | 58 +++--- src/client/views/collections/CollectionSubView.tsx | 2 +- src/client/views/collections/CollectionView.tsx | 6 +- .../views/collections/FlashcardPracticeUI.tsx | 36 ++-- src/client/views/nodes/ComparisonBox.scss | 3 +- src/client/views/nodes/ComparisonBox.tsx | 196 +++++++++++---------- .../views/nodes/formattedText/FormattedTextBox.tsx | 20 --- src/fields/Doc.ts | 2 +- 12 files changed, 223 insertions(+), 182 deletions(-) (limited to 'src/client/documents/DocUtils.ts') diff --git a/src/client/documents/DocUtils.ts b/src/client/documents/DocUtils.ts index 5f54f9d0a..19f3c89ef 100644 --- a/src/client/documents/DocUtils.ts +++ b/src/client/documents/DocUtils.ts @@ -103,7 +103,6 @@ export namespace DocUtils { return false; } const facetKeys = Object.keys(filterFacets).filter(fkey => fkey !== 'cookies' && fkey !== ClientUtils.noDragDocsFilter.split(Doc.FilterSep)[0]); - // eslint-disable-next-line no-restricted-syntax for (const facetKey of facetKeys) { const facet = filterFacets[facetKey]; @@ -288,7 +287,6 @@ export namespace DocUtils { return doc; } export function AssignDocField(doc: Doc, field: string, creator: (reqdOpts: DocumentOptions, items?: Doc[]) => Doc, reqdOpts: DocumentOptions, items?: Doc[], scripts?: { [key: string]: string }, funcs?: { [key: string]: string }) { - // eslint-disable-next-line no-return-assign return DocUtils.AssignScripts(DocUtils.AssignOpts(DocCast(doc[field]), reqdOpts, items) ?? (doc[field] = creator(reqdOpts, items)), scripts, funcs); } diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index f71b9f879..99af1f1a9 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -18,6 +18,7 @@ import { PointData } from '../../pen-gestures/GestureTypes'; import { DocServer } from '../DocServer'; import { dropActionType } from '../util/DropActionTypes'; import { CollectionViewType, DocumentType } from './DocumentTypes'; +import { Id } from '../../fields/FieldSymbols'; class EmptyBox { public static LayoutString() { @@ -360,6 +361,7 @@ export class DocumentOptions { isFolder?: BOOLt = new BoolInfo('is document a tree view folder'); _isTimelineLabel?: BOOLt = new BoolInfo('is document a timeline label'); _isLightbox?: BOOLt = new BoolInfo('whether a collection acts as a lightbox by opening lightbox links by hiding all other documents in collection besides link target'); + cloneOnCopy?: BOOLt = new BoolInfo('if this Doc is a field of another Doc, then it should be copied when the other Doc is copied'); mapPin?: DOCt = new DocInfo('pin associated with a config anchor', false); config_latitude?: NUMt = new NumInfo('latitude of a map', false); @@ -420,6 +422,12 @@ export class DocumentOptions { flexGap?: NUMt = new NumInfo('Linear view flex gap'); flexDirection?: 'unset' | 'row' | 'column' | 'row-reverse' | 'column-reverse'; + // Comparison + data_revealOp?: STRt = new StrInfo("visual reveal type for front and back of comparison - 'slide' or 'flip' "); + data_revealOp_hover?: BOOLt = new BoolInfo('reveal back of comparison manually or by hovering'); + data_front?: DOCt = new DocInfo('contents of front of flashcard/comparison'); + data_back?: DOCt = new DocInfo('contents of back of flashcard/comparison'); + link?: string; link_description?: string; // added for links link_relationship?: string; // type of relatinoship a link represents @@ -784,8 +792,39 @@ export namespace Docs { return InstanceFromProto(Prototypes.get(DocumentType.SCREENSHOT), '', options); } - export function ComparisonDocument(text: string, options: DocumentOptions = { title: 'Comparison Box' }) { - return InstanceFromProto(Prototypes.get(DocumentType.COMPARISON), text, options); + export function ComparisonDocument(title: string, options: DocumentOptions) { + return InstanceFromProto(Prototypes.get(DocumentType.COMPARISON), '', options); + } + /** + * Creates a text box where the supplied text (and optional iimage) will be vertically + * and horizontally centered. If text_placeholder is set to true, then the text will be + * treated as placeholder text and automatically selected when the text box is selected. + * @param title name of text box + * @param text text to display in text box + * @param opts metadata fields to set on text box + * @param img optional image to add to text box + * @returns + */ + export function CenteredTextCreator(title: string, text: string, opts: DocumentOptions, img?: Doc) { + return TextDocument(RichTextField.textToRtf(text, img?.[Id]), { + title, // + _layout_autoHeight: true, + _layout_centered: true, + text_align: 'center', + _layout_fitWidth: true, + ...opts, + }); + } + + export function FlashcardDocument(title: string, front?: Doc, back?: Doc, options: DocumentOptions = { title: 'Flashcard' }) { + return InstanceFromProto(Prototypes.get(DocumentType.COMPARISON), '', { + data_front: front ?? CenteredTextCreator('question', 'hint: Enter a topic, select this document and click the stack button to have GPT create a deck of cards', { text_placeholder: true, cloneOnCopy: true }, undefined), + data_back: back ?? CenteredTextCreator('answer', 'answer here', { text_placeholder: true, cloneOnCopy: true }, undefined), + _layout_fitWidth: true, + _layout_isFlashcard: true, + title, + ...options, + }); } export function DiagramDocument(options: DocumentOptions = { title: '' }) { return InstanceFromProto(Prototypes.get(DocumentType.DIAGRAM), undefined, options); diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index 798cdf5a9..555ccdd88 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -369,14 +369,14 @@ pie title Minerals in my tap water creator:(opts:DocumentOptions)=> Doc // how to create the empty thing if it doesn't exist }[] = [ {key: "Note", creator: opts => Docs.Create.TextDocument("", opts), opts: { _width: 200, _layout_autoHeight: true }}, - {key: "Flashcard", creator: opts => Docs.Create.ComparisonDocument("", opts), opts: { _layout_isFlashcard: true, _layout_fitWidth: true, _width: 300, _height: 300}}, + {key: "Flashcard", creator: opts => Docs.Create.FlashcardDocument("", undefined, undefined, opts),opts: { _width: 300, _height: 300}}, {key: "Image", creator: opts => Docs.Create.ImageDocument("", opts), opts: { _width: 400, _height:400 }}, {key: "Equation", creator: opts => Docs.Create.EquationDocument("",opts), opts: { _width: 300, _height: 35, }}, {key: "Noteboard", creator: opts => Docs.Create.NoteTakingDocument([], opts), opts: { _width: 250, _height: 200, _layout_fitWidth: true}}, {key: "Simulation", creator: opts => Docs.Create.SimulationDocument(opts), opts: { _width: 300, _height: 300, }}, {key: "Collection", creator: opts => Docs.Create.FreeformDocument([], opts), opts: { _width: 150, _height: 100, _layout_fitWidth: true }}, {key: "Webpage", creator: opts => Docs.Create.WebDocument("",opts), opts: { _width: 400, _height: 512, _nativeWidth: 850, data_useCors: true, }}, - {key: "Comparison", creator: opts => Docs.Create.ComparisonDocument("",opts), opts: { _width: 300, _height: 300 }}, + {key: "Comparison", creator: opts => Docs.Create.ComparisonDocument("", opts), opts: { _width: 300, _height: 300 }}, {key: "Diagram", creator: Docs.Create.DiagramDocument, opts: { _width: 300, _height: 300, _type_collection: CollectionViewType.Freeform, layout_diagramEditor: CollectionView.LayoutString("data") }, scripts: { onPaint: `toggleDetail(documentView, "diagramEditor","")`}}, {key: "Audio", creator: opts => Docs.Create.AudioDocument(nullAudio, opts),opts: { _width: 200, _height: 100, }}, {key: "Audio", creator: opts => Docs.Create.AudioDocument(nullAudio, opts),opts: { _width: 200, _height: 100, _layout_fitWidth: true, }}, diff --git a/src/client/views/collections/CollectionCarousel3DView.tsx b/src/client/views/collections/CollectionCarousel3DView.tsx index 05be376ca..e9ace733e 100644 --- a/src/client/views/collections/CollectionCarousel3DView.tsx +++ b/src/client/views/collections/CollectionCarousel3DView.tsx @@ -15,6 +15,7 @@ import { DocumentView } from '../nodes/DocumentView'; import { FocusViewOptions } from '../nodes/FocusViewOptions'; import './CollectionCarousel3DView.scss'; import { CollectionSubView, SubCollectionViewProps } from './CollectionSubView'; +import { computedFn } from 'mobx-utils'; // eslint-disable-next-line @typescript-eslint/no-require-imports const { CAROUSEL3D_CENTER_SCALE, CAROUSEL3D_SIDE_SCALE, CAROUSEL3D_TOP } = require('../global/globalCssVariables.module.scss'); @@ -53,18 +54,22 @@ export class CollectionCarousel3DView extends CollectionSubView() { centerScale = Number(CAROUSEL3D_CENTER_SCALE); sideScale = Number(CAROUSEL3D_SIDE_SCALE); - panelWidth = () => this._props.PanelWidth() / 3; - panelHeight = () => this._props.PanelHeight() * this.sideScale; + panelWidth = () => this._props.PanelWidth() / 3 / this.nativeScaling(); + panelHeight = () => (this._props.PanelHeight() * this.sideScale) / this.nativeScaling(); onChildDoubleClick = () => ScriptCast(this.layoutDoc.onChildDoubleClick); isContentActive = () => this._props.isSelected() || this._props.isContentActive() || this._props.isAnyChildContentActive(); - isChildContentActive = () => - this._props.isContentActive?.() === false - ? false - : this._props.isDocumentActive?.() && (this._props.childDocumentsActive?.() || BoolCast(this.Document.childDocumentsActive)) - ? true - : this._props.childDocumentsActive?.() === false || this.Document.childDocumentsActive === false + isChildContentActive = computedFn( + (doc: Doc) => () => + this._props.isContentActive?.() === false ? false - : undefined; + : this._props.isDocumentActive?.() && (this._props.childDocumentsActive?.() || BoolCast(this.Document.childDocumentsActive)) + ? true + : this._props.isContentActive?.() && this.curDoc() === doc + ? true + : this._props.childDocumentsActive?.() === false || this.Document.childDocumentsActive === false + ? false + : undefined + ); contentScreenToLocalXf = () => this._props.ScreenToLocalTransform().scale(this._props.NativeDimScaling?.() || 1); childScreenLeftToLocal = () => this.contentScreenToLocalXf() @@ -110,7 +115,7 @@ export class CollectionCarousel3DView extends CollectionSubView() { LayoutTemplateString={this._props.childLayoutString} focus={this.focus} ScreenToLocalTransform={dxf} - isContentActive={this.isChildContentActive} + isContentActive={this.isChildContentActive(child)} isDocumentActive={this._props.childDocumentsActive?.() || this.Document._childDocumentsActive ? this._props.isDocumentActive : this.isContentActive} PanelWidth={this.panelWidth} PanelHeight={this.panelHeight} @@ -125,7 +130,6 @@ export class CollectionCarousel3DView extends CollectionSubView() { } changeSlide = (direction: number) => { - DocumentView.DeselectAll(); this.layoutDoc._carousel_index = !this.curDoc() ? 0 : (NumCast(this.layoutDoc._carousel_index) + direction + this.carouselItems.length) % (this.carouselItems.length || 1); }; @@ -205,9 +209,10 @@ export class CollectionCarousel3DView extends CollectionSubView() { docViewProps = () => ({ ...this._props, // isDocumentActive: this._props.childDocumentsActive?.() ? this._props.isDocumentActive : this._props.isContentActive, - isContentActive: this.isChildContentActive, + isContentActive: this._props.isContentActive, ScreenToLocalTransform: this.contentScreenToLocalXf, }); + nativeScaling = () => this._props.NativeDimScaling?.() || 1; render() { return (
{this.content} diff --git a/src/client/views/collections/CollectionCarouselView.tsx b/src/client/views/collections/CollectionCarouselView.tsx index ef66a2c83..ff587b199 100644 --- a/src/client/views/collections/CollectionCarouselView.tsx +++ b/src/client/views/collections/CollectionCarouselView.tsx @@ -58,18 +58,12 @@ export class CollectionCarouselView extends CollectionSubView() { /** * Goes to the next Doc in the stack subject to the currently selected filter option. */ - advance = (e?: React.MouseEvent) => { - e?.stopPropagation(); - this.move(1); - }; + advance = () => this.move(1); /** * Goes to the previous Doc in the stack subject to the currently selected filter option. */ - goback = (e: React.MouseEvent) => { - e.stopPropagation(); - this.move(-1); - }; + goback = () => this.move(-1); curDoc = () => this.carouselItems[this.carouselIndex]?.layout; @@ -78,24 +72,23 @@ export class CollectionCarouselView extends CollectionSubView() { const childValue = doc?.['caption_' + property] ? this._props.styleProvider?.(doc, captionProps, property) : undefined; return childValue ?? this._props.styleProvider?.(this.layoutDoc, captionProps, property); }; - contentPanelWidth = () => this._props.PanelWidth() - 2 * NumCast(this.layoutDoc.xMargin); - contentPanelHeight = () => this._props.PanelHeight() - (StrCast(this.layoutDoc._layout_showCaption) ? 50 : 0) - 2 * NumCast(this.layoutDoc.yMargin); + contentPanelWidth = () => (this._props.PanelWidth() - 2 * NumCast(this.layoutDoc.xMargin)) / this.nativeScaling(); + contentPanelHeight = () => (this._props.PanelHeight() - (StrCast(this.layoutDoc._layout_showCaption) ? 50 : 0) - 2 * NumCast(this.layoutDoc.yMargin)) / this.nativeScaling(); onContentDoubleClick = () => ScriptCast(this.layoutDoc.onChildDoubleClick); onContentClick = () => ScriptCast(this.layoutDoc.onChildClick); captionWidth = () => this._props.PanelWidth() - 2 * this.captionMarginX; contentScreenToLocalXf = () => this._props - .ScreenToLocalTransform() - .translate(-NumCast(this.layoutDoc.xMargin), -NumCast(this.layoutDoc.yMargin)) - .scale(this._props.NativeDimScaling?.() || 1); + .ScreenToLocalTransform() // + .translate(-NumCast(this.layoutDoc.xMargin) / this.nativeScaling(), -NumCast(this.layoutDoc.yMargin) / this.nativeScaling()); isChildContentActive = () => this._props.isContentActive?.() === false ? false - : this._props.isDocumentActive?.() && (this._props.childDocumentsActive?.() || BoolCast(this.Document.childDocumentsActive)) + : this._props.isContentActive() ? true : this._props.childDocumentsActive?.() === false || this.Document.childDocumentsActive === false ? false - : undefined; + : undefined; // prettier-ignore onPassiveWheel = (e: WheelEvent) => e.stopPropagation(); renderDoc = (doc: Doc, showCaptions: boolean, overlayFunc?: (r: DocumentView | null) => void) => { return ( @@ -202,6 +195,8 @@ export class CollectionCarouselView extends CollectionSubView() { ); } + nativeScaling = () => this._props.NativeDimScaling?.() || 1; + docViewProps = () => ({ ...this._props, // isDocumentActive: this._props.childDocumentsActive?.() ? this._props.isDocumentActive : this._props.isContentActive, @@ -212,20 +207,25 @@ export class CollectionCarouselView extends CollectionSubView() { render() { return ( -
- {this.content} - {this.flashCardUI(this.curDoc, this.docViewProps, this.answered)} - {this.navButtons} +
+
+ {this.content} + {this.flashCardUI(this.curDoc, this.docViewProps, this.answered)} + {this.navButtons} +
); } diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index f85b0b433..ab5b70a85 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -523,7 +523,7 @@ export function CollectionSubView() { /** * How much the content of the collection is being scaled based on its nesting and its fit-to-width settings */ - @computed get contentScaling() { return this.ScreenToLocalBoxXf().Scale * (this._props.NativeDimScaling?.() ?? 1); } // prettier-ignore + @computed get contentScaling() { return this.ScreenToLocalBoxXf().Scale; } // prettier-ignore /** * The maximum size a UI widget can be in collection coordinates based on not wanting the widget to visually obscure too much of the collection * This takes the desired screen space size and converts into collection coordinates. It then returns the smaller of the converted diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx index 7418d4360..6f0833a22 100644 --- a/src/client/views/collections/CollectionView.tsx +++ b/src/client/views/collections/CollectionView.tsx @@ -83,7 +83,7 @@ export class CollectionView extends ViewBoxAnnotatableComponent (this._props.renderDepth ? this.ScreenToLocalBoxXf() : this.ScreenToLocalBoxXf().scale(this._props.PanelWidth() / this.bodyPanelWidth())); + screenToLocalTransform = this.ScreenToLocalBoxXf; // prettier-ignore private renderSubView = (type: CollectionViewType | undefined, props: SubCollectionViewProps) => { TraceMobx(); @@ -202,8 +202,6 @@ export class CollectionView extends ViewBoxAnnotatableComponent this._props.PanelWidth(); - childLayoutTemplate = () => this._props.childLayoutTemplate?.() || Cast(this.Document.childLayoutTemplate, Doc, null); isContentActive = () => this._isContentActive; @@ -221,7 +219,7 @@ export class CollectionView extends ViewBoxAnnotatableComponent togglePracticeMode(val as practiceMode)} /> - } - label={StrCast(this._props.layoutDoc.revealOp)} - onPointerDown={e => - setupMoveUpEvents(this, e, returnFalse, emptyFunction, () => { - this._props.layoutDoc.revealOp = this._props.layoutDoc.revealOp === flashcardRevealOp.HOVER ? flashcardRevealOp.FLIP : flashcardRevealOp.HOVER; - this._props.layoutDoc.childDocumentsActive = this._props.layoutDoc.revealOp === 'hover' ? true : undefined; - }) - } + multiSelect={false} + isToggle={false} + toggleStatus={!!this.practiceMode} + label={StrCast(this._props.layoutDoc.revealOp, flashcardRevealOp.FLIP)} + items={[ + ['reveal', StrCast(this._props.layoutDoc.revealOp) === flashcardRevealOp.SLIDE ? 'expand' : 'question', StrCast(this._props.layoutDoc.revealOp, flashcardRevealOp.FLIP)], + ['trigger', this._props.layoutDoc.revealOp_hover ? 'hand-point-up' : 'hand', this._props.layoutDoc.revealOp_hover ? 'show on hover' : 'show on click'], + ].map(([item, icon, tooltip]) => ({ + icon: , + tooltip: tooltip, + val: item, + }))} + selectedItems={this._props.layoutDoc.revealOp_hover ? ['reveal', 'trigger'] : 'reveal'} + onSelectionChange={(val: (string | number) | (string | number)[]) => { + if (val === 'reveal') this._props.layoutDoc.revealOp = this._props.layoutDoc.revealOp === flashcardRevealOp.SLIDE ? flashcardRevealOp.FLIP : flashcardRevealOp.SLIDE; + if (val === 'trigger') this._props.layoutDoc.revealOp_hover = !this._props.layoutDoc.revealOp_hover; + }} />
); diff --git a/src/client/views/nodes/ComparisonBox.scss b/src/client/views/nodes/ComparisonBox.scss index c328ef4bf..d1cc48051 100644 --- a/src/client/views/nodes/ComparisonBox.scss +++ b/src/client/views/nodes/ComparisonBox.scss @@ -246,8 +246,7 @@ pointer-events: none; } -.comparisonBox-interactive { - pointer-events: unset; +.comparisonBox-slide { cursor: ew-resize; .slide-bar { diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx index 80ef126dc..38ce5f2f7 100644 --- a/src/client/views/nodes/ComparisonBox.tsx +++ b/src/client/views/nodes/ComparisonBox.tsx @@ -35,22 +35,21 @@ import { FormattedTextBox } from './formattedText/FormattedTextBox'; const API_URL = 'https://api.unsplash.com/search/photos'; /** - * This view serves three distinct functions depending on the metadata field layout_isFlashcard - * 1) it provides a before/after animated sliding transition between two Docs - * 2) it provides a question/answer switch between two Docs (flashcard) - * 3) it provides a quiz view that displays a question and a user answer that can be "scored" by GPT + * This view serves two distinct functions depending on the revealOp field ('slide' or 'flip) + * 1) ('slide') - provides a before/after animated sliding transition between two Docs + * 2) ('flip') - provides a question/answer flip between two Docs + * And a third function that overrides the first two if the doc's container has its 'practiceMode' set to 'quiz' + * 3) ('quiz') - it provides a quiz view that displays a question and a user answer that can be "scored" by GPT + * NOTE: this should probably be changed to passing down a prop to the flashcard telling it to render as a quiz. * * In each case, the two docs are stored in the _front and _back fields * - * In the case of the flashcard, there is an icon that allows the user to choose between a - * hover and a flip action to switch between cards. The transition is stored in the 'revealOp' field. - * In addition, if a flashcard is created without data in the front/back fields, this will - * create Text documents with placeholder text indicating to the user how to fill in the cards. - * One option is to allow the user to enter a topic and, by clicking on the flashcard stack button, - * convert the comparision box into a stack of comparison boxes filled in by GPT about the topic. + * For 'flip' and 'slide', the trigger can either be clicking, or hovering as determined by the revealOp_hover field. + * For 'quiz' the data of both Docs are shown in a single-view quiz display. * - * Quiz mode is activated when the parent collection has its 'quiz' field set when it renders a flashcard. - * NOTE: this should probably be changed to passing down a prop to the flashcard telling it to render as a quiz. + * Users can create a stack of flashcards all at once (only) from an empty flashcard by entering a topic into the front card + * and clicking on the flashcard stack button. This will convert the comparision box into a stack of comparison boxes + * filled in by GPT about the topic. * */ @@ -67,7 +66,6 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent() */ public static createFlashcard(tuple: string, frontKey: string, backKey: string, useDoc?: Doc) { const [ktoken, atoken] = [ComparisonBox.ktoken, ComparisonBox.atoken]; - const newDoc = useDoc ?? Docs.Create.ComparisonDocument('', { _layout_isFlashcard: true, _width: 300, _height: 300 }); const question = (tuple.includes(ktoken) ? tuple.split(ktoken)[0] : tuple).split(atoken)[0]; const rest = tuple.replace(question, ''); // prettier-ignore @@ -78,9 +76,14 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent() rest.replace(atoken,""); // finally if there's no keyword, just get rid of answer token and take what's left const keyword = rest.replace(atoken, '').replace(answer, '').replace(ktoken, '').trim(); const fillInFlashcard = (img?: Doc) => { - newDoc[DocData][frontKey] = FormattedTextBox.centeredTextCreator('question', question, img); - newDoc[DocData][backKey] = FormattedTextBox.centeredTextCreator('answer', answer); - return newDoc; + const front = Docs.Create.CenteredTextCreator('question', question, {}, img); + const back = Docs.Create.CenteredTextCreator('answer', answer, {}); + if (useDoc) { + useDoc[DocData][frontKey] = front; + useDoc[DocData][backKey] = back; + return useDoc; + } + return Docs.Create.FlashcardDocument('flashcard', front, back, { _width: 300, _height: 300 }); }; return keyword && keyword.toLowerCase() !== 'none' ? ComparisonBox.fetchImages(keyword).then(img => fillInFlashcard(img)) : fillInFlashcard(); } @@ -98,6 +101,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent() .map(tuple => ComparisonBox.createFlashcard(tuple, front, back)) ).then(docs => { return Docs.Create.CarouselDocument(docs, { + title: 'flashcard deck', _width: width, _height: height, _layout_fitWidth: false, @@ -116,12 +120,11 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent() private _sideBtnWidth = 35; private _closeRef = React.createRef(); private _disposers: { [key: string]: DragManager.DragDropDisposer | undefined } = {}; - private _reactDisposer: IReactionDisposer | undefined; + private _reactDisposer: { [key: string]: IReactionDisposer } = {}; @observable private _inputValue = ''; @observable private _outputValue = ''; @observable private _loading = false; - @observable private _isEmpty = false; @observable private _childActive = false; @observable private _animating = ''; @observable private _listening = false; @@ -135,18 +138,28 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent() componentDidMount() { this._props.setContentViewBox?.(this); - this._reactDisposer = reaction( - () => this._props.isSelected(), // when this reaction should update + this._reactDisposer.select = reaction( + () => this._props.isSelected(), selected => { - if (selected && this.isFlashcard) this.activateContent(); + if (selected && this.revealOp !== flashcardRevealOp.SLIDE) this.activateContent(); !selected && (this._childActive = false); }, // what it should update to { fireImmediately: true } ); + this._reactDisposer.hover = reaction( + () => this._props.isContentActive(), + hover => { + if (!hover) { + this.revealOp === flashcardRevealOp.FLIP && this.animateFlipping(this.frontKey); + this.revealOp === flashcardRevealOp.SLIDE && this.animateSliding(this._props.PanelWidth() - 3); + } + }, // what it should update to + { fireImmediately: true } + ); } componentWillUnmount() { - this._reactDisposer?.(); + Object.values(this._reactDisposer).forEach(disposer => disposer?.()); } protected createDropTarget = (ele: HTMLDivElement | null, fieldKey: string) => { @@ -169,7 +182,8 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent() return undefined; }, 'internal drop'); - @computed get isQuizMode() { return DocCast(this.Document.embedContainer)?.practiceMode === practiceMode.QUIZ; } // prettier-ignore + @computed get containerDoc() { return this._props.docViewPath().slice(-2)[0]?.Document; } // prettier-ignore + @computed get isQuizMode() { return this.containerDoc?.practiceMode === practiceMode.QUIZ; } // prettier-ignore @computed get isFlashcard() { return BoolCast(this.Document.layout_isFlashcard); } // prettier-ignore @computed get frontKey() { return this._props.fieldKey + '_front'; } // prettier-ignore @computed get backKey() { return this._props.fieldKey + '_back'; } // prettier-ignore @@ -178,10 +192,10 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent() @computed get revealOpKey() { return `_${this._props.fieldKey}_revealOp`; } // prettier-ignore @computed get clipHeightKey() { return `_${this._props.fieldKey}_clipHeight`; } // prettier-ignore @computed get clipWidthKey() { return `_${this._props.fieldKey}_clipWidth`; } // prettier-ignore - @computed get clipWidth() { return NumCast(this.layoutDoc[this.clipWidthKey], 50); } // prettier-ignore + @computed get clipWidth() { return NumCast(this.layoutDoc[this.clipWidthKey], this.isFlashcard ? 100: 50); } // prettier-ignore @computed get clipHeight() { return NumCast(this.layoutDoc[this.clipHeightKey], 200); } // prettier-ignore - @computed get revealOp() { return StrCast(this.layoutDoc[this.revealOpKey], StrCast(this._props.docViewPath().slice(-2)[0]?.Document.revealOp)); } // prettier-ignore - set revealOp(value:string) { this.layoutDoc[this.revealOpKey] = value; } // prettier-ignore + @computed get revealOp() { return StrCast(this.layoutDoc[this.revealOpKey], StrCast(this.containerDoc?.revealOp, this.isFlashcard ? flashcardRevealOp.FLIP : flashcardRevealOp.SLIDE)) as flashcardRevealOp; } // prettier-ignore + @computed get revealOpHover() { return BoolCast(this.layoutDoc[this.revealOpKey+"_hover"], BoolCast(this.containerDoc?.revealOp_hover)); } // prettier-ignore @computed get loading() { return this._loading; } // prettier-ignore set loading(value) { runInAction(() => { this._loading = value; })} // prettier-ignore @@ -193,13 +207,13 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent() onPointerDown={e => setupMoveUpEvents(e.target, e, returnFalse, emptyFunction, () => { if (!this.revealOp || this.revealOp === flashcardRevealOp.FLIP) { - this.flipFlashcard(); + this.animateFlipping(); } }) } style={{ - background: this.revealOp === flashcardRevealOp.HOVER ? 'gray' : this._renderSide === this.backKey ? 'white' : 'black', - color: this.revealOp === flashcardRevealOp.HOVER ? 'black' : this._renderSide === this.backKey ? 'black' : 'white', + background: this.revealOpHover ? 'gray' : this._renderSide === this.backKey ? 'white' : 'black', + color: this.revealOpHover ? 'black' : this._renderSide === this.backKey ? 'black' : 'white', display: 'inline-block', }}> @@ -223,7 +237,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent() @computed get flashcardMenu() { return SnappingManager.HideDecorations ? null : (
- {this.revealOp === flashcardRevealOp.HOVER || !this._props.isSelected() ? null : this.overlayAlternateIcon} + {this.revealOpHover || !this._props.isSelected() ? null : this.overlayAlternateIcon} {!this._props.isSelected() || this._renderSide === this.frontKey ? null : ( Ask GPT to create an answer for the question on the front
}>
this.askGPT(GPTCallType.CHATCARD)}> @@ -296,13 +310,11 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent() moveDoc = (doc: Doc, addDocument: (document: Doc | Doc[]) => boolean, which: string) => this.remDoc(doc, which) && addDocument(doc); addDoc = (doc: Doc, which: string) => { - if (this.dataDoc[which] && !this._isEmpty) return false; this.dataDoc[which] = doc; return true; }; remDoc = (doc: Doc, which: string) => { if (this.dataDoc[which] === doc) { - this._isEmpty = true; this.dataDoc[which] = undefined; return true; } @@ -334,6 +346,26 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent() moveDocBack = (docs: Doc | Doc[], targetCol: Doc | undefined, addDoc: (doc: Doc | Doc[]) => boolean) => toList(docs).reduce((res, doc: Doc) => res && this.moveDoc(doc, addDoc, this.backKey), true); remDocFront = (docs: Doc | Doc[]) => toList(docs).reduce((res, doc) => res && this.remDoc(doc, this.frontKey), true); remDocBack = (docs: Doc | Doc[]) => toList(docs).reduce((res, doc) => res && this.remDoc(doc, this.backKey), true); + animateSliding = action((targetWidth: number) => { + this._animating = `all ${this._slideTiming}ms`; // on click, animate slider movement to the targetWidth + this.layoutDoc[this.clipWidthKey] = (targetWidth * 100) / this._props.PanelWidth(); + setTimeout(action(() => {this._animating = ''; }), this._slideTiming); // prettier-ignore + }); + + _flipAnim: NodeJS.Timeout | undefined; + animateFlipping = action((side?: string) => { + if (side !== this._renderSide) { + this._renderSide = side ?? (this._renderSide === this.frontKey ? this.backKey : this.frontKey); // switches to new front + this._animating = '0'; // reveals old front on the bottom layer by making top layer transparent + setTimeout( + action(() => { + this._animating = `all ${this._slideTiming * 5}ms`; // makes new front fade in + clearTimeout(this._flipAnim); + this._flipAnim = setTimeout( action(() => { this._animating = ''; }), this._slideTiming * 5 ); // prettier-ignore + }) + ); + } + }); registerSliding = (e: React.PointerEvent, targetWidth: number) => { if (e.button !== 2) { @@ -351,13 +383,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent() }), false, undefined, - action(() => { - if (!this._childActive) { - this._animating = `all ${this._slideTiming}ms`; // on click, animate slider movement to the targetWidth - this.layoutDoc[this.clipWidthKey] = (targetWidth * 100) / this._props.PanelWidth(); - setTimeout( action(() => {this._animating = ''; }), this._slideTiming); // prettier-ignore - } - }) + action(() => !this._childActive && this.animateSliding(targetWidth)) ); } }; @@ -584,16 +610,6 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent() } }; - @action - flipFlashcard = () => { - this._renderSide = this._renderSide === this.frontKey ? this.backKey : this.frontKey; - }; - - @action - hoverFlip = (side: string) => { - if (this.revealOp === flashcardRevealOp.HOVER) this._renderSide = side; - }; - flashcardContextMenu = () => { const appearance = ContextMenu.Instance.findByDescription('Appearance...'); const appearanceItems = appearance?.subitems ?? []; @@ -680,16 +696,8 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent() }; displayBox = (which: string, cover: number) => ( -
{ - this.registerSliding(e, cover); - this.isFlashcard && this.activateContent(); - }} - ref={ele => this.createDropTarget(ele, which)}> - {!this._isEmpty ? this.displayDoc(which) : null} +
this.registerSliding(e, cover)} ref={ele => this.createDropTarget(ele, which)}> + {this.displayDoc(which)}
); @@ -727,7 +735,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent() -
@@ -736,40 +744,33 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent() ); // if flashcard is rendered that has no data, then add some placeholders for question and answer - addPlaceholdersForEmptyFlashcard = () => { - if (this.dataDoc.data) { - if (!this.dataDoc[this.backKey] || !this.dataDoc[this.frontKey]) ComparisonBox.createFlashcard(StrCast(this.dataDoc.data), this.frontKey, this.backKey, this.Document); - } else { - // add text box to each side when comparison box is first created - if (!this.dataDoc[this.backKey] && !this._isEmpty) { - this.dataDoc[this.backKey] = FormattedTextBox.centeredTextCreator('answer', 'answer here', undefined, true); - } - - if (!this.dataDoc[this.frontKey] && !this._isEmpty) { - this.dataDoc[this.frontKey] = FormattedTextBox.centeredTextCreator('question', 'hint: Enter a topic, select this document and click the stack button to have GPT create a deck of cards', undefined, true); - } - } - }; - - renderAsFlashcard = () => ( + // addPlaceholdersForEmptyFlashcard = () => { + // if (this.dataDoc.data) { + // if (!this.dataDoc[this.backKey] || !this.dataDoc[this.frontKey]) ComparisonBox.createFlashcard(StrCast(this.dataDoc.data), this.frontKey, this.backKey, this.Document); + // } + // }; + + // render a button that flips between front and back + renderAsFlip = () => (
this.hoverFlip(this.backKey)} - onMouseLeave={() => this.hoverFlip(this.frontKey)}> - {this.displayBox(this._renderSide, this._props.PanelWidth() - 3)} - {this.loading ? ( -
- -
- ) : null} + style={{ display: 'flex', pointerEvents: this.revealOpHover && this._props.isContentActive() ? 'unset' : undefined }} // + onMouseEnter={() => this.revealOpHover && this.animateFlipping(this.backKey)} + onMouseLeave={() => this.revealOpHover && this.animateFlipping(this.frontKey)}> +
+ {this.displayBox(this._renderSide === this.backKey ? this.frontKey : this.backKey, 0)} +
+
{this.displayBox(this._renderSide, 0)}
{this.flashcardMenu}
); - // render a comparison box that compares items side by side + // render a slider that reveals front and back as slider is dragged horizonally renderAsBeforeAfter = () => ( -
+
this.revealOpHover && this.animateSliding(0)} + onMouseLeave={() => this.revealOpHover && this.animateSliding(this._props.PanelWidth() - 3)}> {this.displayBox(this.backKey, this._props.PanelWidth() - 3)}
{this.displayBox(this.frontKey, 0)} @@ -789,11 +790,20 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent() ); render() { - this.isFlashcard && this.addPlaceholdersForEmptyFlashcard(); - return this.isFlashcard ? - this.isQuizMode ? this.renderAsQuiz(this.frontText) : - this.renderAsFlashcard() : - this.renderAsBeforeAfter(); // prettier-ignore + const renderMode = new Map JSX.Element>([ + [flashcardRevealOp.FLIP, this.renderAsFlip], + [flashcardRevealOp.SLIDE, this.renderAsBeforeAfter]]); // prettier-ignore + if (this.isQuizMode) this.renderAsQuiz(this.frontText); + return ( +
+ {renderMode.get(this.revealOp)?.() ?? null} + {this.loading ? ( +
+ +
+ ) : null} +
+ ); } } diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index 9d3a899f5..29be8d285 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -76,26 +76,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent Date: Mon, 28 Oct 2024 20:07:51 -0400 Subject: reworked LabelBox's to be fully editable. gor rid of singleLine for label boxes. made a text_fitBox field to render text with a labelBox. fixed styleprovider in freeformview clusters to be untracked() to avoid invalidations. added a default text color to Settings panel. fixed clicking text buttons to not lose focus in labelBox. --- package-lock.json | 8 +- package.json | 2 +- src/client/documents/DocUtils.ts | 5 +- src/client/documents/Documents.ts | 14 +- src/client/util/CurrentUserUtils.ts | 9 +- src/client/util/SettingsManager.tsx | 24 +- src/client/views/MarqueeAnnotator.tsx | 3 +- src/client/views/PinFuncs.ts | 4 +- src/client/views/SidebarAnnos.tsx | 4 +- src/client/views/StyleProvider.tsx | 4 +- src/client/views/StyleProviderQuiz.tsx | 2 +- .../views/collections/CollectionCardDeckView.tsx | 1 - .../CollectionFreeFormClusters.ts | 6 +- .../collections/collectionSchema/SchemaRowBox.tsx | 2 +- src/client/views/global/globalScripts.ts | 24 +- .../views/nodes/DataVizBox/DocCreatorMenu.tsx | 35 +-- src/client/views/nodes/DocumentView.tsx | 12 +- src/client/views/nodes/EquationBox.tsx | 2 +- .../views/nodes/FontIconBox/FontIconBox.scss | 4 + src/client/views/nodes/FontIconBox/FontIconBox.tsx | 4 +- src/client/views/nodes/LabelBox.scss | 1 - src/client/views/nodes/LabelBox.tsx | 168 +++++++++---- .../views/nodes/formattedText/FormattedTextBox.tsx | 259 +++++++++++---------- .../formattedText/ProsemirrorExampleTransfer.ts | 4 +- .../views/nodes/formattedText/RichTextMenu.tsx | 71 +++--- .../views/nodes/formattedText/RichTextRules.ts | 2 +- src/client/views/nodes/trails/PresBox.tsx | 14 +- src/client/views/pdf/GPTPopup/GPTPopup.tsx | 23 +- src/fields/documentSchemas.ts | 7 +- 29 files changed, 426 insertions(+), 292 deletions(-) (limited to 'src/client/documents/DocUtils.ts') diff --git a/package-lock.json b/package-lock.json index 4e95fcee0..43dcb1c3f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -72,7 +72,7 @@ "body-parser": "^1.20.2", "bootstrap": "^5.3.2", "brotli": "^1.3.3", - "browndash-components": "^0.1.50", + "browndash-components": "0.1.53", "browser-assert": "^1.2.1", "bson": "^6.2.0", "canvas": "^2.11.2", @@ -13575,9 +13575,9 @@ } }, "node_modules/browndash-components": { - "version": "0.1.50", - "resolved": "https://registry.npmjs.org/browndash-components/-/browndash-components-0.1.50.tgz", - "integrity": "sha512-8EgU82os8/Tg3gayh+nYXCQZ8P7GGQWnibz0U2RM0sXTbc3BWTSYiC1Sj1VpL7HPkZ1KMGHzWynCo9QKIOWZBg==", + "version": "0.1.53", + "resolved": "https://registry.npmjs.org/browndash-components/-/browndash-components-0.1.53.tgz", + "integrity": "sha512-lBZlXe2u/6d9NlrR+fH+9ncCFGRUxGqJRejpfhbhVmQ4Q4uvP9LlmTuaVQElqys5wGUyK0SuqrLYPGhJthdQLQ==", "dependencies": { "@emotion/react": "^11.11.1", "@emotion/styled": "^11.11.0", diff --git a/package.json b/package.json index 06179fd7d..63d5e387d 100644 --- a/package.json +++ b/package.json @@ -155,7 +155,7 @@ "body-parser": "^1.20.2", "bootstrap": "^5.3.2", "brotli": "^1.3.3", - "browndash-components": "^0.1.50", + "browndash-components": "0.1.53", "browser-assert": "^1.2.1", "bson": "^6.2.0", "canvas": "^2.11.2", diff --git a/src/client/documents/DocUtils.ts b/src/client/documents/DocUtils.ts index 19f3c89ef..e3cb5e72c 100644 --- a/src/client/documents/DocUtils.ts +++ b/src/client/documents/DocUtils.ts @@ -679,10 +679,13 @@ export namespace DocUtils { ? {} // if the new doc will inherit from a template, don't set any layout fields since that would block the inheritance : { _width: width || 200, - _height: 35, + _height: BoolCast(Doc.UserDoc().fitBox) ? 70 : 35, _layout_centered: BoolCast(Doc.UserDoc()._layout_centered), _layout_fitWidth: true, _layout_autoHeight: true, + text_fitBox: BoolCast(Doc.UserDoc().fitBox), + text_align: StrCast(Doc.UserDoc().textAlign), + text_fontColor: StrCast(Doc.UserDoc().fontColor), }), }); diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index 98e8bc932..b0e1a7545 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -294,7 +294,6 @@ export class DocumentOptions { _yMargin?: NUMt = new NumInfo('gap between top edge of dcoument and start of masonry/stacking layouts', false); _xPadding?: NUMt = new NumInfo('x padding', false); _yPadding?: NUMt = new NumInfo('y padding', false); - _singleLine?: boolean; // whether label box is restricted to one line of text _createDocOnCR?: boolean; // whether carriage returns and tabs create new text documents _columnWidth?: NUMt = new NumInfo('width of table column', false); _columnsHideIfEmpty?: BOOLt = new BoolInfo('whether stacking view column headings should be hidden'); @@ -302,10 +301,14 @@ export class DocumentOptions { _caption_yMargin?: NUMt = new NumInfo('y margin of caption inside of a carousel collection', false, true); icon_nativeWidth?: NUMt = new NumInfo('native width of icon view', false, true); icon_nativeHeight?: NUMt = new NumInfo('native height of icon view', false, true); - _text_fontSize?: string; - _text_fontFamily?: string; - _text_fontWeight?: string; - text_align?: STRt = new StrInfo('horizontal text alignment default'); + text_fontSize?: string; + text_fontFamily?: string; + text_fontWeight?: string; + text_fitBox?: BOOLt = new BoolInfo("whether text box should be scaled to fit it's containing render box"); + text_align?: STRt = new StrInfo('horizontal text alignment default', undefined, undefined, ['left', 'center', 'right']); + title_align?: STRt = new StrInfo('horizontal title alignment in label box', undefined, undefined, ['left', 'center', 'right']); + title_transform?: STRt = new StrInfo('transformation to apply to title in label box (eg., uppercase)', undefined, undefined, ['uppercase', 'lowercase', 'capitalize']); + text_transform?: STRt = new StrInfo('transformation to apply to text in text box (eg., uppercase)', undefined, undefined, ['uppercase', 'lowercase', 'capitalize']); text_placeholder?: BOOLt = new BoolInfo('makes the text act like a placeholder and automatically select when the text box is selected'); fontSize?: string; _pivotField?: string; // field key used to determine headings for sections in stacking, masonry, pivot views @@ -496,7 +499,6 @@ export class DocumentOptions { sidebar_type_collection?: string; // collection type of text sidebar data_dashboards?: List; // 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_ where type is the Doc's type(pdf,rich text, etc)) diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index 30c75c659..b3aa5e145 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -186,7 +186,7 @@ export class CurrentUserUtils { const templateIconsDoc = DocUtils.AssignOpts(DocCast(doc[field]), reqdOpts) ?? (doc[field] = Docs.Create.TreeDocument([], reqdOpts)); const labelBox = (opts: DocumentOptions, fieldKey:string) => Docs.Create.LabelDocument({ - layout: LabelBox.LayoutString(fieldKey), textTransform: "unset", letterSpacing: "unset", _singleLine: false, _label_minFontSize: 14, _label_maxFontSize: 14, layout_borderRounding: "5px", _width: 150, _height: 70, _xPadding: 10, _yPadding: 10, ...opts + layout: LabelBox.LayoutString(fieldKey), letterSpacing: "unset", _label_minFontSize: 14, _label_maxFontSize: 14, layout_borderRounding: "5px", _width: 150, _height: 70, _xPadding: 10, _yPadding: 10, ...opts }); const imageBox = (opts: DocumentOptions, fieldKey:string) => Docs.Create.ImageDocument( "http://www.cs.brown.edu/~bcz/noImage.png", { layout:ImageBox.LayoutString(fieldKey), "icon_nativeWidth": 360 / 4, "icon_nativeHeight": 270 / 4, iconTemplate:DocumentType.IMG, _width: 360 / 4, _height: 270 / 4, _layout_showTitle: "title", ...opts }); const fontBox = (opts:DocumentOptions, fieldKey:string) => Docs.Create.FontIconDocument({ layout:FontIconBox.LayoutString(fieldKey), _nativeHeight: 30, _nativeWidth: 30, _width: 30, _height: 30, ...opts }); @@ -268,7 +268,7 @@ export class CurrentUserUtils { MakeTemplate(Docs.Create.MultirowDocument( [ Docs.Create.MulticolumnDocument([], { title: "hero", _height: 200, isSystem: true }), - Docs.Create.TextDocument("", { title: "text", _layout_fitWidth:true, _height: 100, isSystem: true, _text_fontFamily: StrCast(Doc.UserDoc().fontFamily), _text_fontSize: StrCast(Doc.UserDoc().fontSize) }) + Docs.Create.TextDocument("", { title: "text", _layout_fitWidth:true, _height: 100, isSystem: true, text_fontFamily: StrCast(Doc.UserDoc().fontFamily), text_fontSize: StrCast(Doc.UserDoc().fontSize) }) ], {...opts, title: "Slide View Template"})); const plotlyApi = () => { let plotly = Doc.MyPublishedDocs.find(fdoc => fdoc.title === "@plotly"); @@ -392,7 +392,7 @@ pie title Minerals in my tap water {key: "Trail", creator: Docs.Create.PresDocument, opts: { _width: 400, _height: 30, _type_collection: CollectionViewType.Stacking, _layout_dontCenter:'xy', dropAction: dropActionType.embed, treeView_HideTitle: true, _layout_fitWidth:true, layout_boxShadow: "0 0" }}, {key: "Tab", creator: opts => Docs.Create.FreeformDocument([], opts), opts: { _width: 500, _height: 800, _layout_fitWidth: true, _freeform_backgroundGrid: true, }}, {key: "Slide", creator: opts => Docs.Create.TreeDocument([], opts), opts: { _width: 300, _height: 200, _type_collection: CollectionViewType.Tree, - treeView_HasOverlay: true, _text_fontSize: "20px", _layout_autoHeight: true, + treeView_HasOverlay: true, text_fontSize: "20px", _layout_autoHeight: true, dropAction:dropActionType.move, treeView_Type: TreeViewType.outline, backgroundColor: "white", _xMargin: 0, _yMargin: 0, _createDocOnCR: true }, funcs: {title: 'this.text?.Text'}}, @@ -731,7 +731,7 @@ pie title Minerals in my tap water return [ { title: "Font", toolTip: "Font", width: 100, btnType: ButtonType.DropdownList, toolType:"font", ignoreClick: true, scripts: {script: '{ return setFontAttr(this.toolType, value, _readOnly_);}'}, btnList: new List(["Roboto", "Roboto Mono", "Nunito", "Times New Roman", "Arial", "Georgia", "Comic Sans MS", "Tahoma", "Impact", "Crimson Text"]) }, - { title: "Font Size",toolTip: "Font size (%size)", btnType: ButtonType.NumberDropdownButton, toolType:"fontSize", ignoreClick: true, scripts: {script: '{ return setFontAttr(this.toolType, value, _readOnly_);}'}, numBtnMax: 200, numBtnMin: 6 }, + { title: "Font Size",toolTip: "Font size (%size)", btnType: ButtonType.NumberDropdownButton, toolType:"fontSize", ignoreClick: true, scripts: {script: '{ return setFontAttr(this.toolType, value, _readOnly_);}'}, numBtnMax: 200, numBtnMin: 9 }, { title: "Color", toolTip: "Font color (%color)", btnType: ButtonType.ColorButton, icon: "font", toolType:"fontColor",ignoreClick: true, scripts: {script: '{ return setFontAttr(this.toolType, value, _readOnly_);}'} }, { title: "Highlight",toolTip: "Font highlight", btnType: ButtonType.ColorButton, icon: "highlighter", toolType:"highlight",ignoreClick: true, scripts: {script: '{ return setFontAttr(this.toolType, value, _readOnly_);}'} }, { title: "Bold", toolTip: "Bold (Ctrl+B)", btnType: ButtonType.ToggleButton, icon: "bold", toolType:"bold", ignoreClick: true, scripts: {onClick: '{ return toggleCharStyle(this.toolType, _readOnly_);}'} }, @@ -746,6 +746,7 @@ pie title Minerals in my tap water { title: "Center", toolTip: "Center align (Cmd-\\)",btnType: ButtonType.ToggleButton, icon: "align-center",toolType:"center",ignoreClick: true, scripts: {onClick: '{ return toggleCharStyle(this.toolType, _readOnly_);}'} }, { title: "Right", toolTip: "Right align (Cmd-])", btnType: ButtonType.ToggleButton, icon: "align-right", toolType:"right", ignoreClick: true, scripts: {onClick: '{ return toggleCharStyle(this.toolType, _readOnly_);}'} }, ]}, + { title: "Fit Box", toolTip: "Fit text to box", btnType: ButtonType.ToggleButton, icon: "object-group",toolType:"fitBox", ignoreClick: true, scripts: {onClick: '{ return toggleCharStyle(this.toolType, _readOnly_);}'} }, { title: "Elide", toolTip: "Elide selection", btnType: ButtonType.ToggleButton, icon: "eye", toolType:"elide", ignoreClick: true, scripts: {onClick: '{ return toggleCharStyle(this.toolType, _readOnly_);}'} }, { title: "Dictate", toolTip: "Dictate", btnType: ButtonType.ToggleButton, icon: "microphone", toolType:"dictation", ignoreClick: true, scripts: {onClick: '{ return toggleCharStyle(this.toolType, _readOnly_);}'} }, { title: "NoLink", toolTip: "Auto Link", btnType: ButtonType.ToggleButton, icon: "link", toolType:"noAutoLink", expertMode:true, scripts: {onClick: '{ return toggleCharStyle(this.toolType, _readOnly_);}'}, funcs: {hidden: 'IsNoviceMode()'}}, diff --git a/src/client/util/SettingsManager.tsx b/src/client/util/SettingsManager.tsx index 9200d68db..33d7d90a8 100644 --- a/src/client/util/SettingsManager.tsx +++ b/src/client/util/SettingsManager.tsx @@ -1,5 +1,5 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { Button, ColorPicker, Dropdown, DropdownType, EditableText, Group, NumberDropdown, Size, Toggle, ToggleType, Type } from 'browndash-components'; +import { Button, ColorPicker, Colors, Dropdown, DropdownType, EditableText, Group, NumberDropdown, Size, Toggle, ToggleType, Type } from 'browndash-components'; import { action, computed, makeObservable, observable, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; @@ -45,7 +45,6 @@ export class SettingsManager extends React.Component { public closeMgr = action(() => { this._isOpen = false; }); - // eslint-disable-next-line react/no-unused-class-component-methods public openMgr = action(() => { this._isOpen = true; }); @@ -354,6 +353,27 @@ export class SettingsManager extends React.Component { Doc.UserDoc().fontSize = val + 'px'; }} /> + } + 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; + }} + /> ({ text: val, diff --git a/src/client/views/MarqueeAnnotator.tsx b/src/client/views/MarqueeAnnotator.tsx index 7266875c5..fa1123a2d 100644 --- a/src/client/views/MarqueeAnnotator.tsx +++ b/src/client/views/MarqueeAnnotator.tsx @@ -34,7 +34,7 @@ export interface MarqueeAnnotatorProps { getPageFromScroll?: (top: number) => number; finishMarquee: (x?: number, y?: number) => void; anchorMenuClick?: () => undefined | ((anchor: Doc) => void); - anchorMenuFlashcard?: () => Promise; + anchorMenuFlashcard?: () => Promise; anchorMenuCrop?: (anchor: Doc | undefined, addCrop: boolean) => Doc | undefined; highlightDragSrcColor?: string; } @@ -220,7 +220,6 @@ export class MarqueeAnnotator extends ObservableReactComponent (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, props: Opt, props: Opt, props: Opt, property: string) => { - if (doc && this.childDocs?.includes(doc)) + // without untracked, every inquired style property for any Doc will be invalidated if a change is made to the collection's childDocs. + // this prevents that by assuming that a Doc is generally always (or never) a member of childDocs - if it's removed or added, then all of its properties get updated anyway. + if (doc && untracked(() => this.childDocs)?.includes(doc)) switch (property.split(':')[0]) { case StyleProp.BackgroundColor: { diff --git a/src/client/views/collections/collectionSchema/SchemaRowBox.tsx b/src/client/views/collections/collectionSchema/SchemaRowBox.tsx index 6ffb0865a..7f519b065 100644 --- a/src/client/views/collections/collectionSchema/SchemaRowBox.tsx +++ b/src/client/views/collections/collectionSchema/SchemaRowBox.tsx @@ -188,7 +188,7 @@ export class SchemaRowBox extends ViewBoxBaseComponent() { selectedCells={this.selectedCells} selectedCol={this.selectedCol} setColumnValues={this.setColumnValues} - oneLine={BoolCast(this.schemaDoc?._singleLine)} + oneLine={BoolCast(this.schemaDoc?._schema_singleLine)} menuTarget={this.schemaView.MenuTarget} transform={() => { const ind = index === this.schemaView.columnKeys.length - 1 ? this.schemaView.columnKeys.length - 3 : index; diff --git a/src/client/views/global/globalScripts.ts b/src/client/views/global/globalScripts.ts index 954c79f7d..013b18a97 100644 --- a/src/client/views/global/globalScripts.ts +++ b/src/client/views/global/globalScripts.ts @@ -295,10 +295,10 @@ ScriptingGlobals.add(function setTagFilter(tag: string, added: boolean, checkRes }, ''); // eslint-disable-next-line prefer-arrow-callback -ScriptingGlobals.add(function setFontAttr(attr: 'font' | 'fontColor' | 'highlight' | 'fontSize' | 'alignment', value: string | number, checkResult?: boolean) { +ScriptingGlobals.add(function setFontAttr(attr: 'font' | 'fontColor' | 'highlight' | 'fontSize', value: string | number, checkResult?: boolean) { const editorView = RichTextMenu.Instance?.TextView?.EditorView; // prettier-ignore - const map: Map<'font'|'fontColor'|'highlight'|'fontSize'|'alignment', { checkResult: () => string | undefined; setDoc: () => void;}> = new Map([ + const map: Map<'font'|'fontColor'|'highlight'|'fontSize', { checkResult: () => string | undefined; setDoc: () => void;}> = new Map([ ['font', { checkResult: () => RichTextMenu.Instance?.fontFamily, setDoc: () => value && RichTextMenu.Instance?.setFontField(value.toString(), 'fontFamily'), @@ -311,10 +311,6 @@ ScriptingGlobals.add(function setFontAttr(attr: 'font' | 'fontColor' | 'highligh checkResult: () => RichTextMenu.Instance?.fontColor, setDoc: () => value && RichTextMenu.Instance?.setFontField(value.toString(), 'fontColor'), }], - ['alignment', { - checkResult: () => RichTextMenu.Instance?.textAlign, - setDoc: () => { value && editorView?.state ? RichTextMenu.Instance?.align(editorView, editorView.dispatch, value.toString() as "center"|"left"|"right"):(Doc.UserDoc().textAlign = value); }, - }], ['fontSize', { checkResult: () => RichTextMenu.Instance?.fontSize.replace('px', ''), setDoc: () => { @@ -334,7 +330,7 @@ ScriptingGlobals.add(function setFontAttr(attr: 'font' | 'fontColor' | 'highligh return undefined; }); -type attrname = 'noAutoLink' | 'dictation' | 'bold' | 'italics' | 'elide' | 'underline' | 'left' | 'center' | 'right' | 'vcent' | 'bullet' | 'decimal'; +type attrname = 'noAutoLink' | 'dictation' | 'fitBox' | 'bold' | 'italics' | 'elide' | 'underline' | 'left' | 'center' | 'right' | 'vcent' | 'bullet' | 'decimal'; type attrfuncs = [attrname, { checkResult: () => boolean; toggle?: () => unknown }]; // eslint-disable-next-line prefer-arrow-callback @@ -343,14 +339,10 @@ ScriptingGlobals.add(function toggleCharStyle(charStyle: attrname, checkResult?: const editorView = textView?.EditorView; // prettier-ignore const alignments:attrfuncs[] = (['left','right','center','vcent'] as ("left"|"center"|"right"|"vcent")[]).map((where) => - [ where, { checkResult: () => editorView ? (where === 'vcent' ? RichTextMenu.Instance?.textVcenter ?? false: - (RichTextMenu.Instance?.textAlign === where)): - where === 'vcent' ? BoolCast(Doc.UserDoc()._layout_centered): - (Doc.UserDoc().textAlign === where), - toggle: () => { editorView?.state ? (where === 'vcent' ? RichTextMenu.Instance?.vcenterToggle(): - RichTextMenu.Instance?.align(editorView, editorView.dispatch, where)): - where === 'vcent' ? Doc.UserDoc()._layout_centered = !Doc.UserDoc()._layout_centered: - (Doc.UserDoc().textAlign = where); } + [ where, { checkResult: () => (where === 'vcent' ? RichTextMenu.Instance?.textVcenter ?? false: + (RichTextMenu.Instance?.textAlign === where)), + toggle: () => { (where === 'vcent' ? RichTextMenu.Instance?.vcenterToggle(): + RichTextMenu.Instance?.align(editorView, editorView?.dispatch, where)); } }]); // prettier-ignore // prettier-ignore const listings:attrfuncs[] = (['bullet','decimal'] as attrname[]).map(list => @@ -360,6 +352,8 @@ ScriptingGlobals.add(function toggleCharStyle(charStyle: attrname, checkResult?: const attrs:attrfuncs[] = [ ['dictation', { checkResult: () => !!textView?._recordingDictation, toggle: () => textView && runInAction(() => { textView._recordingDictation = !textView._recordingDictation;} ) }], + ['fitBox', { checkResult: () => RichTextMenu.Instance?.fitBox ?? false, + toggle: () => RichTextMenu.Instance?.toggleFitBox()}], ['elide', { checkResult: () => false, toggle: () => editorView ? RichTextMenu.Instance?.elideSelection(): 0}], ['noAutoLink',{ checkResult: () => ((editorView && RichTextMenu.Instance?.noAutoLink) ?? false), diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu.tsx b/src/client/views/nodes/DataVizBox/DocCreatorMenu.tsx index 16c016d6c..7c601185e 100644 --- a/src/client/views/nodes/DataVizBox/DocCreatorMenu.tsx +++ b/src/client/views/nodes/DataVizBox/DocCreatorMenu.tsx @@ -245,7 +245,7 @@ export class DocCreatorMenu extends ObservableReactComponent { } updateIcons = (docs: Doc[]) => { - console.log('called') + console.log('called'); docs.map(this.getIcon); }; @@ -416,7 +416,7 @@ export class DocCreatorMenu extends ObservableReactComponent { 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 { @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 = (
{ 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 { yPadding={0} />
- + ); return (
-
+
{rendered}
-
- ); } @@ -2260,11 +2266,11 @@ export class FieldUtils { title: title, x: coord.x, y: coord.y, - _text_fontSize: `${FieldUtils.calculateFontSize(width, height, content, true)}`, + text_fontSize: `${FieldUtils.calculateFontSize(width, height, content, true)}`, backgroundColor: opts.backgroundColor ?? '', text_fontColor: opts.color, contentBold: opts.fontBold, - textTransform: opts.fontTransform, + text_transform: opts.fontTransform, color: opts.color, _layout_borderRounding: `${opts.cornerRounding ?? 0}px`, borderColor: opts.borderColor, @@ -2306,7 +2312,7 @@ export class FieldUtils { public static CarouselField = (coords: { tl: [number, number]; br: [number, number] }, parentWidth: number, parentHeight: number, title: string, fields: Doc[]) => { const { width, height, coord } = FieldUtils.getDimensions(coords, parentWidth, parentHeight); - const doc = Docs.Create.Carousel3DDocument(fields, { _height: height, _width: width, title: title, x: coord.x, y: coord.y, _text_fontSize: `${height / 2}` }); + const doc = Docs.Create.Carousel3DDocument(fields, { _height: height, _width: width, title: title, x: coord.x, y: coord.y, text_fontSize: `${height / 2}` }); return doc; }; @@ -2359,4 +2365,3 @@ export class FieldUtils { // }] // }; // } - diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index a343b9a39..eb7f333b9 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -880,7 +880,7 @@ export class DocumentViewInternal extends DocComponent{renderDoc} - case PresEffect.Flip: return {renderDoc} - case PresEffect.Rotate: return {renderDoc} - case PresEffect.Bounce: return {renderDoc} - case PresEffect.Roll: return {renderDoc} + 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 {renderDoc} // case PresEffect.Fade: return {renderDoc} case PresEffect.Fade: return {renderDoc} // keep as preset, doesn't really make sense with spring config diff --git a/src/client/views/nodes/EquationBox.tsx b/src/client/views/nodes/EquationBox.tsx index fefe25764..290c90d6e 100644 --- a/src/client/views/nodes/EquationBox.tsx +++ b/src/client/views/nodes/EquationBox.tsx @@ -122,7 +122,7 @@ export class EquationBox extends ViewBoxBaseComponent() { 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()}> diff --git a/src/client/views/nodes/FontIconBox/FontIconBox.scss b/src/client/views/nodes/FontIconBox/FontIconBox.scss index 2db285910..ab03a2318 100644 --- a/src/client/views/nodes/FontIconBox/FontIconBox.scss +++ b/src/client/views/nodes/FontIconBox/FontIconBox.scss @@ -10,6 +10,10 @@ height: 3px !important; } } +.fonticonbox { + margin: auto; + width: 100%; +} .menuButton { height: 100%; display: flex; diff --git a/src/client/views/nodes/FontIconBox/FontIconBox.tsx b/src/client/views/nodes/FontIconBox/FontIconBox.tsx index d4898eb3c..b45774a75 100644 --- a/src/client/views/nodes/FontIconBox/FontIconBox.tsx +++ b/src/client/views/nodes/FontIconBox/FontIconBox.tsx @@ -262,11 +262,13 @@ export class FontIconBox extends ViewBoxBaseComponent() { 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 ( () { render() { return ( -
+
{this.renderButton()}
); 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() { @@ -22,7 +25,8 @@ export class LabelBox extends ViewBoxBaseComponent() { } 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() { } }; - @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() { 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() { 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 ( -
+
{ + 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: `${this.Title.startsWith('#') ? null : (this.Title ?? '')}`, }} 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} -
+ }} + />
); @@ -161,9 +249,9 @@ export class LabelBox extends ViewBoxBaseComponent() { Docs.Prototypes.TemplateMap.set(DocumentType.LABEL, { layout: { view: LabelBox, dataField: 'title' }, - options: { acl: '', _singleLine: true, _layout_nativeDimEditable: true, _layout_reflowHorizontal: true, _layout_reflowVertical: true }, + options: { acl: '', _layout_nativeDimEditable: true, _layout_reflowHorizontal: true, _layout_reflowVertical: true, title_align: 'center', title_transform: 'uppercase' }, }); Docs.Prototypes.TemplateMap.set(DocumentType.BUTTON, { layout: { view: LabelBox, dataField: 'title' }, - options: { acl: '', _layout_nativeDimEditable: true, _layout_reflowHorizontal: true, _layout_reflowVertical: true }, + options: { acl: '', _layout_nativeDimEditable: true, _layout_reflowHorizontal: true, _layout_reflowVertical: true, title_align: 'center', title_transform: 'uppercase' }, }); diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index 2aee7b6b3..659cda9dd 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -65,6 +65,7 @@ import { RichTextMenu, RichTextMenuPlugin } from './RichTextMenu'; import { RichTextRules } from './RichTextRules'; import { schema } from './schema_rts'; import { Property } from 'csstype'; +import { LabelBox } from '../LabelBox'; // import * as applyDevTools from 'prosemirror-dev-tools'; export interface FormattedTextBoxProps extends FieldViewProps { @@ -158,6 +159,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent void = unimplementedFunction; public addToCollection: ((doc: Doc | Doc[], annotationKey?: string | undefined) => boolean) | undefined; @@ -175,10 +177,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent { const rootDoc: Doc = Doc.isTemplateDoc(this._props.docViewPath().lastElement()?.Document) ? this.Document : DocCast(this.Document.rootDocument, this.Document); - if (!pinProps && this._editorView?.state.selection.empty) return rootDoc; + if (!pinProps && this.EditorView?.state.selection.empty) return rootDoc; const anchor = Docs.Create.ConfigDocument({ title: StrCast(rootDoc.title), annotationOn: rootDoc }); this.addDocument(anchor); this._finishingLink = true; @@ -263,7 +265,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent this._editorView?.state && RichTextMenu.Instance?.setFontField(color, 'fontHighlight'), 'highlght text'); + AnchorMenu.Instance.Highlight = undoable((color: string) => this.EditorView?.state && RichTextMenu.Instance?.setFontField(color, 'fontHighlight'), 'highlght text'); AnchorMenu.Instance.onMakeAnchor = () => this.getAnchor(true); AnchorMenu.Instance.StartCropDrag = unimplementedFunction; /** @@ -292,7 +294,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent; try { @@ -309,7 +311,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent { - if (node.type === this._editorView?.state.schema.nodes.dashField) { + if (node.type === this.EditorView?.state.schema.nodes.dashField) { const refDoc = !node.attrs.docId ? DocCast(this.Document.rootDocument, this.Document) : (DocServer.GetCachedRefField(node.attrs.docId as string) as Doc); const fieldKey = StrCast(node.attrs.fieldKey); return ( @@ -320,16 +322,16 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent { - if (this._editorView && !this._editorView.isDestroyed) { - const state = this._editorView.state.apply(tx); - this._editorView.updateState(state); + if (this.EditorView && !this.EditorView.isDestroyed) { + const state = this.EditorView.state.apply(tx); + this.EditorView.updateState(state); this.tryUpdateDoc(false); } }; tryUpdateDoc = (force: boolean) => { - if (this._editorView) { - const { state } = this._editorView; + if (this.EditorView) { + const { state } = this.EditorView; const { dataDoc } = this; const newText = state.doc.textBetween(0, state.doc.content.size, ' \n', this.leafText); const newJson = JSON.stringify(state.toJSON()); @@ -372,7 +374,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent term.title).forEach(term => { tr = this.hyperlinkTerm(tr, term, newAutoLinks); }); tr = tr.setSelection(new TextSelection(tr.doc.resolve(from), tr.doc.resolve(to))); - this._editorView?.dispatch(tr); + this.EditorView?.dispatch(tr); } oldAutoLinks.filter(oldLink => !newAutoLinks.has(oldLink) && oldLink.link_anchor_2 !== this.Document).forEach(doc => Doc.DeleteLink?.(doc)); }; @@ -450,11 +452,11 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent) => { let tr = trIn; - const editorView = this._editorView; + const editorView = this.EditorView; if (editorView && !Doc.AreProtosEqual(target, this.Document)) { const autoLinkTerm = Field.toString(target.title as FieldType).replace(/^@/, ''); let alink: Doc | undefined; @@ -553,18 +555,18 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent { - if (this._editorView) { - const { state } = this._editorView; + if (this.EditorView) { + const { state } = this.EditorView; if (state) { const mark = state.schema.mark(state.schema.marks.search_highlight); const activeMark = state.schema.mark(state.schema.marks.search_highlight, { selected: true }); const end = state.doc.nodeSize - 2; - this._editorView.dispatch(state.tr.removeMark(0, end, mark).removeMark(0, end, activeMark)); + this.EditorView.dispatch(state.tr.removeMark(0, end, mark).removeMark(0, end, activeMark)); } } }; adoptAnnotation = (start: number, end: number, mark: Mark) => { - const view = this._editorView!; + const view = this.EditorView!; const nmark = view.state.schema.marks.user_mark.create({ ...mark.attrs, userid: ClientUtils.CurrentUserEmail() }); view.dispatch(view.state.tr.removeMark(start, end, nmark).addMark(start, end, nmark)); }; @@ -616,7 +618,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent node that wraps the hyerlink while (target && (!(target instanceof HTMLElement) || !target.dataset?.targethrefs)) target = target.parentElement; - const editor = this._editorView; + const editor = this.EditorView; if (editor && target && !(e.nativeEvent instanceof simMouseEvent ? e.nativeEvent.dash : false)) { const hrefs = (target.dataset?.targethrefs as string) ?.trim() @@ -996,8 +998,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent { if (resIndex < newText.length) { - const marks = this._editorView?.state.storedMarks ?? []; - this._editorView?.dispatch(this._editorView?.state.tr.insertText(newText[resIndex]).setStoredMarks(marks)); + const marks = this.EditorView?.state.storedMarks ?? []; + this.EditorView?.dispatch(this.EditorView?.state.tr.insertText(newText[resIndex]).setStoredMarks(marks)); setTimeout(() => this.animateRes(resIndex + 1, newText), 20); } }; @@ -1035,8 +1035,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent { - if (this._editorView && this._recordingDictation) { + if (this.EditorView && this._recordingDictation) { this.stopDictation(/* true */); this._break = true; - const { state } = this._editorView; + const { state } = this.EditorView; const { to } = state.selection; const updated = TextSelection.create(state.doc, to, to); - this._editorView.dispatch(state.tr.setSelection(updated).insert(to, state.schema.nodes.paragraph.create({}))); + this.EditorView.dispatch(state.tr.setSelection(updated).insert(to, state.schema.nodes.paragraph.create({}))); if (this._recordingDictation) { this.recordDictation(); } @@ -1082,7 +1082,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent DictationManager.Controls.stop(/* !abort */); setDictationContent = (value: string) => { - if (this._editorView && this._recordingStart) { + if (this.EditorView && this._recordingStart) { if (this._break) { const textanchorFunc = () => { const tanch = Docs.Create.ConfigDocument({ title: 'dictation anchor' }); @@ -1095,22 +1095,22 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent { const examinedNode = findAnchorNode(node, editor); - if (examinedNode?.node && (examinedNode.node.textContent || examinedNode.node.type === this._editorView?.state.schema.nodes.dashDoc || examinedNode.node.type === this._editorView?.state.schema.nodes.audiotag)) { + if (examinedNode?.node && (examinedNode.node.textContent || examinedNode.node.type === this.EditorView?.state.schema.nodes.dashDoc || examinedNode.node.type === this.EditorView?.state.schema.nodes.audiotag)) { nodes.push(examinedNode.node); !hadStart && (start = index + examinedNode.start); hadStart = true; @@ -1186,13 +1186,13 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent { - if (node.type === this._editorView?.state.schema.nodes.audiotag) { + if (node.type === this.EditorView?.state.schema.nodes.audiotag) { if (node.attrs.textId === textAnchorId) { return { node, start: 0 }; } return undefined; } - if (node.type === this._editorView?.state.schema.nodes.dashDoc) { + if (node.type === this.EditorView?.state.schema.nodes.dashDoc) { if (node.attrs.docId === textAnchorId) { return { node, start: 0 }; } @@ -1208,9 +1208,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent= 0 && (ret.frag.size || (firstChild && [state.schema.nodes.dashDoc, state.schema.nodes.audioTag].includes(firstChild.type)))) { @@ -1219,7 +1219,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent= '0' && textAnchorId[0] <= '9' ? `\\3${textAnchorId[0]} ${textAnchorId.substr(1)}` : textAnchorId; addStyleSheetRule(FormattedTextBox._highlightStyleSheet, `${escAnchorId}`, { background: 'yellow', transform: 'scale(3)', 'transform-origin': 'left bottom' }); setTimeout(() => { @@ -1307,15 +1307,15 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent { - if (this._editorView && this._applyingChange !== this.fieldKey) { + if (this.EditorView && this._applyingChange !== this.fieldKey) { if (incomingValue?.data) { const updatedState = JSON.parse(incomingValue.data.Data); - if (JSON.stringify(this._editorView.state.toJSON()) !== JSON.stringify(updatedState)) { - this._editorView.updateState(EditorState.fromJSON(this.config, updatedState)); + if (JSON.stringify(this.EditorView.state.toJSON()) !== JSON.stringify(updatedState)) { + this.EditorView.updateState(EditorState.fromJSON(this.config, updatedState)); this.tryUpdateScrollHeight(); } - } else if (this._editorView.state.doc.textContent !== incomingValue?.str) { - selectAll(this._editorView.state, tx => this._editorView?.dispatch(tx.insertText(incomingValue?.str ?? ''))); + } else if (this.EditorView.state.doc.textContent !== (incomingValue?.str ?? '')) { + selectAll(this.EditorView.state, tx => this.EditorView?.dispatch(tx.insertText(incomingValue?.str ?? ''))); } } }, @@ -1333,9 +1333,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent { if (selected && this.dataDoc[this.fieldKey + '_placeholder']) { setTimeout(() => { - selectAll(this._editorView!.state, (tx: Transaction) => { - this._editorView?.dispatch(tx); - this._editorView!.focus(); + selectAll(this.EditorView!.state, (tx: Transaction) => { + this.EditorView?.dispatch(tx); + this.EditorView!.focus(); }); }); } @@ -1343,12 +1343,12 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent { - const view = this._editorView!; + const view = this.EditorView!; if (pdfAnchorId) { DocServer.GetRefField(pdfAnchorId).then(pdfAnchor => { if (pdfAnchor instanceof Doc) { @@ -1487,7 +1487,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent { @@ -1519,14 +1519,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent { - this._editorView?.dispatch(tr.replaceSelectionWith(state.schema.nodes.paragraph.create({ align: textAlign }))); + this.EditorView?.dispatch(tr.replaceSelectionWith(state.schema.nodes.paragraph.create({ align: textAlign }))); }); } if (startupText) { - this._editorView?.dispatch(this._editorView.state.tr.insertText(startupText)); + this.EditorView?.dispatch(this.EditorView.state.tr.insertText(startupText)); } this.tryUpdateDoc(true); } @@ -1539,56 +1539,57 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent m.type !== mark.type), mark]; - const tr1 = this._editorView.state.tr.setStoredMarks(storedMarks); - const tr2 = selLoadChar === 'Enter' ? tr1.insert(this._editorView.state.doc.content.size - 1, schema.nodes.paragraph.create()) : tr1.insertText(selLoadChar, this._editorView.state.doc.content.size - 1); + const tr1 = this.EditorView.state.tr.setStoredMarks(storedMarks); + const tr2 = selLoadChar === 'Enter' ? tr1.insert(this.EditorView.state.doc.content.size - 1, schema.nodes.paragraph.create()) : tr1.insertText(selLoadChar, this.EditorView.state.doc.content.size - 1); const tr = tr2.setStoredMarks(storedMarks); - this._editorView.dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(tr.doc.content.size)))); + this.EditorView.dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(tr.doc.content.size)))); this.tryUpdateDoc(true); // calling select() above will make isContentActive() true only after a render .. which means the selectAll() above won't write to the Document and the incomingValue will overwrite the selection with the non-updated data } else if (!FormattedTextBox.DontSelectInitialText) { const mark = schema.marks.user_mark.create({ userid: ClientUtils.CurrentUserEmail(), modified: Math.floor(Date.now() / 1000) }); - selectAll(this._editorView.state, (tx: Transaction) => { - this._editorView?.dispatch(tx.addStoredMark(mark)); + selectAll(this.EditorView.state, (tx: Transaction) => { + this.EditorView?.dispatch(tx.addStoredMark(mark)); }); + this.EditorView?.dispatch(this.EditorView.state.tr.setSelection(new TextSelection(this.EditorView.state.doc.resolve(1)))); this.tryUpdateDoc(true); // calling select() above will make isContentActive() true only after a render .. which means the selectAll() above won't write to the Document and the incomingValue will overwrite the selection with the non-updated data } else { - const $from = this._editorView.state.selection.anchor ? this._editorView.state.doc.resolve(this._editorView.state.selection.anchor - 1) : undefined; + const $from = this.EditorView.state.selection.anchor ? this.EditorView.state.doc.resolve(this.EditorView.state.selection.anchor - 1) : undefined; const mark = schema.marks.user_mark.create({ userid: ClientUtils.CurrentUserEmail(), modified: Math.floor(Date.now() / 1000) }); - const curMarks = this._editorView.state.storedMarks ?? $from?.marksAcross(this._editorView.state.selection.$head) ?? []; + const curMarks = this.EditorView.state.storedMarks ?? $from?.marksAcross(this.EditorView.state.selection.$head) ?? []; const storedMarks = [...curMarks.filter(m => m.type !== mark.type), mark]; - const { tr } = this._editorView.state; - this._editorView.dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(tr.doc.content.size))).setStoredMarks(storedMarks)); + const { tr } = this.EditorView.state; + this.EditorView.dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(tr.doc.content.size))).setStoredMarks(storedMarks)); this.tryUpdateDoc(true); // calling select() above will make isContentActive() true only after a render .. which means the selectAll() above won't write to the Document and the incomingValue will overwrite the selection with the non-updated data } } if (selectOnLoad) { FormattedTextBox.DontSelectInitialText = false; - this._editorView!.focus(); + this.EditorView!.focus(); } if (this._props.isContentActive()) this.prepareForTyping(); - if (this._editorView && FormattedTextBox.PasteOnLoad) { + if (this.EditorView && FormattedTextBox.PasteOnLoad) { const pdfAnchorId = FormattedTextBox.PasteOnLoad.clipboardData?.getData('dash/pdfAnchor'); FormattedTextBox.PasteOnLoad = undefined; pdfAnchorId && this.addPdfReference(pdfAnchorId); } - if (this._props.autoFocus) setTimeout(() => this._editorView!.focus()); // not sure why setTimeout is needed but editing dashFieldView's doesn't work without it. + if (this._props.autoFocus) setTimeout(() => this.EditorView!.focus()); // not sure why setTimeout is needed but editing dashFieldView's doesn't work without it. } // add user mark for any first character that was typed since the user mark that gets set in KeyPress won't have been called yet. prepareForTyping = () => { - if (this._editorView) { + if (this.EditorView) { const { text, paragraph } = schema.nodes; - const selNode = this._editorView.state.selection.$anchor.node(); - if (this._editorView.state.selection.from === 1 && this._editorView.state.selection.empty && [undefined, text, paragraph].includes(selNode?.type)) { + const selNode = this.EditorView.state.selection.$anchor.node(); + if (this.EditorView.state.selection.from === 1 && this.EditorView.state.selection.empty && [undefined, text, paragraph].includes(selNode?.type)) { const docDefaultMarks = [schema.marks.user_mark.create({ userid: ClientUtils.CurrentUserEmail(), modified: Math.floor(Date.now() / 1000) })]; - this._editorView.state.selection.empty && this._editorView.state.selection.from === 1 && this._editorView?.dispatch(this._editorView?.state.tr.setStoredMarks(docDefaultMarks).removeStoredMark(schema.marks.pFontColor)); + this.EditorView.state.selection.empty && this.EditorView.state.selection.from === 1 && this.EditorView?.dispatch(this.EditorView?.state.tr.setStoredMarks(docDefaultMarks).removeStoredMark(schema.marks.pFontColor)); } } }; @@ -1602,7 +1603,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent { - const pos = ipos ?? (this._editorView?.state.selection.$from.pos || 1); - setTimeout(() => this._editorView?.dispatch(this._editorView.state.tr.setSelection(TextSelection.near(this._editorView.state.doc.resolve(pos)))), 100); + const pos = ipos ?? (this.EditorView?.state.selection.$from.pos || 1); + setTimeout(() => this.EditorView?.dispatch(this.EditorView.state.tr.setSelection(TextSelection.near(this.EditorView.state.doc.resolve(pos)))), 100); setTimeout(() => (this.ProseRef?.children?.[0] as HTMLElement).focus(), 200); }; @action onFocused = (e: React.FocusEvent): void => { - // applyDevTools.applyDevTools(this._editorView); + // applyDevTools.applyDevTools(this.EditorView); e.stopPropagation(); }; onClick = (e: React.MouseEvent): void => { if (!this._props.isContentActive()) return; - const editorView = this._editorView; + const editorView = this.EditorView; const editorRoot = editorView?.root instanceof Document ? editorView.root : undefined; if (editorView && (!this._forceUncollapse || editorRoot?.getSelection()?.isCollapsed)) { // this is a hack to allow the cursor to be placed at the end of a document when the document ends in an inline dash comment. Apparently Chrome on Windows has a bug/feature which breaks this when clicking after the end of the text. const pcords = editorView.posAtCoords({ left: e.clientX, top: e.clientY }); const node = pcords && editorView.state.doc.nodeAt(pcords.pos); // get what prosemirror thinks the clicked node is (if it's null, then we didn't click on any text) if (pcords && node?.type === editorView.state.schema.nodes.dashComment) { - this._editorView!.dispatch(editorView.state.tr.setSelection(TextSelection.create(editorView.state.doc, pcords.pos + 2))); + this.EditorView!.dispatch(editorView.state.tr.setSelection(TextSelection.create(editorView.state.doc, pcords.pos + 2))); e.preventDefault(); } if (!node && this.ProseRef) { @@ -1726,33 +1727,33 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent { if (this.ProseRef?.children[0] !== e.nativeEvent.target) return; if (!(this.EditorView?.state.selection instanceof NodeSelection) || this.EditorView.state.selection.node.type !== this.EditorView.state.schema.nodes.footnote) { - const stordMarks = this._editorView?.state.storedMarks?.slice(); + const stordMarks = this.EditorView?.state.storedMarks?.slice(); if (!(this.EditorView?.state.selection instanceof NodeSelection)) { this.autoLink(); - if (this._editorView?.state.tr) { + if (this.EditorView?.state.tr) { const tr = stordMarks?.reduce((tr2, m) => { tr2.addStoredMark(m); return tr2; - }, this._editorView.state.tr); - tr && this._editorView.dispatch(tr); + }, this.EditorView.state.tr); + tr && this.EditorView.dispatch(tr); } } } - if (RichTextMenu.Instance?.view === this._editorView && !(this._props.isContentActive() || this._props.rootSelected?.())) { + if (RichTextMenu.Instance?.view === this.EditorView && !(this._props.isContentActive() || this._props.rootSelected?.())) { RichTextMenu.Instance?.updateMenu(undefined, undefined, undefined, undefined); } @@ -1831,7 +1832,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent { - this._editorView!.dispatch(updateBullets(this._editorView!.state.tr, this._editorView!.state.schema)); + this.EditorView?.dispatch(updateBullets(this.EditorView.state.tr, this.EditorView.state.schema)); e.stopPropagation(); // drag n drop of text within text note will generate a new note if not caughst, as will dragging in from outside of Dash. }; onScroll = (e: React.UIEvent) => { @@ -1872,7 +1873,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent { const margins = 2 * NumCast(this.layoutDoc._yMargin, this._props.yPadding || 0); const children = this.ProseRef?.children.length ? Array.from(this.ProseRef.children[0].children) : undefined; - if (children && !SnappingManager.IsDragging) { + if (this.EditorView && children && !SnappingManager.IsDragging) { const getChildrenHeights = (kids: Element[] | undefined) => kids?.reduce((p, child) => p + toHgt(child), margins) ?? 0; const toNum = (val: string) => Number(val.replace('px', '')); const toHgt = (node: Element): number => { @@ -2093,7 +2094,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent - return styleFromLayout?.height === '0px' ? null : ( + return this.isLabel ? ( + + ) : styleFromLayout?.height === '0px' ? null : (
{ @@ -2156,8 +2159,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent
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>(schema: S, props: any): KeyMa dispatch(tx4); } - if (view.state.selection.$anchor.nodeAfter?.type === schema.nodes.text && once) { + if (view.state.selection.$anchor.depth > 0 && + view.state.selection.$anchor.node(view.state.selection.$anchor.depth-1).type === schema.nodes.list_item && + view.state.selection.$anchor.nodeAfter?.type === schema.nodes.text && once) { // if text is selected across list items, then we need to forcibly insert a new line since the splitBlock code joins the two list items. enter(view.state, dispatch, view, false); } diff --git a/src/client/views/nodes/formattedText/RichTextMenu.tsx b/src/client/views/nodes/formattedText/RichTextMenu.tsx index 55e6a3a5b..65a415a23 100644 --- a/src/client/views/nodes/formattedText/RichTextMenu.tsx +++ b/src/client/views/nodes/formattedText/RichTextMenu.tsx @@ -1,6 +1,6 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Tooltip } from '@mui/material'; -import { action, computed, IReactionDisposer, makeObservable, observable, runInAction } from 'mobx'; +import { action, computed, makeObservable, observable, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import { lift, toggleMark, wrapIn } from 'prosemirror-commands'; import { Mark, MarkType } from 'prosemirror-model'; @@ -32,7 +32,7 @@ export class RichTextMenu extends AntimodeMenu { public overMenu: boolean = false; // kind of hacky way to prevent selects not being selectable private _linkToRef = React.createRef(); - layoutDoc: Doc | undefined; + dataDoc: Doc | undefined; @observable public view?: EditorView & { TextView?: FormattedTextBox } = undefined; public editorProps: FieldViewProps | AntimodeMenuProps | undefined; @@ -49,6 +49,7 @@ export class RichTextMenu extends AntimodeMenu { @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 { @observable private currentLink: string | undefined = ''; @observable private showLinkDropdown: boolean = false; - _reaction: IReactionDisposer | undefined; constructor(props: AntimodeMenuProps) { super(props); makeObservable(this); runInAction(() => { RichTextMenu._instance.menu = this; - this.updateMenu(undefined, undefined, props, this.layoutDoc); + this.updateMenu(undefined, undefined, props, this.dataDoc); this._canFade = false; this.Pinned = true; }); @@ -101,6 +101,9 @@ export class RichTextMenu extends AntimodeMenu { @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 { 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 { 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 { toggleMark(mark.type, mark.attrs)(state, dispatch); } } - // this.updateMenu(this.view, undefined, undefined, this.layoutDoc); } }; @@ -195,8 +188,10 @@ export class RichTextMenu extends AntimodeMenu { return node.attrs.align || 'left'; } } + } else if (this.dataDoc) { + return StrCast(this.dataDoc.text_align) || 'left'; } - return 'left'; + return StrCast(Doc.UserDoc().textAlign) || 'left'; }; // finds font sizes and families in selection @@ -330,6 +325,17 @@ export class RichTextMenu extends AntimodeMenu { this.view.focus(); } }; + toggleFitBox = () => { + if (this.dataDoc) { + const doc = this.dataDoc; + (document.activeElement as HTMLElement)?.blur(); + doc.text_fitBox = !doc.text_fitBox; + } else { + Doc.UserDoc().fitBox = !Doc.UserDoc().fitBox; + Doc.UserDoc().textAlign = Doc.UserDoc().fitBox ? 'center' : undefined; + } + this.updateMenu(undefined, undefined, undefined, this.dataDoc); + }; toggleBold = () => { if (this.view) { const mark = this.view.state.schema.mark(this.view.state.schema.marks.strong); @@ -354,11 +360,14 @@ export class RichTextMenu extends AntimodeMenu { } }; - setFontField = (value: string, fontField: 'fontSize' | 'fontFamily' | 'fontColor' | 'fontHighlight') => { - if (this.TextView && this.view) { + setFontField = (value: string, fontField: 'fitBox' | 'fontSize' | 'fontFamily' | 'fontColor' | 'fontHighlight') => { + if (this.dataDoc) { + this.dataDoc[`text_${fontField}`] = value; + this.updateMenu(undefined, undefined, undefined, this.dataDoc); + } else if (this.TextView && this.view) { const { text, paragraph } = this.view.state.schema.nodes; const selNode = this.view.state.selection.$anchor.node(); - if (this.view.state.selection.from === 1 && this.view.state.selection.empty && [undefined, text, paragraph].includes(selNode?.type)) { + if ((fontField === 'fontSize' && value === '0px') || (this.view.state.selection.from === 1 && this.view.state.selection.empty && [undefined, text, paragraph].includes(selNode?.type))) { this.TextView.dataDoc[this.TextView.fieldKey + `_${fontField}`] = value; this.view.focus(); } @@ -369,7 +378,6 @@ export class RichTextMenu extends AntimodeMenu { this.view.focus(); } else { Doc.UserDoc()[fontField] = value; - // this.updateMenu(this.view, undefined, this.props, this.layoutDoc); } }; @@ -395,7 +403,6 @@ export class RichTextMenu extends AntimodeMenu { this.view!.dispatch(tx3); }); this.view.focus(); - // this.updateMenu(this.view, undefined, this.props, this.layoutDoc); }; insertSummarizer(state: EditorState, dispatch: (tr: Transaction) => void) { @@ -410,10 +417,11 @@ export class RichTextMenu extends AntimodeMenu { } vcenterToggle = () => { - this.layoutDoc && (this.layoutDoc._layout_centered = !this.layoutDoc._layout_centered); + if (this.dataDoc) this.dataDoc._layout_centered = !this.dataDoc._layout_centered; + else Doc.UserDoc()._layout_centered = !Doc.UserDoc()._layout_centered; }; - align = (view: EditorView, dispatch: (tr: Transaction) => void, alignment: 'left' | 'right' | 'center') => { - if (this.RootSelected) { + align = (view: EditorView | undefined, dispatch: undefined | ((tr: Transaction) => void), alignment: 'left' | 'right' | 'center') => { + if (view && dispatch && this.RootSelected) { let { tr } = view.state; view.state.doc.nodesBetween(view.state.selection.from, view.state.selection.to, (node, pos) => { if ([schema.nodes.paragraph, schema.nodes.heading].includes(node.type)) { @@ -425,6 +433,11 @@ export class RichTextMenu extends AntimodeMenu { }); view.focus(); dispatch?.(tr); + } else { + if (this.dataDoc) { + this.dataDoc.text_align = alignment; + } else Doc.UserDoc().textAlign = alignment; + this.updateMenu(undefined, undefined, undefined, this.dataDoc); } }; @@ -702,7 +715,7 @@ interface RichTextMenuPluginProps { } export class RichTextMenuPlugin extends React.Component { 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() { 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 {
{this.heading(this.mode === GPTPopupMode.SORT ? 'SORTING' : 'QUIZ')} <> - {!this.cardsDoneLoading ? ( -
-
- - {this.loading ? Loading... : Reading Cards...} + { + !this.cardsDoneLoading ? ( +
+
+ + {this.loading ? Loading... : Reading Cards...} +
-
- ) : 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 }
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); -- cgit v1.2.3-70-g09d2 From 1a444532d026a208817c6997f00022a3e09ead23 Mon Sep 17 00:00:00 2001 From: bobzel Date: Tue, 29 Oct 2024 13:39:16 -0400 Subject: fixes to allow typing and drawing without a mode switch. --- src/client/documents/DocUtils.ts | 4 ++-- src/client/views/GestureOverlay.tsx | 8 +++++++- src/client/views/nodes/formattedText/RichTextMenu.tsx | 1 + 3 files changed, 10 insertions(+), 3 deletions(-) (limited to 'src/client/documents/DocUtils.ts') diff --git a/src/client/documents/DocUtils.ts b/src/client/documents/DocUtils.ts index e3cb5e72c..dae3e48ad 100644 --- a/src/client/documents/DocUtils.ts +++ b/src/client/documents/DocUtils.ts @@ -678,8 +678,8 @@ 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: BoolCast(Doc.UserDoc().fitBox) ? 70 : 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, diff --git a/src/client/views/GestureOverlay.tsx b/src/client/views/GestureOverlay.tsx index afeecaa63..402a7c20a 100644 --- a/src/client/views/GestureOverlay.tsx +++ b/src/client/views/GestureOverlay.tsx @@ -95,6 +95,8 @@ export class GestureOverlay extends ObservableReactComponent { + DocumentView.DeselectAll(); + (document.activeElement as HTMLElement)?.blur(); if (!(e.target as HTMLElement)?.className?.toString().startsWith('lm_')) { if ([InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool)) { this._points.push({ X: e.clientX, Y: e.clientY }); @@ -244,8 +246,9 @@ export class GestureOverlay extends ObservableReactComponent { + 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; @@ -286,6 +289,9 @@ export class GestureOverlay extends ObservableReactComponent { this.view.focus(); } else { Doc.UserDoc()[fontField] = value; + this.updateMenu(undefined, undefined, undefined, this.dataDoc); } }; -- cgit v1.2.3-70-g09d2 From 4c768162e0436115a05b9c8b0e4d837d626d45ba Mon Sep 17 00:00:00 2001 From: bobzel Date: Wed, 30 Oct 2024 18:54:52 -0400 Subject: reworked how context menu buttons for ink and text work. added disableMixBlend for making transparent docs not use 'multiply'. --- src/ClientUtils.ts | 11 +- src/client/documents/DocUtils.ts | 16 +- src/client/documents/Documents.ts | 1 - src/client/util/CurrentUserUtils.ts | 78 +++++---- src/client/util/InteractionUtils.tsx | 6 +- src/client/util/SettingsManager.tsx | 4 +- src/client/util/SnappingManager.ts | 7 + src/client/views/FilterPanel.tsx | 2 +- src/client/views/GestureOverlay.tsx | 164 ++++++------------ src/client/views/GlobalKeyHandler.ts | 9 +- src/client/views/InkingStroke.tsx | 3 +- src/client/views/LightboxView.tsx | 4 +- src/client/views/StyleProp.ts | 4 +- src/client/views/StyleProvider.tsx | 5 +- .../views/collections/CollectionDockingView.tsx | 2 +- .../CollectionFreeFormInfoUI.tsx | 6 +- .../collectionFreeForm/CollectionFreeFormView.tsx | 71 ++++---- .../collections/collectionFreeForm/MarqueeView.tsx | 2 +- src/client/views/global/globalScripts.ts | 112 +++++++------ .../views/newlightbox/ButtonMenu/ButtonMenu.tsx | 6 +- .../views/nodes/CollectionFreeFormDocumentView.tsx | 5 +- src/client/views/nodes/DataVizBox/DataVizBox.tsx | 4 +- src/client/views/nodes/DocumentView.tsx | 72 ++++---- .../views/nodes/FontIconBox/FontIconBox.scss | 3 + src/client/views/nodes/FontIconBox/FontIconBox.tsx | 186 ++++++++++++--------- src/client/views/nodes/ImageBox.tsx | 10 +- src/client/views/nodes/VideoBox.tsx | 2 +- src/client/views/nodes/WebBox.tsx | 13 +- .../views/nodes/formattedText/FormattedTextBox.tsx | 12 +- .../views/nodes/formattedText/RichTextMenu.tsx | 22 +-- src/client/views/pdf/PDFViewer.tsx | 2 +- src/client/views/smartdraw/SmartDrawHandler.tsx | 12 +- src/fields/Doc.ts | 6 +- src/fields/InkField.ts | 27 +-- src/pen-gestures/GestureTypes.ts | 1 - 35 files changed, 444 insertions(+), 446 deletions(-) (limited to 'src/client/documents/DocUtils.ts') 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 dae3e48ad..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("...")) @@ -683,9 +688,14 @@ export namespace DocUtils { _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 b0e1a7545..efd6ce3c9 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -927,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 b3aa5e145..98a3e8914 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, 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 @@ -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(["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: 9 }, + { 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_);}'} }, @@ -758,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: "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: "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 segments up to 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: "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: "inkMask", 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: "labels", scripts: {onClick:'{ 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()"}}, ]; } @@ -814,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 @@ -863,7 +867,7 @@ pie title Minerals in my tap water } // linear view const reqdSubMenuOpts = { ...OmitKeys(params, ["scripts", "funcs", "subMenu"]).omit, undoIgnoreFields: new List(['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) ); @@ -987,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"); @@ -1130,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 ( - + {' '} {/* 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 33d7d90a8..628097ca8 100644 --- a/src/client/util/SettingsManager.tsx +++ b/src/client/util/SettingsManager.tsx @@ -268,9 +268,9 @@ export class SettingsManager extends React.Component { 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} /> 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 = observer(({ hotKey /*, sel return (
{ e.stopPropagation(); state.startEditing(); diff --git a/src/client/views/GestureOverlay.tsx b/src/client/views/GestureOverlay.tsx index 59d37df91..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 = 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 { @@ -97,7 +91,7 @@ export class GestureOverlay extends ObservableReactComponent { (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); } @@ -123,13 +117,9 @@ export class GestureOverlay extends ObservableReactComponent ({ 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) ?? @@ -292,6 +282,7 @@ export class GestureOverlay extends ObservableReactComponent { - const b = { left: -20000, right: 20000, top: -20000, bottom: 20000, width: 40000, height: 40000 }; // this.getBounds(l, true); - return ( - - {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 - )} - - ); - }), - this._points.length <= 1 ? null : ( - - {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 - )} - - ), - ], + this._points.length <= 1 ? null : ( + + {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 + )} + + ), ]; } - screenToLocalTransform = () => new Transform(-(this._thumbX ?? 0), -(this._thumbY ?? 0) + this.height, 1); - return300 = () => 300; - @action - public openFloatingDoc = (doc: Doc) => { - this._clipboardDoc = ( - - ); - }; @action public closeFloatingDoc = () => { @@ -694,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() 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} - {!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 : (
{ // 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 { {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)}
); 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 12c032964..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, props: Opt activeTool() === InkTool.Pen, () => penMode], + activePen: [() => activeTool() === InkTool.Ink, () => penMode], }, 'documentation.png', () => TopBar.Instance.FlipDocumentationIcon() @@ -187,7 +187,7 @@ export class CollectionFreeFormInfoUI extends ObservableReactComponent 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 = 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 - 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 { @@ -1195,14 +1208,14 @@ export class CollectionFreeFormView extends CollectionSubView - {Doc.ActiveTool === InkTool.RadiusEraser && this._showEraserCircle && ( + {Doc.ActiveTool === InkTool.Eraser && Doc.ActiveEraser === InkEraserTool.Radius && this._showEraserCircle && (
e.preventDefault()} onScroll={e => { diff --git a/src/client/views/global/globalScripts.ts b/src/client/views/global/globalScripts.ts index 013b18a97..40144c4ce 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, 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 ''; }); @@ -330,7 +333,7 @@ ScriptingGlobals.add(function setFontAttr(attr: 'font' | 'fontColor' | 'highligh return undefined; }); -type attrname = 'noAutoLink' | 'dictation' | 'fitBox' | '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 @@ -360,10 +363,10 @@ ScriptingGlobals.add(function toggleCharStyle(charStyle: attrname, checkResult?: 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) { @@ -373,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; @@ -419,44 +425,44 @@ 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(); + 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', { 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()), + setMode: () => SetActiveIsInkMask(value ? true : false) }], ['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()), + checkResult: () => ((selected?._layout_isSvg ? BoolCast(selected[DocData].stroke_showLabel) : !ActiveHideTextLabels())), + setInk: (doc: Doc) => { doc[DocData].stroke_showLabel = value; }, + setMode: () => SetactiveHideTextLabels(value? false : true), }], ['fillColor', { - checkResult: () => (selected?._layout_isSvg ? StrCast(selected[DocData].fillColor) : ActiveFillColor() ?? "transparent"), + checkResult: () => (selected?._layout_isSvg ? StrCast(selected[DocData].fillColor) : ActiveInkFillColor() ?? "transparent"), setInk: (doc: Doc) => { doc[DocData].fillColor = StrCast(value); }, - setMode: () => SetActiveFillColor(StrCast(value)), + setMode: () => SetActiveInkFillColor(StrCast(value)), }], [ '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', { 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', { 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() {
{ e.stopPropagation(); - Doc.ActiveTool = Doc.ActiveTool === InkTool.Pen ? InkTool.None : InkTool.Pen; + Doc.ActiveTool = Doc.ActiveTool === InkTool.Ink ? InkTool.None : InkTool.Ink; }} />
{this.RenderCutoffProvider(this.Document) ? (
) : ( 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() { return
; } 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() { @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/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index eb7f333b9..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 { - 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(); @@ -1560,55 +1560,45 @@ export class DocumentView extends DocComponent() { } 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/FontIconBox/FontIconBox.scss b/src/client/views/nodes/FontIconBox/FontIconBox.scss index ab03a2318..2405889cf 100644 --- a/src/client/views/nodes/FontIconBox/FontIconBox.scss +++ b/src/client/views/nodes/FontIconBox/FontIconBox.scss @@ -13,6 +13,9 @@ .fonticonbox { margin: auto; width: 100%; + .formLabel { + height: 5px; + } } .menuButton { height: 100%; diff --git a/src/client/views/nodes/FontIconBox/FontIconBox.tsx b/src/client/views/nodes/FontIconBox/FontIconBox.tsx index b45774a75..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() { 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)} @@ -148,65 +151,80 @@ export class FontIconBox extends ViewBoxBaseComponent() { return false; }; + /** + * 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 ? ( + 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={} + fillWidth + /> + ) : ( +
@@ -1042,7 +1041,6 @@ export class WebBox extends ViewBoxAnnotatableComponent() { } renderAnnotations = (childFilters: () => string[]) => ( () {
{ @observable private collapsed: boolean = false; @observable private _noLinkActive: boolean = false; @observable private _boldActive: boolean = false; - @observable private _italicsActive: boolean = false; + @observable private _italicActive: boolean = false; @observable private _underlineActive: boolean = false; @observable private _strikethroughActive: boolean = false; @observable private _subscriptActive: boolean = false; @@ -89,8 +89,8 @@ export class RichTextMenu extends AntimodeMenu { @computed get underline() { return this._underlineActive; } - @computed get italics() { - return this._italicsActive; + @computed get italic() { + return this._italicActive; } @computed get strikeThrough() { return this._strikethroughActive; @@ -280,7 +280,7 @@ export class RichTextMenu extends AntimodeMenu { this._noLinkActive = false; this._boldActive = false; - this._italicsActive = false; + this._italicActive = false; this._underlineActive = false; this._strikethroughActive = false; this._subscriptActive = false; @@ -290,7 +290,7 @@ export class RichTextMenu extends AntimodeMenu { switch (mark.name) { case 'noAutoLinkAnchor': this._noLinkActive = true; break; case 'strong': this._boldActive = true; break; - case 'em': this._italicsActive = true; break; + case 'em': this._italicActive = true; break; case 'underline': this._underlineActive = true; break; case 'strikethrough': this._strikethroughActive = true; break; case 'subscript': this._subscriptActive = true; break; @@ -352,7 +352,7 @@ export class RichTextMenu extends AntimodeMenu { } }; - toggleItalics = () => { + toggleItalic = () => { if (this.view) { const mark = this.view.state.schema.mark(this.view.state.schema.marks.em); this.setMark(mark, this.view.state, this.view.dispatch, false); @@ -361,13 +361,10 @@ export class RichTextMenu extends AntimodeMenu { }; setFontField = (value: string, fontField: 'fitBox' | 'fontSize' | 'fontFamily' | 'fontColor' | 'fontHighlight') => { - if (this.dataDoc) { - this.dataDoc[`text_${fontField}`] = value; - this.updateMenu(undefined, undefined, undefined, this.dataDoc); - } else if (this.TextView && this.view) { + if (this.TextView && this.view && fontField !== 'fitBox') { const { text, paragraph } = this.view.state.schema.nodes; const selNode = this.view.state.selection.$anchor.node(); - if ((fontField === 'fontSize' && value === '0px') || (this.view.state.selection.from === 1 && this.view.state.selection.empty && [undefined, text, paragraph].includes(selNode?.type))) { + if (this.view.state.selection.from === 1 && this.view.state.selection.empty && [undefined, text, paragraph].includes(selNode?.type)) { this.TextView.dataDoc[this.TextView.fieldKey + `_${fontField}`] = value; this.view.focus(); } @@ -376,6 +373,9 @@ export class RichTextMenu extends AntimodeMenu { 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(undefined, undefined, undefined, this.dataDoc); 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 { 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 { 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; } // 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..f3ff5f11f 100644 --- a/src/fields/InkField.ts +++ b/src/fields/InkField.ts @@ -8,17 +8,22 @@ 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 type Segment = Array; 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; -- cgit v1.2.3-70-g09d2 From 8680d1c31d4f835663c070f5b8cef57254e75e28 Mon Sep 17 00:00:00 2001 From: bobzel Date: Tue, 12 Nov 2024 16:42:35 -0500 Subject: made equation background same as text. fixed dflt stroke/link width to both be 1. made function plot axis ranges get saved to Doc. marked equation->function links as being svgs. fixed initial size of equation boxes.. --- src/client/documents/DocUtils.ts | 3 +- src/client/util/CurrentUserUtils.ts | 2 +- src/client/views/PropertiesView.tsx | 2 +- src/client/views/StyleProvider.tsx | 2 +- .../collectionFreeForm/CollectionFreeFormView.tsx | 7 +--- src/client/views/global/globalScripts.ts | 2 +- src/client/views/linking/LinkMenuItem.tsx | 2 +- src/client/views/nodes/DocumentView.tsx | 3 +- src/client/views/nodes/EquationBox.scss | 2 - src/client/views/nodes/EquationBox.tsx | 2 +- src/client/views/nodes/FunctionPlotBox.tsx | 43 +++++++++++++++++++--- src/client/views/nodes/LinkBox.tsx | 2 +- 12 files changed, 50 insertions(+), 22 deletions(-) (limited to 'src/client/documents/DocUtils.ts') diff --git a/src/client/documents/DocUtils.ts b/src/client/documents/DocUtils.ts index 067b9c5e0..741d3a10e 100644 --- a/src/client/documents/DocUtils.ts +++ b/src/client/documents/DocUtils.ts @@ -168,7 +168,7 @@ export namespace DocUtils { return rangeFilteredDocs; } - export function MakeLink(source: Doc, target: Doc, linkSettings: { link_relationship?: string; link_description?: string }, id?: string, showPopup?: number[]) { + export function MakeLink(source: Doc, target: Doc, linkSettings: { layout_isSvg?: boolean; link_relationship?: string; link_description?: string }, id?: string, showPopup?: number[]) { if (!linkSettings.link_relationship) linkSettings.link_relationship = target.type === DocumentType.RTF ? 'Commentary:Comments On' : 'link'; if (target.doc === Doc.UserDoc()) return undefined; @@ -220,6 +220,7 @@ export namespace DocUtils { link_anchor_2_useSmallAnchor: target.useSmallAnchor ? true : undefined, link_relationship: linkSettings.link_relationship, link_description: linkSettings.link_description, + layout_isSvg: linkSettings.layout_isSvg, x: ComputedField.MakeFunction(`((this.${a}?.x||0)+(this.${b}?.x||0))/2`) as unknown as number, // x can accept functions even though type says it can't y: ComputedField.MakeFunction(`((this.${a}?.y||0)+(this.${b}?.y||0))/2`) as unknown as number, // y can accept functions even though type says it can't link_autoMoveAnchors: true, diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index fe23fe062..7b2a7ae0f 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -1142,7 +1142,7 @@ pie title Minerals in my tap water } // eslint-disable-next-line prefer-arrow-callback -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"); +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 diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx index 10c2a9898..2bd8bc98a 100644 --- a/src/client/views/PropertiesView.tsx +++ b/src/client/views/PropertiesView.tsx @@ -830,7 +830,7 @@ export class PropertiesView extends ObservableReactComponent { doc[DocData].stroke_width = Math.round(value * 100) / 100; }); } // prettier-ignore @computed get hgtInput() { diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx index 3a5f57908..0a9da1237 100644 --- a/src/client/views/StyleProvider.tsx +++ b/src/client/views/StyleProvider.tsx @@ -257,7 +257,7 @@ export function DefaultStyleProvider(doc: Opt, props: Opt SetactiveHideTextLabels(value? false : true), }], [ InkProperty.StrokeWidth, { - checkResult: () => (selected?._layout_isSvg ? NumCast(selected[DocData].stroke_width) : ActiveInkWidth()), + checkResult: () => (selected?._layout_isSvg ? NumCast(selected[DocData].stroke_width, 1) : ActiveInkWidth()), setInk: (doc: Doc) => { doc[DocData].stroke_width = NumCast(value); }, setMode: () => SetActiveInkWidth(value.toString()), }], diff --git a/src/client/views/linking/LinkMenuItem.tsx b/src/client/views/linking/LinkMenuItem.tsx index b24fca8e2..1c92c3b0d 100644 --- a/src/client/views/linking/LinkMenuItem.tsx +++ b/src/client/views/linking/LinkMenuItem.tsx @@ -90,7 +90,7 @@ export class LinkMenuItem extends ObservableReactComponent { moveEv => { const dragData = new DragManager.DocumentDragData([this._props.linkDoc], dropActionType.embed); dragData.dropPropertiesToRemove = ['hidden']; - DragManager.StartDocumentDrag([this._editRef.current!], dragData, moveEv.x, moveEv.y, undefined, e => (this._props.linkDoc._layout_isSvg = true)); + DragManager.StartDocumentDrag([this._editRef.current!], dragData, moveEv.x, moveEv.y, undefined, () => (this._props.linkDoc._layout_isSvg = true)); return true; }, emptyFunction, diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 30f9e6363..92e98db87 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -459,10 +459,9 @@ export class DocumentViewInternal extends DocComponent span { width: fit-content; } diff --git a/src/client/views/nodes/EquationBox.tsx b/src/client/views/nodes/EquationBox.tsx index 472fa56a0..34b46381d 100644 --- a/src/client/views/nodes/EquationBox.tsx +++ b/src/client/views/nodes/EquationBox.tsx @@ -81,7 +81,7 @@ export class EquationBox extends ViewBoxBaseComponent() { _height: 300, backgroundColor: 'white', }); - const link = DocUtils.MakeLink(this.Document, graph, { link_relationship: 'function', link_description: 'input' }); + const link = DocUtils.MakeLink(this.Document, graph, { layout_isSvg: true, link_relationship: 'function', link_description: 'input' }); this._props.addDocument?.(graph); link && this._props.addDocument?.(link); e.stopPropagation(); diff --git a/src/client/views/nodes/FunctionPlotBox.tsx b/src/client/views/nodes/FunctionPlotBox.tsx index 6b439cd64..91c351895 100644 --- a/src/client/views/nodes/FunctionPlotBox.tsx +++ b/src/client/views/nodes/FunctionPlotBox.tsx @@ -1,5 +1,5 @@ import functionPlot, { Chart } from 'function-plot'; -import { computed, makeObservable, reaction } from 'mobx'; +import { action, computed, makeObservable, reaction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { Doc, DocListCast } from '../../../fields/Doc'; @@ -15,6 +15,8 @@ import { undoBatch } from '../../util/UndoManager'; import { ViewBoxAnnotatableComponent } from '../DocComponent'; import { PinDocView, PinProps } from '../PinFuncs'; import { FieldView, FieldViewProps } from './FieldView'; +import { returnFalse, setupMoveUpEvents } from '../../../ClientUtils'; +import { emptyFunction } from '../../../Utils'; @observer export class FunctionPlotBox extends ViewBoxAnnotatableComponent() { @@ -65,18 +67,24 @@ export class FunctionPlotBox extends ViewBoxAnnotatableComponent ); return funcs; } + computeYScale = (width: number, height: number, xScale: number[]) => { + const xDiff = xScale[1] - xScale[0]; + const yDiff = (height * xDiff) / width; + return [-yDiff / 2, yDiff / 2]; + }; createGraph = (ele?: HTMLDivElement) => { this._plotEle = ele || this._plotEle; const width = this._props.PanelWidth(); const height = this._props.PanelHeight(); + const xrange = Cast(this.layoutDoc.xRange, listSpec('number'), [-10, 10]); try { this._plotEle?.children.length && this._plotEle.removeChild(this._plotEle.children[0]); this._plot = functionPlot({ target: '#' + this._plotEle?.id, width, height, - xAxis: { domain: Cast(this.layoutDoc.xRange, listSpec('number'), [-10, 10]) }, - yAxis: { domain: Cast(this.layoutDoc.yRange, listSpec('number'), [-1, 9]) }, + xAxis: { domain: xrange }, + yAxis: { domain: this.computeYScale(width, height, xrange) }, // Cast(this.layoutDoc.yRange, listSpec('number'), [-1, 9]) }, grid: true, data: this.graphFuncs.map(fn => ({ fn, @@ -94,7 +102,7 @@ export class FunctionPlotBox extends ViewBoxAnnotatableComponent const added = de.complete.docDragData.droppedDocuments.reduce((res, doc) => { // const ret = res && Doc.AddDocToList(this.dataDoc, this._props.fieldKey, doc); if (res) { - const link = DocUtils.MakeLink(doc, this.Document, { link_relationship: 'function', link_description: 'input' }); + const link = DocUtils.MakeLink(doc, this.Document, { layout_isSvg: true, link_relationship: 'function', link_description: 'input' }); link && this._props.addDocument?.(link); } return res; @@ -115,7 +123,32 @@ export class FunctionPlotBox extends ViewBoxAnnotatableComponent // if (this.layout_autoHeight) this.tryUpdateScrollHeight(); }; @computed get theGraph() { - return
r && this.createGraph(r)} style={{ position: 'absolute', width: '100%', height: '100%' }} onPointerDown={e => e.stopPropagation()} />; + return ( +
r && this.createGraph(r)} + style={{ position: 'absolute', width: '100%', height: '100%' }} + onPointerDown={e => { + e.stopPropagation(); + setupMoveUpEvents( + this, + e, + returnFalse, + action(() => { + if (this._plot?.options.xAxis?.domain) { + this.Document.xRange = new List(this._plot.options.xAxis.domain); + } + if (this._plot?.options.yAxis?.domain) { + this.Document.yRange = new List(this._plot.options.yAxis.domain); + } + }), + emptyFunction, + false, + false + ); + }} + /> + ); } render() { TraceMobx(); diff --git a/src/client/views/nodes/LinkBox.tsx b/src/client/views/nodes/LinkBox.tsx index 4d9d2460e..c62a8afb1 100644 --- a/src/client/views/nodes/LinkBox.tsx +++ b/src/client/views/nodes/LinkBox.tsx @@ -160,7 +160,7 @@ export class LinkBox extends ViewBoxBaseComponent() { // eslint-disable-next-line camelcase const { stroke_markerScale: strokeMarkerScale, stroke_width: strokeRawWidth, stroke_startMarker: strokeStartMarker, stroke_endMarker: strokeEndMarker, stroke_dash: strokeDash } = this.Document; - const strokeWidth = NumCast(strokeRawWidth, 4); + const strokeWidth = NumCast(strokeRawWidth, 1); const linkDesc = StrCast(this.dataDoc.link_description) || ' '; const labelText = linkDesc.substring(0, 50) + (linkDesc.length > 50 ? '...' : ''); return ( -- cgit v1.2.3-70-g09d2 From 6a6215a447c3104f5fea8a813270b8c1fc39ad75 Mon Sep 17 00:00:00 2001 From: bobzel Date: Tue, 12 Nov 2024 18:03:12 -0500 Subject: fixed resizing height to 0 to autoResize. fixed setting background color default for equations (and everything else) to textBackgroundColor from user doc. Added a math pseudo-font to trigger entering equations instead of rich text. --- src/client/documents/DocUtils.ts | 70 +++++++++++++++++++++----------- src/client/util/CurrentUserUtils.ts | 4 +- src/client/views/DocumentDecorations.tsx | 6 +-- src/client/views/nodes/EquationBox.tsx | 14 +++++-- 4 files changed, 63 insertions(+), 31 deletions(-) (limited to 'src/client/documents/DocUtils.ts') diff --git a/src/client/documents/DocUtils.ts b/src/client/documents/DocUtils.ts index 741d3a10e..c466344b3 100644 --- a/src/client/documents/DocUtils.ts +++ b/src/client/documents/DocUtils.ts @@ -358,6 +358,16 @@ export namespace DocUtils { return ctor ? ctor(path, overwriteDoc ? { ...options, title: StrCast(overwriteDoc.title, path) } : options, overwriteDoc) : undefined; } + /** + * Adds items to the doc creator (':') context menu for creating each document type + * @param docTextAdder + * @param docAdder + * @param x + * @param y + * @param simpleMenu + * @param pivotField + * @param pivotValue + */ export function addDocumentCreatorMenuItems(docTextAdder: (d: Doc) => void, docAdder: (d: Doc) => void, x: number, y: number, simpleMenu: boolean = false, pivotField?: string, pivotValue?: string | number | boolean): void { const documentList: ContextMenuProps[] = DocListCast(DocListCast(Doc.MyTools?.data)[0]?.data) .filter(btnDoc => !btnDoc.hidden) @@ -371,6 +381,7 @@ export namespace DocUtils { newDoc.author = ClientUtils.CurrentUserEmail(); newDoc.x = x; newDoc.y = y; + newDoc[DocData].backgroundColor = Doc.UserDoc().textBackgroundColor; Doc.SetSelectOnLoad(newDoc); if (pivotField) { newDoc[pivotField] = pivotValue; @@ -675,30 +686,43 @@ export namespace DocUtils { export function GetNewTextDoc(title: string, x: number, y: number, width?: number, height?: number, annotationOn?: Doc, backgroundColor?: string) { const defaultTextTemplate = DocCast(Doc.UserDoc().defaultTextLayout); - const tbox = Docs.Create.TextDocument('', { - annotationOn, - backgroundColor, - x, - y, - title, - ...(defaultTextTemplate - ? {} // if the new doc will inherit from a template, don't set any layout fields since that would block the inheritance - : { - _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), + const tbox = + StrCast(Doc.UserDoc().fontFamily) === 'Math' + ? Docs.Create.EquationDocument('', { + // + annotationOn, + backgroundColor: backgroundColor ?? StrCast(Doc.UserDoc().textBackgroundColor), + x, + y, + title, 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), - }), - }); + _width: 35, + _height: 35, + }) + : Docs.Create.TextDocument('', { + annotationOn, + backgroundColor, + x, + y, + title, + ...(defaultTextTemplate + ? {} // if the new doc will inherit from a template, don't set any layout fields since that would block the inheritance + : { + _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), + }), + }); if (defaultTextTemplate) { tbox.layout_fieldKey = 'layout_' + StrCast(defaultTextTemplate.title); diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index 7b2a7ae0f..c01f57a59 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -372,7 +372,7 @@ pie title Minerals in my tap water {key: "Note", creator: opts => Docs.Create.TextDocument("", opts), opts: { _width: 200, _layout_autoHeight: true }}, {key: "Flashcard", creator: opts => Docs.Create.FlashcardDocument("", undefined, undefined, opts),opts: { _width: 300, _height: 300}}, {key: "Image", creator: opts => Docs.Create.ImageDocument("", opts), opts: { _width: 400, _height:400 }}, - {key: "Equation", creator: opts => Docs.Create.EquationDocument("",opts), opts: { _width: 300, _height: 35, }}, + {key: "Equation", creator: opts => Docs.Create.EquationDocument("",opts), opts: { _width: 35, _height: 35, }}, {key: "Noteboard", creator: opts => Docs.Create.NoteTakingDocument([], opts), opts: { _width: 250, _height: 200, _layout_fitWidth: true}}, {key: "Simulation", creator: opts => Docs.Create.SimulationDocument(opts), opts: { _width: 300, _height: 300, }}, {key: "Collection", creator: opts => Docs.Create.FreeformDocument([], opts), opts: { _width: 150, _height: 100, _layout_fitWidth: true }}, @@ -731,7 +731,7 @@ pie title Minerals in my tap water static textTools():Button[] { 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(["Roboto", "Roboto Mono", "Nunito", "Times New Roman", "Arial", "Georgia", "Comic Sans MS", "Tahoma", "Impact", "Crimson Text"]) }, + btnList: new List(["Roboto", "Roboto Mono", "Nunito", "Times New Roman", "Arial", "Georgia", "Comic Sans MS", "Tahoma", "Impact", "Crimson Text", "Math"]) }, { 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_);}'} }, diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index 492c2bda1..07e362672 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -506,7 +506,7 @@ export class DocumentDecorations extends ObservableReactComponent { - const [w, h] = [this.Bounds.r - this.Bounds.x, this.Bounds.b - this.Bounds.y]; + const [w, h] = [Math.max(1, this.Bounds.r - this.Bounds.x), Math.max(1, this.Bounds.b - this.Bounds.y)]; const [moveX, moveY] = [thisPt.x - this._snapPt.x, thisPt.y - this._snapPt.y]; switch (dragHdl) { case 'topLeft': return { scale: { x: 1 - moveX / w, y: 1 -moveY / h }, refPt: [this.Bounds.r, this.Bounds.b] }; @@ -566,8 +566,8 @@ export class DocumentDecorations extends ObservableReactComponent() { @@ -56,6 +57,8 @@ export class EquationBox extends ViewBoxBaseComponent() { { fireImmediately: true } ); } + @computed get fontSize() { return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.FontSize) as string; } // prettier-ignore + @computed get fontColor() { return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.FontColor) as string; } // prettier-ignore @action keyPressed = (e: KeyboardEvent) => { @@ -68,6 +71,9 @@ export class EquationBox extends ViewBoxBaseComponent() { _height: 25, x: NumCast(this.layoutDoc.x), y: NumCast(this.layoutDoc.y) + _height + 10, + backgroundColor: StrCast(this.Document.backgroundColor), + color: StrCast(this.Document.color), + fontSize: this.fontSize, }); Doc.SetSelectOnLoad(nextEq); this._props.addDocument?.(nextEq); @@ -118,10 +124,12 @@ export class EquationBox extends ViewBoxBaseComponent() { className="equationBox-cont" onPointerDown={e => !e.ctrlKey && e.stopPropagation()} style={{ + minWidth: `${100 / scale}%`, transform: `scale(${scale})`, height: `${100 / scale}%`, pointerEvents: !this._props.isSelected() ? 'none' : undefined, - fontSize: StrCast(this.Document._text_fontSize), + fontSize: this.fontSize, + color: this.fontColor, }} onKeyDown={e => e.stopPropagation()}> @@ -132,5 +140,5 @@ export class EquationBox extends ViewBoxBaseComponent() { Docs.Prototypes.TemplateMap.set(DocumentType.EQUATION, { layout: { view: EquationBox, dataField: 'text' }, - options: { acl: '', fontSize: '14px', _layout_reflowHorizontal: true, _layout_reflowVertical: true, _layout_nativeDimEditable: true, layout_hideDecorationTitle: true, systemIcon: 'BsCalculatorFill' }, // systemIcon: 'BsSuperscript' + BsSubscript + options: { acl: '', fontSize: '14px', _nativeWidth: 35, _nativeHeight: 35, _layout_reflowHorizontal: false, _layout_reflowVertical: false, _layout_nativeDimEditable: false, layout_hideDecorationTitle: true, systemIcon: 'BsCalculatorFill' }, // systemIcon: 'BsSuperscript' + BsSubscript }); -- cgit v1.2.3-70-g09d2 From d57fdc07fb88282903157b414c4a0886ddaf8bc6 Mon Sep 17 00:00:00 2001 From: bobzel Date: Tue, 12 Nov 2024 21:06:42 -0500 Subject: updated equationBox to support margins/padding like text. fixed initial undo of newly typed equation. --- src/client/documents/DocUtils.ts | 8 +++++-- src/client/documents/Documents.ts | 6 +++-- src/client/util/CurrentUserUtils.ts | 2 +- src/client/views/PropertiesView.tsx | 2 +- src/client/views/nodes/EquationBox.tsx | 42 ++++++++++++++++++++++++++-------- 5 files changed, 44 insertions(+), 16 deletions(-) (limited to 'src/client/documents/DocUtils.ts') diff --git a/src/client/documents/DocUtils.ts b/src/client/documents/DocUtils.ts index c466344b3..d11a3e235 100644 --- a/src/client/documents/DocUtils.ts +++ b/src/client/documents/DocUtils.ts @@ -696,8 +696,12 @@ export namespace DocUtils { y, title, text_fontColor: StrCast(Doc.UserDoc().fontColor), - _width: 35, - _height: 35, + _width: 50, + _height: 50, + _yMargin: 10, + _xMargin: 10, + nativeWidth: 40, + nativeHeight: 40, }) : Docs.Create.TextDocument('', { annotationOn, diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index d898fe0c5..6828a1929 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -197,8 +197,10 @@ export class DocumentOptions { data_nativeWidth?: NUMt = new NumInfo('native width of data field contents (e.g., the pixel width of an image)', false); data_nativeHeight?: NUMt = new NumInfo('native height of data field contents (e.g., the pixel height of an image)', false); linearBtnWidth?: NUMt = new NumInfo('unexpanded width of a linear menu button (button "width" changes when it expands)', false); - _nativeWidth?: NUMt = new NumInfo('native width of document contents (e.g., the pixel width of an image)', false); - _nativeHeight?: NUMt = new NumInfo('native height of document contents (e.g., the pixel height of an image)', false); + _nativeWidth?: NUMt = new NumInfo('Deprecated: use nativeWidth. native width of document contents (e.g., the pixel width of an image)', false); + _nativeHeight?: NUMt = new NumInfo('Deprecated: use nativeHeight. native height of document contents (e.g., the pixel height of an image)', false); + nativeWidth?: NUMt = new NumInfo('native width of document contents (e.g., the pixel width of an image)', false); + nativeHeight?: NUMt = new NumInfo('native height of document contents (e.g., the pixel height of an image)', false); acl?: STRt = new StrInfo('unused except as a display category in KeyValueBox'); acl_Guest?: STRt = new StrInfo("permissions granted to users logged in as 'guest' (either view, or private)"); // public permissions diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index c01f57a59..49a4a981a 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -372,7 +372,7 @@ pie title Minerals in my tap water {key: "Note", creator: opts => Docs.Create.TextDocument("", opts), opts: { _width: 200, _layout_autoHeight: true }}, {key: "Flashcard", creator: opts => Docs.Create.FlashcardDocument("", undefined, undefined, opts),opts: { _width: 300, _height: 300}}, {key: "Image", creator: opts => Docs.Create.ImageDocument("", opts), opts: { _width: 400, _height:400 }}, - {key: "Equation", creator: opts => Docs.Create.EquationDocument("",opts), opts: { _width: 35, _height: 35, }}, + {key: "Equation", creator: opts => Docs.Create.EquationDocument("",opts), opts: { _width: 50, _height: 50, nativeWidth: 40, nativeHeight: 40, _xMargin: 10, _yMargin: 10}}, {key: "Noteboard", creator: opts => Docs.Create.NoteTakingDocument([], opts), opts: { _width: 250, _height: 200, _layout_fitWidth: true}}, {key: "Simulation", creator: opts => Docs.Create.SimulationDocument(opts), opts: { _width: 300, _height: 300, }}, {key: "Collection", creator: opts => Docs.Create.FreeformDocument([], opts), opts: { _width: 150, _height: 100, _layout_fitWidth: true }}, diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx index 2bd8bc98a..1158d93e9 100644 --- a/src/client/views/PropertiesView.tsx +++ b/src/client/views/PropertiesView.tsx @@ -141,7 +141,7 @@ export class PropertiesView extends ObservableReactComponent() { @@ -68,7 +69,7 @@ export class EquationBox extends ViewBoxBaseComponent() { const nextEq = Docs.Create.EquationDocument(e.shiftKey ? StrCast(this.dataDoc.text) : 'x', { title: '# math', _width, - _height: 25, + _height: 20, x: NumCast(this.layoutDoc.x), y: NumCast(this.layoutDoc.y) + _height + 10, backgroundColor: StrCast(this.Document.backgroundColor), @@ -102,27 +103,32 @@ export class EquationBox extends ViewBoxBaseComponent() { updateSize = () => { const style = this._ref.current?.element.current && getComputedStyle(this._ref.current.element.current); if (style?.width.endsWith('px') && style?.height.endsWith('px')) { + const mathWidth = Math.max(35, NumCast(this.layoutDoc.xMargin) * 2 + Number(style.width.replace('px', ''))); + const mathHeight = Math.max(20, NumCast(this.layoutDoc.yMargin) * 2 + Number(style.height.replace('px', ''))); if (this.layoutDoc._nativeWidth) { // if equation has been scaled then editing the expression must also edit the native dimensions to keep the aspect ratio const prevNwidth = NumCast(this.layoutDoc._nativeWidth); - const newNwidth = (this.layoutDoc._nativeWidth = Math.max(35, Number(style.width.replace('px', '')))); - const newNheight = (this.layoutDoc._nativeHeight = Math.max(25, Number(style.height.replace('px', '')))); - this.layoutDoc._width = (NumCast(this.layoutDoc._width) * NumCast(this.layoutDoc._nativeWidth)) / prevNwidth; - this.layoutDoc._height = (NumCast(this.layoutDoc._width) * newNheight) / newNwidth; + this.layoutDoc._nativeWidth = mathWidth; + this.layoutDoc._nativeHeight = mathHeight; + this.layoutDoc._width = mathWidth * (NumCast(this.layoutDoc._width) / prevNwidth); + this.layoutDoc._height = mathHeight * (NumCast(this.layoutDoc._width) / mathWidth); } else { - this.layoutDoc._width = Math.max(35, Number(style.width.replace('px', ''))); - this.layoutDoc._height = Math.max(25, Number(style.height.replace('px', ''))); + this.layoutDoc._width = mathWidth; + this.layoutDoc._height = mathHeight; } } }; render() { TraceMobx(); - const scale = (this._props.NativeDimScaling?.() || 1) * NumCast(this.layoutDoc._freeform_scale, 1); + const scale = this._props.NativeDimScaling?.() || 1; return (
this.updateSize()} className="equationBox-cont" onPointerDown={e => !e.ctrlKey && e.stopPropagation()} + onBlur={() => { + FormattedTextBox.LiveTextUndo?.end(); + }} style={{ minWidth: `${100 / scale}%`, transform: `scale(${scale})`, @@ -130,6 +136,10 @@ export class EquationBox extends ViewBoxBaseComponent() { pointerEvents: !this._props.isSelected() ? 'none' : undefined, fontSize: this.fontSize, color: this.fontColor, + paddingLeft: NumCast(this.layoutDoc.xMargin), + paddingRight: NumCast(this.layoutDoc.xMargin), + paddingTop: NumCast(this.layoutDoc.yMargin), + paddingBottom: NumCast(this.layoutDoc.yMargin), }} onKeyDown={e => e.stopPropagation()}> @@ -140,5 +150,17 @@ export class EquationBox extends ViewBoxBaseComponent() { Docs.Prototypes.TemplateMap.set(DocumentType.EQUATION, { layout: { view: EquationBox, dataField: 'text' }, - options: { acl: '', fontSize: '14px', _nativeWidth: 35, _nativeHeight: 35, _layout_reflowHorizontal: false, _layout_reflowVertical: false, _layout_nativeDimEditable: false, layout_hideDecorationTitle: true, systemIcon: 'BsCalculatorFill' }, // systemIcon: 'BsSuperscript' + BsSubscript + options: { + acl: '', + _xMargin: 10, + _yMargin: 10, + fontSize: '14px', + _nativeWidth: 40, + _nativeHeight: 40, + _layout_reflowHorizontal: false, + _layout_reflowVertical: false, + _layout_nativeDimEditable: false, + layout_hideDecorationTitle: true, + systemIcon: 'BsCalculatorFill', + }, // systemIcon: 'BsSuperscript' + BsSubscript }); -- cgit v1.2.3-70-g09d2 From 1e673454f8cabf894e8dfec36734d2cb41caa7b1 Mon Sep 17 00:00:00 2001 From: bobzel Date: Mon, 3 Feb 2025 10:23:56 -0500 Subject: changed filter 'check' to be exact match and 'match' to be substring. fixed GPTpopup filtering to use a #chat tag instead of a chatFilter field. --- src/client/apis/gpt/GPT.ts | 2 +- src/client/documents/DocUtils.ts | 4 +- src/client/util/CurrentUserUtils.ts | 3 +- src/client/views/TagsView.scss | 4 +- src/client/views/TagsView.tsx | 2 +- .../views/collections/CollectionCardDeckView.tsx | 11 +++-- src/client/views/global/globalScripts.ts | 4 +- src/client/views/nodes/ComparisonBox.tsx | 2 +- src/client/views/nodes/IconTagBox.scss | 2 - src/client/views/pdf/GPTPopup/GPTPopup.tsx | 57 ++++++++++------------ 10 files changed, 44 insertions(+), 47 deletions(-) (limited to 'src/client/documents/DocUtils.ts') diff --git a/src/client/apis/gpt/GPT.ts b/src/client/apis/gpt/GPT.ts index 269a4284a..1894bb4df 100644 --- a/src/client/apis/gpt/GPT.ts +++ b/src/client/apis/gpt/GPT.ts @@ -42,7 +42,7 @@ const callTypeMap: { [type: string]: GPTCallOpts } = { model: 'gpt-4o', maxTokens: 2048, temp: 0.7, - prompt: 'Create a stack of flashcards out of this text with each question and answer labeled as question and answer. Create a title that represents the question in just a few words and label it "title". For some questions, ask "what is this image of" but tailored to stacks theme and the image and write a keyword that represents the image and label it "keyword". Otherwise, write none. Do not label each flashcard and do not include asterisks.', + prompt: 'Create a stack of at least 10 flashcards out of this text with each question and answer labeled as question and answer. Each flashcard should have a title that represents the question in just a few words and label it "title". For some questions, ask "what is this image of" but tailored to stacks theme and the image and write a keyword that represents the image and label it "keyword". Otherwise, write none. Do not label each flashcard and do not include asterisks.', }, completion: { model: 'gpt-4-turbo', maxTokens: 256, temp: 0.5, prompt: "You are a helpful assistant. Answer the user's prompt." }, diff --git a/src/client/documents/DocUtils.ts b/src/client/documents/DocUtils.ts index d11a3e235..23032b62e 100644 --- a/src/client/documents/DocUtils.ts +++ b/src/client/documents/DocUtils.ts @@ -72,9 +72,9 @@ export namespace DocUtils { } const vals = StrListCast(fieldVal); // list typing is very imperfect. casting to a string list doesn't mean that the entries will actually be strings if (vals.length) { - return vals.some(v => typeof v === 'string' && v.includes(value as string)); // bcz: arghh: Todo: comparison should be parameterized as exact, or substring + return vals.some(v => typeof v === 'string' && v === (value as string)); // bcz: arghh: Todo: comparison should be parameterized as exact, or substring } - return Field.toString(fieldVal as FieldType).includes(value as string); // bcz: arghh: Todo: comparison should be parameterized as exact, or substring + return Field.toString(fieldVal as FieldType) === (value as string); // bcz: arghh: Todo: comparison should be parameterized as exact, or substring } /** * @param docs diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index b75961575..0fd09b479 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -728,7 +728,8 @@ pie title Minerals in my tap water this.filterBtnDesc("Star", "star"), this.filterBtnDesc("Like", "heart"), this.filterBtnDesc("Todo", "bolt"), - this.filterBtnDesc("Idea", "cloud") + this.filterBtnDesc("Idea", "cloud"), + this.filterBtnDesc("Chat", "robot") ]; return [ { title:"Options",isSystem: true,icon: "gear", toolTip:"Click to customize list of filter buttons", btnType: ButtonType.ClickButton, expertMode: false, toolType:"-opts-",funcs: {}, scripts: { onClick: '{ return setTagFilter(this.toolType, false,_readOnly_);}'}}, diff --git a/src/client/views/TagsView.scss b/src/client/views/TagsView.scss index 303a08e1e..b21d303fb 100644 --- a/src/client/views/TagsView.scss +++ b/src/client/views/TagsView.scss @@ -12,7 +12,7 @@ .tagsView-list { display: flex; flex-wrap: wrap; - height: inherit; + height: 1; .iconButton-container { min-height: unset !important; } @@ -58,7 +58,7 @@ } .tagsView-editing-box { - margin-top: 8px; + margin-top: 20px; } .tagsView-input-box { diff --git a/src/client/views/TagsView.tsx b/src/client/views/TagsView.tsx index 9e936ea54..b70e21918 100644 --- a/src/client/views/TagsView.tsx +++ b/src/client/views/TagsView.tsx @@ -146,7 +146,7 @@ export class TagItem extends ObservableReactComponent { } } } - doc[DocData].tags = new List((doc[DocData].tags as List).filter(label => label !== tag)); + doc[DocData].tags = new List(StrListCast(doc[DocData].tags).filter(label => label !== tag)); }; private _ref: React.RefObject; diff --git a/src/client/views/collections/CollectionCardDeckView.tsx b/src/client/views/collections/CollectionCardDeckView.tsx index f24673c39..43464e50c 100644 --- a/src/client/views/collections/CollectionCardDeckView.tsx +++ b/src/client/views/collections/CollectionCardDeckView.tsx @@ -85,6 +85,10 @@ export class CollectionCardView extends CollectionSubView() { }); componentDidMount() { + this._disposers.chatVis = reaction( + () => GPTPopup.Instance.Visible, + vis => !vis && this.onGptHide() + ); GPTPopup.Instance.setRegenerateCallback(this.Document, this.childPairStringListAndUpdateSortDesc); this._props.setContentViewBox?.(this); // if card deck moves, then the child doc views are hidden so their screen to local transforms will return empty rectangles @@ -106,6 +110,7 @@ export class CollectionCardView extends CollectionSubView() { ); } + onGptHide = () => Doc.setDocFilter(this.Document, 'tags', '#chat', 'remove'); componentWillUnmount() { GPTPopup.Instance.setSortDesc(''); GPTPopup.Instance.onSortComplete = undefined; @@ -445,7 +450,7 @@ export class CollectionCardView extends CollectionSubView() { if (questionType === '2' || questionType === '4') { this.childDocs.forEach(d => { - d.chatFilter = false; + TagItem.removeTagFromDoc(d, '#chat'); }); } @@ -471,8 +476,8 @@ export class CollectionCardView extends CollectionSubView() { break; case '2': case '4': - doc.chatFilter = true; - Doc.setDocFilter(DocCast(this.Document), 'chatFilter', true, 'check'); + TagItem.addTagToDoc(doc, '#chat'); + Doc.setDocFilter(this.Document, 'tags', '#chat', 'check'); break; } } else { diff --git a/src/client/views/global/globalScripts.ts b/src/client/views/global/globalScripts.ts index b02eabab0..b44292164 100644 --- a/src/client/views/global/globalScripts.ts +++ b/src/client/views/global/globalScripts.ts @@ -223,9 +223,9 @@ ScriptingGlobals.add(function showFreeform( setDoc: (doc: Doc, dv: DocumentView) => { doc[Doc.LayoutFieldKey(doc)+"_sort_desc"] = true; }, }], ['toggle-chat', { - checkResult: (doc: Doc) => GPTPopup.Instance.visible, + checkResult: (doc: Doc) => GPTPopup.Instance.Visible, setDoc: (doc: Doc, dv: DocumentView) => { - if (GPTPopup.Instance.visible){ + if (GPTPopup.Instance.Visible){ doc[Doc.LayoutFieldKey(doc)+"_sort"] = ''; GPTPopup.Instance.setVisible(false); diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx index f5291a4c1..53fbd11c5 100644 --- a/src/client/views/nodes/ComparisonBox.tsx +++ b/src/client/views/nodes/ComparisonBox.tsx @@ -510,7 +510,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent() * Calls GPT for each flashcard type. */ askGPT = async (callType: GPTCallType) => { - const questionText = 'Question: ' + this.frontText; + const questionText = this.frontText; const queryText = questionText + (callType == GPTCallType.QUIZ ? ' UserAnswer: ' + this._inputValue + '. ' + ' Rubric: ' + this.backText : ''); this.loading = true; diff --git a/src/client/views/nodes/IconTagBox.scss b/src/client/views/nodes/IconTagBox.scss index 90cc06092..c79d662f4 100644 --- a/src/client/views/nodes/IconTagBox.scss +++ b/src/client/views/nodes/IconTagBox.scss @@ -10,8 +10,6 @@ gap: 5px; padding-left: 5px; padding-right: 5px; - padding-top: 2px; - padding-bottom: 2px; button { pointer-events: auto; diff --git a/src/client/views/pdf/GPTPopup/GPTPopup.tsx b/src/client/views/pdf/GPTPopup/GPTPopup.tsx index da0cbea7a..f5a9f9e6a 100644 --- a/src/client/views/pdf/GPTPopup/GPTPopup.tsx +++ b/src/client/views/pdf/GPTPopup/GPTPopup.tsx @@ -46,9 +46,9 @@ export class GPTPopup extends ObservableReactComponent { @observable private chatMode: boolean = false; private correlatedColumns: string[] = []; - @observable public visible: boolean = false; + @observable public Visible: boolean = false; @action public setVisible = (vis: boolean) => { - this.visible = vis; + this.Visible = vis; }; @observable public loading: boolean = false; @action public setLoading = (loading: boolean) => { @@ -114,8 +114,8 @@ export class GPTPopup extends ObservableReactComponent { this.sortDesc = t; }; - @observable onSortComplete?: (sortResult: string, questionType: string, tag?: string) => void; - @observable onQuizRandom?: () => void; + onSortComplete?: (sortResult: string, questionType: string, tag?: string) => void; + onQuizRandom?: () => void; @observable cardsDoneLoading = false; @observable collectionDoc: Doc | undefined = undefined; @@ -154,30 +154,27 @@ export class GPTPopup extends ObservableReactComponent { generateQuiz = async () => { this.setLoading(true); - const selected = DocumentView.SelectedDocs().lastElement(); + await this.regenerateCallback?.(); - const questionText = 'Question: ' + StrCast(selected.gptInputText); - const rubricText = 'Rubric: ' + StrCast(selected.gptRubric); - const queryText = questionText + ' UserAnswer: ' + this.quizAnswer + '. ' + rubricText; + const selected = DocumentView.SelectedDocs().lastElement(); + if (!StrCast(selected.gptRubric)) { + await this.generateRubric(StrCast(selected.gptInputText), selected); + } try { - const res = await gptAPICall(queryText, GPTCallType.QUIZ); - if (!res) { - console.error('GPT call failed'); - return; - } - console.log(res); - this.setQuizResp(res); - this.conversationArray.push(res); + const res = await gptAPICall('Question: ' + StrCast(selected.gptInputText) + ' UserAnswer: ' + this.quizAnswer + '. Rubric: ' + StrCast(selected.gptRubric), GPTCallType.QUIZ); + if (res) { + this.setQuizResp(res); + this.conversationArray.push(res); - this.setLoading(false); + this.setLoading(false); + this.onQuizRandom?.(); + } else { + console.error('GPT provided no response'); + } } catch (err) { console.error('GPT call failed', err); } - - if (this.onQuizRandom) { - this.onQuizRandom(); - } }; /** @@ -223,9 +220,7 @@ export class GPTPopup extends ObservableReactComponent { generateCard = async () => { this.setLoading(true); - if (this.regenerateCallback) { - await this.regenerateCallback(); - } + await this.regenerateCallback?.(); try { const questionType = await gptAPICall(this.chatSortPrompt, GPTCallType.TYPE); @@ -442,9 +437,7 @@ export class GPTPopup extends ObservableReactComponent { onClick={() => { this.conversationArray = ['Define the selected card!']; this.setMode(GPTPopupMode.QUIZ); - if (this.onQuizRandom) { - this.onQuizRandom(); - } + this.onQuizRandom?.(); }} color={StrCast(Doc.UserDoc().userVariantColor)} type={Type.TERT} @@ -694,13 +687,13 @@ export class GPTPopup extends ObservableReactComponent { } onClick={() => (this.mode = GPTPopupMode.CARD)} style={{ right: '50px', position: 'absolute' }} /> )} this.collectionDoc && (this.collectionDoc.childFilters = undefined)} + onClick={() => this.collectionDoc && Doc.setDocFilter(this.collectionDoc, 'tags', '#chat', 'remove')} /> { } return ( -
+
{content}
-- cgit v1.2.3-70-g09d2 From 1c7c5bcadc7081e1a7e582413eee37b8201e1c4c Mon Sep 17 00:00:00 2001 From: bobzel Date: Mon, 24 Feb 2025 18:07:36 -0500 Subject: added borders (width, color, and style) for DocumentViews. fixed dropdown slider buttons to show text labels. --- .../components/NumberDropdown/NumberDropdown.scss | 18 +- .../components/NumberDropdown/NumberDropdown.tsx | 182 +++++++++------------ packages/components/src/components/Popup/Popup.tsx | 8 +- packages/components/src/global/globalTypes.ts | 137 +++++++--------- src/client/documents/DocUtils.ts | 4 + src/client/documents/Documents.ts | 4 +- src/client/util/CurrentUserUtils.ts | 6 +- src/client/views/StyleProp.ts | 1 + src/client/views/StyleProvider.tsx | 51 +++--- src/client/views/global/globalScripts.ts | 55 +++++++ .../views/nodes/CollectionFreeFormDocumentView.tsx | 2 +- src/client/views/nodes/DocumentView.tsx | 21 +-- src/client/views/nodes/FontIconBox/FontIconBox.tsx | 1 + .../views/nodes/formattedText/FormattedTextBox.tsx | 2 +- 14 files changed, 263 insertions(+), 229 deletions(-) (limited to 'src/client/documents/DocUtils.ts') diff --git a/packages/components/src/components/NumberDropdown/NumberDropdown.scss b/packages/components/src/components/NumberDropdown/NumberDropdown.scss index 0999afb98..4ed5855d9 100644 --- a/packages/components/src/components/NumberDropdown/NumberDropdown.scss +++ b/packages/components/src/components/NumberDropdown/NumberDropdown.scss @@ -1,5 +1,19 @@ @import '../../global/globalCssVariables.scss'; .numberDropdown-container { - -} \ No newline at end of file +} + +.form-wrapper { + .iconButton-label { + position: relative; + z-index: 2; + width: 100%; + overflow: hidden; + white-space: normal; + display: flex; + text-align: center; + justify-content: center; + align-items: center; + font-size: $xsmall-fontSize !important; + } +} diff --git a/packages/components/src/components/NumberDropdown/NumberDropdown.tsx b/packages/components/src/components/NumberDropdown/NumberDropdown.tsx index a26cd71ab..7f12198d5 100644 --- a/packages/components/src/components/NumberDropdown/NumberDropdown.tsx +++ b/packages/components/src/components/NumberDropdown/NumberDropdown.tsx @@ -1,137 +1,101 @@ -import * as React from 'react' -import { Colors, IGlobalProps, INumberProps, Size, Type, getFontSize , getFormLabelSize } from '../../global' -import { Popup } from '../Popup' -import { Toggle, ToggleType } from '../Toggle' -import { useState } from 'react' -import { Slider } from '../Slider' -import { ListBox } from '../ListBox' -import { IListItemProps } from '../ListItem' -import { Group } from '../Group' -import { IconButton } from '../IconButton' -import * as fa from 'react-icons/fa' +import * as React from 'react'; +import { Colors, INumberProps, Size, getFormLabelSize } from '../../global'; +import { Popup } from '../Popup'; +import { Toggle, ToggleType } from '../Toggle'; +import { useState } from 'react'; +import { Slider } from '../Slider'; +import { ListBox } from '../ListBox'; +import { IListItemProps } from '../ListItem'; +import { Group } from '../Group'; +import { IconButton } from '../IconButton'; +import * as fa from 'react-icons/fa'; +import './NumberDropdown.scss'; - -export type NumberDropdownType = 'slider' | 'dropdown' | 'input' +export type NumberDropdownType = 'slider' | 'dropdown' | 'input'; export interface INumberDropdownProps extends INumberProps { - numberDropdownType: NumberDropdownType, - showPlusMinus?: boolean + numberDropdownType: NumberDropdownType; + showPlusMinus?: boolean; } export const NumberDropdown = (props: INumberDropdownProps) => { - const [numberLoc, setNumberLoc] = useState(0) - const { - fillWidth, - numberDropdownType = false, - color = Colors.MEDIUM_BLUE, - type, - formLabelPlacement, - showPlusMinus, - min, - max, - unit, - step = 1, - number = numberLoc, - setNumber = setNumberLoc, - size, - formLabel, - tooltip } = - props; + const [numberLoc, setNumberLoc] = useState(0); + const { fillWidth, numberDropdownType = false, color = Colors.MEDIUM_BLUE, type, formLabelPlacement, showPlusMinus, min, max, unit, step = 1, number = numberLoc, setNumber = setNumberLoc, size, formLabel, tooltip } = props; const [isOpen, setOpen] = useState(false); let toggleText = number.toString(); - if (unit) toggleText = toggleText + unit - let toggle = setOpen(!isOpen)} - />; - + if (unit) toggleText = toggleText + unit; + let toggle = setOpen(!isOpen)} />; + if (showPlusMinus) { - toggle = - } - color={color} - onClick={(e) => { - e.stopPropagation(); - setNumber(number - step); - }} - fillWidth={fillWidth} - tooltip={`Subtract ${step}${unit}`} - /> - {toggle} - } - color={color} - onClick={(e) => { - e.stopPropagation(); - setNumber(number + step); - }} - fillWidth={fillWidth} - tooltip={`Add ${step}${unit}`} - /> - + toggle = ( + + } + color={color} + onClick={e => { + e.stopPropagation(); + setNumber(number - step); + }} + fillWidth={fillWidth} + tooltip={`Subtract ${step}${unit}`} + /> + {toggle} + } + color={color} + onClick={e => { + e.stopPropagation(); + setNumber(number + step); + }} + fillWidth={fillWidth} + tooltip={`Add ${step}${unit}`} + /> + + ); } - + let popup; switch (numberDropdownType) { case 'dropdown': - let items: IListItemProps[] = []; - for (let i = min; i <= max; i += step) { - let text = i.toString() - if (unit) text = i.toString() + unit - items.push( - { + { + const items: IListItemProps[] = []; + for (let i = min; i <= max; i += step) { + let text = i.toString(); + if (unit) text = i.toString() + unit; + items.push({ text: text, val: i, - style: { textAlign: 'center' } - } - ) + style: { textAlign: 'center' }, + }); + } + popup = setNumber(num as number)} items={items} />; } - popup = setNumber(num as number)} - items={items} - /> break; case 'slider': default: - popup = + popup = ; break; case 'input': - popup = + popup = ; break; } - const numberDropdown: JSX.Element =
- -
+ const numberDropdown: JSX.Element = ( +
+ +
+ ); - return ( - formLabel ? -
+ return formLabel ? ( +
{numberDropdown} -
{formLabel}
+
+ {formLabel} +
- : + ) : ( numberDropdown - ) -} \ No newline at end of file + ); +}; diff --git a/packages/components/src/components/Popup/Popup.tsx b/packages/components/src/components/Popup/Popup.tsx index 92e7227bd..aa732d568 100644 --- a/packages/components/src/components/Popup/Popup.tsx +++ b/packages/components/src/components/Popup/Popup.tsx @@ -1,5 +1,5 @@ import React, { useEffect, useRef, useState } from 'react'; -import { Colors, IGlobalProps, Placement, Size, getFormLabelSize, isDark } from '../../global'; +import { Colors, IGlobalProps, Placement, Size, isDark } from '../../global'; import { Toggle, ToggleType } from '../Toggle'; import './Popup.scss'; import { Popper } from '@mui/material'; @@ -16,7 +16,7 @@ export interface IPopupProps extends IGlobalProps { iconPlacement?: Placement; placement?: Placement; size?: Size; - height?: number; + height?: number | string; toggle?: JSX.Element; popup: JSX.Element | string | (() => JSX.Element); trigger?: PopupTrigger; @@ -62,12 +62,12 @@ export const Popup = (props: IPopupProps) => { } = props; const triggerRef = useRef(null); - const popperRef = useRef(null); + const popperRef = useRef(null); let timeout = setTimeout(() => {}); const handlePointerAwayDown = (e: PointerEvent) => { - const rect = (popperRef.current as any)?.getBoundingClientRect(); + const rect = popperRef.current?.getBoundingClientRect(); if (rect && !(rect.left < e.clientX && rect.top < e.clientY && rect.right > e.clientX && rect.bottom > e.clientY) && !popupContainsPt?.(e.clientX, e.clientY)) { e.preventDefault(); setOpen(false); diff --git a/packages/components/src/global/globalTypes.ts b/packages/components/src/global/globalTypes.ts index aa8451a9c..764fe7422 100644 --- a/packages/components/src/global/globalTypes.ts +++ b/packages/components/src/global/globalTypes.ts @@ -1,87 +1,76 @@ -import { PointerEventHandler } from "react" -import { Size } from "./globalEnums" +import { PointerEventHandler } from 'react'; +import { Size } from './globalEnums'; -export interface IGlobalProps { - // Size - size?: Size - height?: number - width?: number - fillWidth?: boolean - color?: string - background?: string +export enum Type { + PRIM = 'primary', + SEC = 'secondary', + TERT = 'tertiary', +} - // Type - type?: Type +export type Placement = 'bottom-end' | 'bottom-start' | 'bottom' | 'left-end' | 'left-start' | 'left' | 'right-end' | 'right-start' | 'right' | 'top-end' | 'top-start' | 'top'; - // Status - inactive?: boolean +export type Alignment = 'flex-start' | 'flex-end' | 'center'; - // Content - tooltip?: string - tooltipPlacement?: Placement +export type TextAlignment = 'center' | 'left' | 'right'; - // Label - label?: string - hideLabel?: boolean +export interface IGlobalProps { + // Size + size?: Size; + height?: number | string; + width?: number; + fillWidth?: boolean; + color?: string; + background?: string; - // Label when used in forms - formLabel?: string - formLabelPlacement?: Placement + // Type + type?: Type; - // Custom style - style?: React.CSSProperties + // Status + inactive?: boolean; - // Global pointer events - onPointerDown?: PointerEventHandler | undefined; - onPointerDownCapture?: PointerEventHandler | undefined; - onPointerMove?: PointerEventHandler | undefined; - onPointerMoveCapture?: PointerEventHandler | undefined; - onPointerUp?: PointerEventHandler | undefined; - onPointerUpCapture?: PointerEventHandler | undefined; - onPointerCancel?: PointerEventHandler | undefined; - onPointerCancelCapture?: PointerEventHandler | undefined; - onPointerEnter?: PointerEventHandler | undefined; - onPointerEnterCapture?: PointerEventHandler | undefined; - onPointerLeave?: PointerEventHandler | undefined; - onPointerLeaveCapture?: PointerEventHandler | undefined; - onPointerOver?: PointerEventHandler | undefined; - onPointerOverCapture?: PointerEventHandler | undefined; - onPointerOut?: PointerEventHandler | undefined; - onPointerOutCapture?: PointerEventHandler | undefined; - onGotPointerCapture?: PointerEventHandler | undefined; - onGotPointerCaptureCapture?: PointerEventHandler | undefined; - onLostPointerCapture?: PointerEventHandler | undefined; - onLostPointerCaptureCapture?: PointerEventHandler | undefined; -} + // Content + tooltip?: string; + tooltipPlacement?: Placement; -export interface INumberProps extends IGlobalProps { - min: number, - max: number, - step?: number, - number: number - setNumber?: (num: number) => unknown, - unit?: string -} + // Label + label?: string; + hideLabel?: boolean; -export enum Type { - PRIM = "primary", - SEC = "secondary", - TERT = "tertiary", -} + // Label when used in forms + formLabel?: string; + formLabelPlacement?: Placement; -export type Placement = 'bottom-end' - | 'bottom-start' - | 'bottom' - | 'left-end' - | 'left-start' - | 'left' - | 'right-end' - | 'right-start' - | 'right' - | 'top-end' - | 'top-start' - | 'top' + // Custom style + style?: React.CSSProperties; -export type Alignment = 'flex-start' | 'flex-end' | 'center' + // Global pointer events + onPointerDown?: PointerEventHandler | undefined; + onPointerDownCapture?: PointerEventHandler | undefined; + onPointerMove?: PointerEventHandler | undefined; + onPointerMoveCapture?: PointerEventHandler | undefined; + onPointerUp?: PointerEventHandler | undefined; + onPointerUpCapture?: PointerEventHandler | undefined; + onPointerCancel?: PointerEventHandler | undefined; + onPointerCancelCapture?: PointerEventHandler | undefined; + onPointerEnter?: PointerEventHandler | undefined; + onPointerEnterCapture?: PointerEventHandler | undefined; + onPointerLeave?: PointerEventHandler | undefined; + onPointerLeaveCapture?: PointerEventHandler | undefined; + onPointerOver?: PointerEventHandler | undefined; + onPointerOverCapture?: PointerEventHandler | undefined; + onPointerOut?: PointerEventHandler | undefined; + onPointerOutCapture?: PointerEventHandler | undefined; + onGotPointerCapture?: PointerEventHandler | undefined; + onGotPointerCaptureCapture?: PointerEventHandler | undefined; + onLostPointerCapture?: PointerEventHandler | undefined; + onLostPointerCaptureCapture?: PointerEventHandler | undefined; +} -export type TextAlignment = 'center' | 'left' | 'right' \ No newline at end of file +export interface INumberProps extends IGlobalProps { + min: number; + max: number; + step?: number; + number: number; + setNumber?: (num: number) => unknown; + unit?: string; +} diff --git a/src/client/documents/DocUtils.ts b/src/client/documents/DocUtils.ts index 23032b62e..f1de6fe36 100644 --- a/src/client/documents/DocUtils.ts +++ b/src/client/documents/DocUtils.ts @@ -692,6 +692,8 @@ export namespace DocUtils { // annotationOn, backgroundColor: backgroundColor ?? StrCast(Doc.UserDoc().textBackgroundColor), + borderColor: Doc.UserDoc().borderColor as string, + borderWidth: Doc.UserDoc().borderWidth as number, x, y, title, @@ -718,6 +720,8 @@ export namespace DocUtils { _layout_fitWidth: true, _layout_autoHeight: true, backgroundColor: StrCast(Doc.UserDoc().textBackgroundColor), + borderColor: Doc.UserDoc().borderColor as string, + borderWidth: Doc.UserDoc().borderWidth as number, text_fitBox: BoolCast(Doc.UserDoc().fitBox), text_align: StrCast(Doc.UserDoc().textAlign), text_fontColor: StrCast(Doc.UserDoc().fontColor), diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index 891223952..1ce25165c 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -240,8 +240,8 @@ export class DocumentOptions { dataViz?: string; dataViz_savedTemplates?: LISTt; - borderWidth?: STRt = new StrInfo('Width of user-added border', false); - borderColor?: STRt = new StrInfo('Color of user-added border', false); + borderWidth?: NUMt = new NumInfo('Width of docuent border', false); + borderColor?: STRt = new StrInfo('Color of document border', false); text_fontColor?: STRt = new StrInfo('Color of text', false); hCentering?: 'h-left' | 'h-center' | 'h-right'; isDefaultTemplateDoc?: BOOLt = new BoolInfo(''); diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index fb349abd9..d2d7cabd2 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -734,7 +734,7 @@ pie title Minerals in my tap water } static viewTools(): Button[] { return [ - { title: "Show Tags", icon: "id-card", toolTip: "Toggle tags", btnType: ButtonType.ToggleButton, ignoreClick: true, expertMode: false, toolType:"toggle-tags",funcs: { }, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'} }, + { title: "Tags", icon: "id-card", toolTip: "Toggle Tags display", btnType: ButtonType.ToggleButton, ignoreClick: true, expertMode: false, toolType:"toggle-tags",funcs: { }, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'} }, { title: "Snap", icon: "th", toolTip: "Show Snap Lines", btnType: ButtonType.ToggleButton, ignoreClick: true, expertMode: false, toolType:"snaplines", funcs: { hidden: `!SelectedDocType("${CollectionViewType.Freeform}", this.expertMode)`}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}}, // Only when floating document is selected in freeform { title: "Grid", icon: "border-all", toolTip: "Show Grid", btnType: ButtonType.ToggleButton, ignoreClick: true, expertMode: false, toolType:"grid", funcs: { hidden: `!SelectedDocType("${CollectionViewType.Freeform}", this.expertMode)`}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}}, // Only when floating document is selected in freeform { title: "Fit All", icon: "object-group", toolTip:"Fit Docs to View (double tap to persist)", @@ -833,11 +833,13 @@ pie title Minerals in my tap water { 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_)'} }, // Only when a document is selected + { title: "Border", icon: "pen", toolTip: "Border Color", btnType: ButtonType.ColorButton, expertMode: false, ignoreClick: true, width: 30, scripts: { script: 'return setBorderColor(value, _readOnly_)'} }, // Only when a document is selected + { title: "B.Width", toolTip: "Border width", btnType: ButtonType.NumberSliderButton, ignoreClick: true, scripts: {script: '{ return setBorderWidth(value, _readOnly_);}'}, numBtnMin: 0, linearBtnWidth:40}, { 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: "Chat", icon:"lightbulb", toolTip: "Toggle Chat Assistant",btnType: ButtonType.ToggleButton, expertMode: false, toolType:"toggle-chat", funcs: {}, width: 30, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'} }, + { title: "Chat", icon: "lightbulb", toolTip: "Toggle Chat Assistant",btnType: ButtonType.ToggleButton, expertMode: false, toolType:"toggle-chat", funcs: {}, width: 30, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'} }, { title: "Filter", icon: "=", toolTip: "Filter cards by tags", subMenu: CurrentUserUtils.filterTools(), ignoreClick:true, toolType:DocumentType.COL, funcs: {hidden: '!SelectedDocType(this.toolType, this.expertMode)'}, btnType: ButtonType.MultiToggleButton, width: 30, backgroundColor: doc.userVariantColor as string}, { title: "Sort", icon: "Sort", toolTip: "Sort Documents", subMenu: CurrentUserUtils.sortTools(), expertMode: false, toolType:DocumentType.COL, funcs: {hidden: `!SelectedDocType(this.toolType, this.expertMode)`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} }, // Always available { 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 diff --git a/src/client/views/StyleProp.ts b/src/client/views/StyleProp.ts index 56367e70b..1ef7a9e1f 100644 --- a/src/client/views/StyleProp.ts +++ b/src/client/views/StyleProp.ts @@ -5,6 +5,7 @@ export enum StyleProp { Opacity = 'opacity', // opacity of the document view BoxShadow = 'boxShadow', // box shadow - used for making collections standout and for showing clusters in free form views BorderRounding = 'borderRounding', // border radius of the document view + Border = 'border', // border of document view Color = 'color', // foreground color of Document view items BackgroundColor = 'backgroundColor', // background color of a document view FillColor = 'fillColor', // fill color of an ink stroke or shape diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx index bebc9a341..b04b1ae65 100644 --- a/src/client/views/StyleProvider.tsx +++ b/src/client/views/StyleProvider.tsx @@ -206,33 +206,40 @@ export function DefaultStyleProvider(doc: Opt, props: Opt - - - -
- ), - }; - } + // const borderPath = doc && border(doc, NumCast(doc._width), NumCast(doc._height), radius, -ratio/2); + // return !borderPath + // ? null + // : { + // clipPath: `path('${borderPath}')`, + // jsx: ( + //
+ // + // + // + //
+ // ), + // }; + // } case StyleProp.HeaderMargin: return ([CollectionViewType.Stacking, CollectionViewType.NoteTaking, CollectionViewType.Masonry, CollectionViewType.Tree].includes(doc?._type_collection as CollectionViewType) || (doc?.type === DocumentType.RTF && !layoutShowTitle()?.includes('noMargin')) || diff --git a/src/client/views/global/globalScripts.ts b/src/client/views/global/globalScripts.ts index 79873ed8f..b455f76e4 100644 --- a/src/client/views/global/globalScripts.ts +++ b/src/client/views/global/globalScripts.ts @@ -66,6 +66,49 @@ ScriptingGlobals.add(function setView(view: string, shiftKey: boolean, checkResu return undefined; }); +// toggle: Set overlay status of selected document +// eslint-disable-next-line prefer-arrow-callback +ScriptingGlobals.add(function setBorderColor(color?: string, checkResult?: boolean) { + const selectedViews = DocumentView.Selected(); + const defaultBorder = () => StrCast(Doc.UserDoc().borderColor, 'transparent'); + const setDefaultBorder = (c: string) => { Doc.UserDoc().borderColor = c; }; // prettier-ignore + const fieldKey = 'borderColor'; + if (selectedViews.length) { + if (checkResult) { + const selView = selectedViews.lastElement(); + 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] || defaultBorder(); + } + setDefaultBorder(color ?? 'transparent'); + selectedViews.forEach(dv => { + const layoutFrameNumber = Cast(dv.containerViewPath?.().lastElement()?.Document?._currentFrame, 'number'); // frame number that container is at which determines layout frame values + const contentFrameNumber = Cast(dv.Document?._currentFrame, 'number', layoutFrameNumber ?? null); // frame number that content is at which determines what content is displayed + if (contentFrameNumber !== undefined) { + const obj: { [key: string]: Opt } = {}; + obj[fieldKey] = color; + CollectionFreeFormDocumentView.setStringValues(contentFrameNumber, dv.Document, obj); + } else { + const dataKey = Doc.LayoutFieldKey(dv.Document); + const alternate = (dv.layoutDoc[dataKey + '_usePath'] ? '_' + dv.layoutDoc[dataKey + '_usePath'] : '').replace(':hover', ''); + dv.layoutDoc[fieldKey + alternate] = undefined; + dv.dataDoc[fieldKey + alternate] = color; + } + }); + } else { + const selected = DocumentView.SelectedDocs().length ? DocumentView.SelectedDocs() : LinkManager.Instance.currentLink ? [LinkManager.Instance.currentLink] : []; + if (checkResult) { + return (selected.lastElement() ?? Doc.UserDoc()).borderColor ?? defaultBorder(); + } + if (!selected.length) setDefaultBorder(color ?? 'transparent'); + else + selected.forEach(doc => { + doc[DocData].borderColor = color; + }); + } + return ''; +}); + // toggle: Set overlay status of selected document // eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(function setBackgroundColor(color?: string, checkResult?: boolean) { @@ -408,6 +451,18 @@ ScriptingGlobals.add(function activeEraserTool() { return StrCast(Doc.UserDoc().activeEraserTool, InkEraserTool.Stroke); }, 'returns the current eraser tool'); +// eslint-disable-next-line prefer-arrow-callback +ScriptingGlobals.add(function setBorderWidth(value: number, checkResult?: boolean) { + const selected = DocumentView.SelectedDocs().lastElement(); + if (checkResult) return NumCast((selected ?? Doc.UserDoc()).borderWidth); + if (!selected) Doc.UserDoc().borderWidth = value; + else + DocumentView.SelectedDocs().map(doc => { + doc.borderWidth = value; + }); + return undefined; +}, 'sets the border width of the selected document'); + // toggle: Set overlay status of selected document // eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(function setInkProperty(option: InkProperty, value: string | number, checkResult?: boolean) { diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx index beea6ab3c..76d6ed80b 100644 --- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx @@ -69,7 +69,7 @@ export class CollectionFreeFormDocumentView extends DocComponent (Doc.LayoutFieldKey(doc) ? [Doc.LayoutFieldKey(doc)] : []); // fields that are configured to be animatable using animation frames public static from(dv?: DocumentView): CollectionFreeFormDocumentView | undefined { return dv?._props.reactParent instanceof CollectionFreeFormDocumentView ? dv._props.reactParent : undefined; diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 0193fd328..441d6053f 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -131,6 +131,7 @@ export class DocumentViewInternal extends DocComponent this._props.styleProvider?.(doc, this._props, sprop); @computed get opacity() { return this.style(this.layoutDoc, StyleProp.Opacity) as number; } // prettier-ignore @computed get boxShadow() { return this.style(this.layoutDoc, StyleProp.BoxShadow) as string; } // prettier-ignore + @computed get border() { return this.style(this.layoutDoc, StyleProp.Border) as string || ""; } // prettier-ignore @computed get borderRounding() { return this.style(this.layoutDoc, StyleProp.BorderRounding) as string; } // prettier-ignore @computed get widgetDecorations() { return this.style(this.layoutDoc, StyleProp.Decorations) as JSX.Element; } // prettier-ignore @computed get backgroundBoxColor(){ return this.style(this.layoutDoc, StyleProp.BackgroundColor + ':docView') as string; } // prettier-ignore @@ -684,14 +685,14 @@ export class DocumentViewInternal extends DocComponent this._rootSelected; - panelHeight = () => this._props.PanelHeight() - this.headerMargin; + panelHeight = () => this._props.PanelHeight() - this.headerMargin - 2 * NumCast(this.Document.borderWidth); screenToLocalContent = () => this._props .ScreenToLocalTransform() - .translate(0, -this.headerMargin) + .translate(-NumCast(this.Document.borderWidth), -this.headerMargin - NumCast(this.Document.borderWidth)) .scale(this._props.showAIEditor ? (this._props.PanelHeight() || 1) / this.aiContentsHeight() : 1); onClickFunc = this.disableClickScriptFunc ? undefined : () => this.onClickHdlr; - setHeight = (height: number) => { !this._props.suppressSetHeight && (this.layoutDoc._height = Math.min(NumCast(this.layoutDoc._maxHeight, Number.MAX_SAFE_INTEGER), height)); } // prettier-ignore + setHeight = (height: number) => { !this._props.suppressSetHeight && (this.layoutDoc._height = Math.min(NumCast(this.layoutDoc._maxHeight, Number.MAX_SAFE_INTEGER), height + 2 * NumCast(this.Document.borderWidth))); } // prettier-ignore setContentView = action((view: ViewBoxInterface) => { this._componentView = view; }); // prettier-ignore isContentActive = (): boolean | undefined => this._isContentActive; childFilters = () => [...this._props.childFilters(), ...StrListCast(this.layoutDoc.childFilters)]; @@ -989,15 +990,11 @@ export class DocumentViewInternal extends DocComponent (!SnappingManager.IsDragging || SnappingManager.CanEmbed) && Doc.BrushDoc(this.Document)} onPointerLeave={e => !isParentOf(this._contentDiv, document.elementFromPoint(e.nativeEvent.x, e.nativeEvent.y)) && Doc.UnBrushDoc(this.Document)} style={{ - borderRadius: this.borderRounding, + borderRadius: this._componentView?.isUnstyledView?.() ? undefined : this.borderRounding, pointerEvents: this._pointerEvents === 'visiblePainted' ? 'none' : this._pointerEvents, // visible painted means that the underlying doc contents are irregular and will process their own pointer events (otherwise, the contents are expected to fill the entire doc view box so we can handle pointer events here) }}> {this._componentView?.isUnstyledView?.() || this.Document.type === DocumentType.CONFIG || !renderDoc ? renderDoc : DocumentViewInternal.AnimationEffect(renderDoc, this.Document[Animation], this.Document)} @@ -1487,7 +1484,7 @@ export class DocumentView extends DocComponent() { ShouldNotScale = () => this.shouldNotScale; NativeWidth = () => this.effectiveNativeWidth; NativeHeight = () => this.effectiveNativeHeight; - PanelWidth = () => this.panelWidth; + PanelWidth = () => this.panelWidth - 2 * NumCast(this.Document.borderWidth); PanelHeight = () => this.panelHeight; ReducedPanelWidth = () => this.panelWidth / 2; ReducedPanelHeight = () => this.panelWidth / 2; diff --git a/src/client/views/nodes/FontIconBox/FontIconBox.tsx b/src/client/views/nodes/FontIconBox/FontIconBox.tsx index f58862028..f60c83343 100644 --- a/src/client/views/nodes/FontIconBox/FontIconBox.tsx +++ b/src/client/views/nodes/FontIconBox/FontIconBox.tsx @@ -134,6 +134,7 @@ export class FontIconBox extends ViewBoxBaseComponent() { min={NumCast(this.dataDoc.numBtnMin, 0)} max={NumCast(this.dataDoc.numBtnMax, 100)} number={checkResult} + size={Size.XSMALL} setNumber={undoable(value => numScript(value), `${this.Document.title} button set from list`)} fillWidth /> diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index 3abb39ff2..da2131940 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -1282,7 +1282,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent ({ sidebarHeight: this.sidebarHeight, textHeight: this.textHeight, layoutAutoHeight: this.layout_autoHeight, marginsHeight: this.layout_autoHeightMargins }), + () => ({ border: this._props.PanelHeight(), sidebarHeight: this.sidebarHeight, textHeight: this.textHeight, layoutAutoHeight: this.layout_autoHeight, marginsHeight: this.layout_autoHeightMargins }), ({ sidebarHeight, textHeight, layoutAutoHeight, marginsHeight }) => { const newHeight = this.contentScaling * (marginsHeight + Math.max(sidebarHeight, textHeight)); if ( -- cgit v1.2.3-70-g09d2 From 2a71391a44eddcc438a5b8ae7d7c2d873f8adcfc Mon Sep 17 00:00:00 2001 From: bobzel Date: Wed, 26 Feb 2025 21:41:50 -0500 Subject: made regenerate ai show up next to image doc. show ai prompt for firefly in gptpopup . --- src/client/documents/DocUtils.ts | 6 ++++-- src/client/views/ViewBoxInterface.ts | 1 + .../collectionFreeForm/CollectionFreeFormView.tsx | 11 ++++++----- src/client/views/nodes/ImageBox.tsx | 12 +++++++++--- src/client/views/pdf/GPTPopup/GPTPopup.tsx | 2 +- src/client/views/smartdraw/SmartDrawHandler.tsx | 2 -- 6 files changed, 21 insertions(+), 13 deletions(-) (limited to 'src/client/documents/DocUtils.ts') diff --git a/src/client/documents/DocUtils.ts b/src/client/documents/DocUtils.ts index f1de6fe36..18c8d97d4 100644 --- a/src/client/documents/DocUtils.ts +++ b/src/client/documents/DocUtils.ts @@ -33,7 +33,6 @@ import { TaskCompletionBox } from '../views/nodes/TaskCompletedBox'; import { DocumentType } from './DocumentTypes'; import { Docs, DocumentOptions } from './Documents'; import { DocumentView } from '../views/nodes/DocumentView'; -import { CollectionFreeFormView } from '../views/collections/collectionFreeForm'; export namespace DocUtils { function HasFunctionFilter(val: string) { @@ -393,7 +392,10 @@ export namespace DocUtils { })) as ContextMenuProps[]; documentList.push({ description: ':Smart Drawing', - event: e => (DocumentView.Selected().lastElement().ComponentView as CollectionFreeFormView)?.showSmartDraw(e?.x || 0, e?.y || 0), + event: e => + DocumentView.Selected() + .lastElement() + .ComponentView?.showSmartDraw?.(e?.x || 0, e?.y || 0), icon: 'file', }); ContextMenu.Instance.addItem({ diff --git a/src/client/views/ViewBoxInterface.ts b/src/client/views/ViewBoxInterface.ts index b943259ff..0ddac8914 100644 --- a/src/client/views/ViewBoxInterface.ts +++ b/src/client/views/ViewBoxInterface.ts @@ -24,6 +24,7 @@ export abstract class ViewBoxInterface

extends ObservableReactComponent void; // moves contents of collection to parent hasChildDocs?: () => Doc[]; docEditorView?: () => void; + showSmartDraw?: (x: number, y: number, regenerate?: boolean) => void; updateIcon?: (usePanelDimensions?: boolean) => Promise; // updates the icon representation of the document getAnchor?: (addAsAnnotation: boolean, pinData?: PinProps) => Doc; // returns an Anchor Doc that represents the current state of the doc's componentview (e.g., the current playhead location of a an audio/video box) restoreView?: (viewSpec: Doc) => boolean; // DEPRECATED: do not use, it will go away. see PresBox.restoreTargetDocView diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index b33c267ee..752e8b217 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -1230,11 +1230,12 @@ export class CollectionFreeFormView extends CollectionSubView { - SmartDrawHandler.Instance.CreateDrawingDoc = this.createDrawingDoc; - SmartDrawHandler.Instance.RemoveDrawing = this.removeDrawing; - SmartDrawHandler.Instance.AddDrawing = this.addDrawing; - SmartDrawHandler.Instance.displaySmartDrawHandler(x, y, NumCast(this.layoutDoc[this.scaleFieldKey])); + showSmartDraw = (x: number, y: number, regenerate?: boolean) => { + const sm = SmartDrawHandler.Instance; + sm.CreateDrawingDoc = this.createDrawingDoc; + sm.RemoveDrawing = this.removeDrawing; + sm.AddDrawing = this.addDrawing; + (regenerate ? sm.displayRegenerate : sm.displaySmartDrawHandler)(x, y, NumCast(this.layoutDoc[this.scaleFieldKey])); }; _drawing: Doc[] = []; diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index 9395863d8..f3aea76a5 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -368,8 +368,13 @@ export class ImageBox extends ViewBoxAnnotatableComponent() { this.layoutDoc.ai && funcs.push({ description: 'Regenerate AI Image', - event: action(e => { - !SmartDrawHandler.Instance.ShowRegenerate ? SmartDrawHandler.Instance.displayRegenerate(e?.x || 0, e?.y || 0) : SmartDrawHandler.Instance.hideRegenerate(); + event: action(() => { + if (!SmartDrawHandler.Instance.ShowRegenerate && this.DocumentView) { + const [x, y] = this.DocumentView().screenToViewTransform().inverse().transformPoint(NumCast(this.Document.width), 0); + this._props.docViewPath().slice(-2)[0]?.ComponentView?.showSmartDraw?.(x, y, true); + } else { + SmartDrawHandler.Instance.hideRegenerate(); + } }), icon: 'pen-to-square', }); @@ -625,7 +630,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent() { this._regenerateLoading = false; }) ); - } else + } else { SmartDrawHandler.Instance.regenerate([this.Document], undefined, undefined, this._regenInput || StrCast(this.Document.title), true).then( action(newImgs => { const firstImg = newImgs[0]; @@ -643,6 +648,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent() { } }) ); + } })} />

diff --git a/src/client/views/pdf/GPTPopup/GPTPopup.tsx b/src/client/views/pdf/GPTPopup/GPTPopup.tsx index f4ab2f41c..96c7f37a3 100644 --- a/src/client/views/pdf/GPTPopup/GPTPopup.tsx +++ b/src/client/views/pdf/GPTPopup/GPTPopup.tsx @@ -660,7 +660,7 @@ export class GPTPopup extends ObservableReactComponent { //prettier-ignore switch (this._mode) { case GPTPopupMode.USER_PROMPT: return this.promptBox("ASK", this._userPrompt, this.setUserPrompt, 'Ask GPT to sort, tag, define, or filter your documents for you!'); - case GPTPopupMode.FIREFLY: return this.promptBox("CREATE", this._userPrompt, this.setUserPrompt, 'Ask Firefly to generate images'); + case GPTPopupMode.FIREFLY: return this.promptBox("CREATE", this._userPrompt, this.setUserPrompt, StrCast(DocumentView.Selected().lastElement()?.Document.ai_firefly_prompt, 'Ask Firefly to generate images')); case GPTPopupMode.QUIZ_RESPONSE: return this.promptBox("QUIZ", this._quizAnswer, this.setQuizAnswer, 'Describe/answer the selected document!'); case GPTPopupMode.GPT_MENU: return this.menuBox(); case GPTPopupMode.SUMMARY: return this.summaryBox(); diff --git a/src/client/views/smartdraw/SmartDrawHandler.tsx b/src/client/views/smartdraw/SmartDrawHandler.tsx index 532391ac6..811bc6e25 100644 --- a/src/client/views/smartdraw/SmartDrawHandler.tsx +++ b/src/client/views/smartdraw/SmartDrawHandler.tsx @@ -207,8 +207,6 @@ export class SmartDrawHandler extends ObservableReactComponent { if (this.ShowRegenerate) { await this.regenerate(this._selectedDocs).then( action(() => { - this._selectedDocs = []; - this._regenInput = ''; this._showEditBox = false; }) ); -- cgit v1.2.3-70-g09d2 From 215ad40efa2e343e290d18bffbc55884829f1a0d Mon Sep 17 00:00:00 2001 From: bobzel Date: Tue, 4 Mar 2025 00:52:53 -0500 Subject: fixed up smartDrawHandler a bit to support svg's better. you can now drop in a .svg file from the filesystem - still some unfinished business (arcs, background/foreground color inversion) --- src/client/documents/DocUtils.ts | 57 +++- src/client/util/bezierFit.ts | 302 +++++++++++++++++---- src/client/views/collections/CollectionSubView.tsx | 17 +- .../collectionFreeForm/CollectionFreeFormView.tsx | 36 +-- src/client/views/pdf/AnchorMenu.tsx | 2 +- src/client/views/smartdraw/SmartDrawHandler.tsx | 72 +++-- 6 files changed, 363 insertions(+), 123 deletions(-) (limited to 'src/client/documents/DocUtils.ts') diff --git a/src/client/documents/DocUtils.ts b/src/client/documents/DocUtils.ts index 18c8d97d4..7f22e9376 100644 --- a/src/client/documents/DocUtils.ts +++ b/src/client/documents/DocUtils.ts @@ -10,7 +10,7 @@ import { DateField } from '../../fields/DateField'; import { Doc, DocListCast, Field, FieldResult, FieldType, LinkedTo, Opt, StrListCast } from '../../fields/Doc'; import { DocData } from '../../fields/DocSymbols'; import { Id } from '../../fields/FieldSymbols'; -import { InkDataFieldName, InkField } from '../../fields/InkField'; +import { InkData, InkDataFieldName, InkField } from '../../fields/InkField'; import { List, ListFieldName } from '../../fields/List'; import { ProxyField } from '../../fields/Proxy'; import { RichTextField } from '../../fields/RichTextField'; @@ -33,6 +33,10 @@ import { TaskCompletionBox } from '../views/nodes/TaskCompletedBox'; import { DocumentType } from './DocumentTypes'; import { Docs, DocumentOptions } from './Documents'; import { DocumentView } from '../views/nodes/DocumentView'; +import { INode, parse } from 'svgson'; +import { SVGToBezier, SVGType } from '../util/bezierFit'; +import { SmartDrawHandler } from '../views/smartdraw/SmartDrawHandler'; +import { PointData } from '../../pen-gestures/GestureTypes'; export namespace DocUtils { function HasFunctionFilter(val: string) { @@ -780,6 +784,57 @@ export namespace DocUtils { return generatedDocuments; } + export async function openSVGfile(file: File, options: DocumentOptions) { + const reader = new FileReader(); + const scale = 1; + const startPoint = { X: (options.x as number) ?? 0, Y: (options.y as number) ?? 0 }; + const buffer = await new Promise((res, rej) => { + reader.onload = event => { + const fileContent = event.target?.result; + // Process the file content here + console.log(fileContent); + typeof fileContent === 'string' ? res(fileContent) : rej(); + }; + + reader.readAsText(file); + }); + const svg = buffer.match(/]*>([\s\S]*?)<\/svg>/g); + if (svg) { + const svgObject = await parse(svg[0]); + const strokeData: [InkData, string, string][] = []; + const tl = { X: Number.MAX_SAFE_INTEGER, Y: Number.MAX_SAFE_INTEGER }; + let last: PointData = { X: 0, Y: 0 }; + const processStroke = (child: INode) => { + child.attributes.d + .split(/[\n]?M/) + .slice(1) + .map((d, ind) => { + const convertedBezier: InkData = SVGToBezier(child.name as SVGType, { ...child, d: '\nM' + d } as unknown as Record, last); + last = convertedBezier.lastElement(); + convertedBezier.forEach(point => { + if (point.X < tl.X) tl.X = point.X; + if (point.Y < tl.Y) tl.Y = point.Y; + }); + strokeData.push([convertedBezier, child.attributes.stroke || 'black', ind === 0 ? child.attributes.fill : child.attributes.fill === 'none' ? child.attributes.fill : DashColor(child.attributes.fill).negate().toString()]); + }); + }; + const processNode = (parent: INode) => { + if (parent.children.length) parent.children.forEach(processNode); + else if (parent.type !== 'text') processStroke(parent); + }; + processNode(svgObject); + + const mapStroke = (pd: PointData): PointData => ({ X: startPoint.X + (pd.X - tl.X) * scale, Y: startPoint.Y + (pd.Y - tl.Y) * scale }); + + return SmartDrawHandler.CreateDrawingDoc( + strokeData.map(sdata => [sdata[0].map(mapStroke), sdata[1], sdata[2]] as [PointData[], string, string]), + { autoColor: true }, + '', + undefined + ); + } + } + export function uploadFileToDoc(file: File, options: DocumentOptions, overwriteDoc: Doc) { const generatedDocuments: Doc[] = []; // Since this file has an overwriteDoc, we can set the client tracking guid to the overwriteDoc's guid. diff --git a/src/client/util/bezierFit.ts b/src/client/util/bezierFit.ts index 7ef370d48..65bd44bf9 100644 --- a/src/client/util/bezierFit.ts +++ b/src/client/util/bezierFit.ts @@ -1,7 +1,5 @@ /* eslint-disable no-use-before-define */ -/* eslint-disable prefer-destructuring */ /* eslint-disable no-param-reassign */ -/* eslint-disable camelcase */ import { Point } from '../../pen-gestures/ndollar'; export enum SVGType { @@ -625,13 +623,130 @@ export function GenerateControlPoints(coordinates: Point[], alpha = 0.1) { return [...firstEnd, ...points, ...lastEnd]; } -export function SVGToBezier(name: SVGType, attributes: any): Point[] { +function convertToAbsolute(pathData: string): string { + const commands = pathData.match(/[a-zA-Z][^a-zA-Z]*/g); + if (!commands) return pathData; + + let currentX = 0; + let currentY = 0; + let startX = 0; + let startY = 0; + + const absoluteCommands = commands.map(command => { + const type = command[0]; + const values = command + .slice(1) + .trim() + .split(/[\s,]+/) + .map(v => +v); + + switch (type) { + case 'M': + currentX = values[0]; + currentY = values[1]; + startX = currentX; + startY = currentY; + return `M${currentX},${currentY}`; + case 'm': + currentX += values[0]; + currentY += values[1]; + startX = currentX; + startY = currentY; + return `M${currentX},${currentY}`; + case 'L': + currentX = values[0]; + currentY = values[1]; + return `L${currentX},${currentY}`; + case 'l': + currentX += values[0]; + currentY += values[1]; + return `L${currentX},${currentY}`; + case 'H': + currentX = values[0]; + return `H${currentX}`; + case 'h': + currentX += values[0]; + return `H${currentX}`; + case 'V': + currentY = values[0]; + return `V${currentY}`; + case 'v': + currentY += values[0]; + return `V${currentY}`; + case 'C': + currentX = values[4]; + currentY = values[5]; + return `C${values.join(',')}`; + case 'c': { + let str = ''; + for (let i = 0; i < values.length; i += 6) { + str += (i === 0 ? 'C':',') + (values[i] + currentX) + + ',' + (values[i + 1] + currentY) + + ',' + (values[i + 2] + currentX) + + ',' + (values[i + 3] + currentY) + + ',' + (values[i + 4] + currentX) + + ',' + (values[i + 5] + currentY); // prettier-ignore + currentX += values[i + 4]; + currentY += values[i + 5]; + } + return str; + } + case 'S': + currentX = values[2]; + currentY = values[3]; + return `S${values.join(',')}`; + case 's': + return `S${values.map((v, i) => (i % 2 === 0 ? (currentX += v) : (currentY += v))).join(',')}`; + case 'Q': + currentX = values[2]; + currentY = values[3]; + return `Q${values.join(',')}`; + case 'q': { + let str = ''; + for (let i = 0; i < values.length; i += 4) { + str += (i === 0 ? 'Q':',') + (values[i] + currentX) + + ',' + (values[i + 1] + currentY) + + ',' + (values[i + 2] + currentX) + + ',' + (values[i + 3] + currentY); // prettier-ignore + currentX += values[i + 2]; + currentY += values[i + 3]; + } + return str; + } + case 'T': + currentX = values[0]; + currentY = values[1]; + return `T${currentX},${currentY}`; + case 't': + currentX += values[0]; + currentY += values[1]; + return `T${currentX},${currentY}`; + case 'A': + currentX = values[5]; + currentY = values[6]; + return `A${values.join(',')}`; + case 'a': + return `A${values.map((v, i) => (i % 2 === 0 ? (currentX += v) : (currentY += v))).join(',')}`; + case 'Z': + case 'z': + currentX = startX; + currentY = startY; + return 'Z'; + default: + return command; + } + }); + + return absoluteCommands.join(' '); +} + +export function SVGToBezier(name: SVGType, attributes: Record, last: { X: number; Y: number }): Point[] { switch (name) { case 'line': { - const x1 = parseInt(attributes.x1); - const x2 = parseInt(attributes.x2); - const y1 = parseInt(attributes.y1); - const y2 = parseInt(attributes.y2); + const x1 = +attributes.x1; + const x2 = +attributes.x2; + const y1 = +attributes.y1; + const y2 = +attributes.y2; return [ { X: x1, Y: y1 }, { X: x1, Y: y1 }, @@ -642,10 +757,10 @@ export function SVGToBezier(name: SVGType, attributes: any): Point[] { case 'circle': case 'ellipse': { const c = 0.551915024494; - const centerX = parseInt(attributes.cx); - const centerY = parseInt(attributes.cy); - const radiusX = parseInt(attributes.rx) || parseInt(attributes.r); - const radiusY = parseInt(attributes.ry) || parseInt(attributes.r); + const centerX = +attributes.cx; + const centerY = +attributes.cy; + const radiusX = +attributes.rx || +attributes.r; + const radiusY = +attributes.ry || +attributes.r; return [ { X: centerX, Y: centerY + radiusY }, { X: centerX + c * radiusX, Y: centerY + radiusY }, @@ -666,10 +781,10 @@ export function SVGToBezier(name: SVGType, attributes: any): Point[] { ]; } case 'rect': { - const x = parseInt(attributes.x); - const y = parseInt(attributes.y); - const width = parseInt(attributes.width); - const height = parseInt(attributes.height); + const x = +attributes.x; + const y = +attributes.y; + const width = +attributes.width; + const height = +attributes.height; return [ { X: x, Y: y }, { X: x, Y: y }, @@ -691,41 +806,122 @@ export function SVGToBezier(name: SVGType, attributes: any): Point[] { } case 'path': { const coordList: Point[] = []; - const [startX, startY] = attributes.d.match(/M(-?\d+\.?\d*),(-?\d+\.?\d*)/).slice(1); - const startPt = { X: parseInt(startX), Y: parseInt(startY) }; - coordList.push(startPt); - const matches: RegExpMatchArray[] = Array.from( - attributes.d.matchAll(/Q(-?\d+\.?\d*),(-?\d+\.?\d*) (-?\d+\.?\d*),(-?\d+\.?\d*)|C(-?\d+\.?\d*),(-?\d+\.?\d*) (-?\d+\.?\d*),(-?\d+\.?\d*) (-?\d+\.?\d*),(-?\d+\.?\d*)|L(-?\d+\.?\d*),(-?\d+\.?\d*)/g) - ); - let lastPt: Point = startPt; - matches.forEach(match => { - if (match[0].startsWith('Q')) { - coordList.push({ X: parseInt(match[1]), Y: parseInt(match[2]) }); - coordList.push({ X: parseInt(match[1]), Y: parseInt(match[2]) }); - coordList.push({ X: parseInt(match[3]), Y: parseInt(match[4]) }); - coordList.push({ X: parseInt(match[3]), Y: parseInt(match[4]) }); - lastPt = { X: parseInt(match[3]), Y: parseInt(match[4]) }; - } else if (match[0].startsWith('C')) { - coordList.push({ X: parseInt(match[5]), Y: parseInt(match[6]) }); - coordList.push({ X: parseInt(match[7]), Y: parseInt(match[8]) }); - coordList.push({ X: parseInt(match[9]), Y: parseInt(match[10]) }); - coordList.push({ X: parseInt(match[9]), Y: parseInt(match[10]) }); - lastPt = { X: parseInt(match[9]), Y: parseInt(match[10]) }; - } else { - coordList.push(lastPt); - coordList.push({ X: parseInt(match[11]), Y: parseInt(match[12]) }); - coordList.push({ X: parseInt(match[11]), Y: parseInt(match[12]) }); - coordList.push({ X: parseInt(match[11]), Y: parseInt(match[12]) }); - lastPt = { X: parseInt(match[11]), Y: parseInt(match[12]) }; + let fixedattrs = attributes.d.trim().replace(/([0-9])-/g, '$1,-'); + for (let i = 0; i < 100; i++) { + const test = fixedattrs.replace(/([0-9]?\.[0-9]+)(\.[0-9]+)/g, '$1,$2'); + if (test === fixedattrs) break; + fixedattrs = test; + } + const attrdata = convertToAbsolute(fixedattrs); + const move = attrdata.match(/M(-?\d*\.?\d*)[, ]?(-?\d*\.?\d*)/); + const [startX, startY] = move?.slice(1) ?? [last.X + '', last.Y + '']; + const startPt = { X: +startX, Y: +startY }; + let first = true; + let lastCmd = ''; + for (let attr = attrdata.slice(move?.[0].length ?? 0).trim(); attr; ) { + lastCmd = 'AQCLVHZ'.includes(attr[0]) ? attr[0] : lastCmd; + switch (lastCmd) { + case 'Q': { + const match = attr.match(/Q?[, ]?(-?\d*\.?\d*)[, ]?(-?\d*\.?\d*)[, ]?(-?\d*\.?\d*)[, ]?(-?\d*\.?\d*)/); + if (match) { + const prev = first ? startPt : coordList.lastElement(); + const Q = [+match[1], +match[2], +match[3], +match[4]]; + + coordList.push(prev); + coordList.push({ X: prev.X + (2 / 3) * (Q[0] - prev.X), Y: prev.Y + (2 / 3) * (Q[1] - prev.Y) }); + coordList.push({ X: Q[2] + (2 / 3) * (Q[0] - Q[2]), Y: Q[3] + (2 / 3) * (Q[1] - Q[3]) }); + coordList.push({ X: Q[2], Y: Q[3] }); + attr = attr.slice(match[0].length).trim(); + } else { + attr = attr.slice(1).trim(); + alert('error' + attr); + } + break; + } + case 'C': { + const match = attr.match(/C?[, ]?(-?\d*\.?\d*)[, ]?(-?\d*\.?\d*)[, ]?(-?\d*\.?\d*)[, ]?(-?\d*\.?\d*)[, ]?(-?\d*\.?\d*)[, ]?(-?\d*\.?\d*)/); + if (match) { + coordList.push(first ? startPt : coordList.lastElement()); + coordList.push({ X: +match[1], Y: +match[2] }); + coordList.push({ X: +match[3], Y: +match[4] }); + coordList.push({ X: +match[5], Y: +match[6] }); + attr = attr.slice(match[0].length).trim(); + } else { + attr = attr.slice(1).trim(); + alert('error' + attr); + } + break; + } + case 'A': { + const match = attr.match(/A?[, ]?(-?\d*\.?\d*)[, ]?(-?\d*\.?\d*)[, ]?(-?\d*\.?\d*)[, ]?(-?\d*\.?\d*)[, ]?(-?\d*\.?\d*)[, ]?(-?\d*\.?\d*)[, ]?(-?\d*\.?\d*)/); + if (match) { + console.log('SKIPPING arc - not implemented'); + // coordList.push(first ? startPt : coordList.lastElement()); + // coordList.push({ X: +match[1], Y: +match[2] }); + // coordList.push({ X: +match[3], Y: +match[4] }); + // coordList.push({ X: +match[5], Y: +match[6] }); + attr = attr.slice(match[0].length).trim(); + } else { + attr = attr.slice(1).trim(); + alert('error' + attr); + } + break; + } + case 'L': { + const match = attr.match(/L?[, ]?(-?\d*\.?\d*)[, ]?(-?\d*\.?\d*)/); + if (match) { + coordList.push(first ? startPt : coordList.lastElement()); + coordList.push(coordList.lastElement()); + coordList.push({ X: +match[1], Y: +match[2] }); + coordList.push({ X: +match[1], Y: +match[2] }); + attr = attr.slice(match[0].length).trim(); + } else { + attr = attr.slice(1).trim(); + alert('error' + attr); + } + break; + } + case 'H': { + const match = attr.match(/H?[, ]?(-?\d*\.?\d*)/); + if (match) { + coordList.push(first ? startPt : coordList.lastElement()); + coordList.push(coordList.lastElement()); + coordList.push({ X: +match[1], Y: coordList.lastElement().Y }); + coordList.push({ X: +match[1], Y: coordList.lastElement().Y }); + attr = attr.slice(match[0].length).trim(); + } else { + attr = attr.slice(1).trim(); + alert('error' + attr); + } + break; + } + case 'V': { + const match = attr.match(/V?[, ]?(-?\d*\.?\d*)/); + if (match) { + coordList.push(first ? startPt : coordList.lastElement()); + coordList.push(coordList.lastElement()); + coordList.push({ X: coordList.lastElement().X, Y: +match[1] }); + coordList.push({ X: coordList.lastElement().X, Y: +match[1] }); + attr = attr.slice(match[0].length).trim(); + } else { + attr = attr.slice(1).trim(); + alert('error' + attr); + } + break; + } + case 'Z': { + coordList.push(first ? startPt : coordList.lastElement()); + coordList.push(first ? startPt : coordList.lastElement()); + coordList.push(startPt); + coordList.push(startPt); + attr = attr.slice(1).trim(); + break; + } + default: + attr = attr.slice(1).trim(); + debugger; } - }); - const hasZ = attributes.d.match(/Z/); - if (hasZ || attributes.fill) { - coordList.push(lastPt); - coordList.push(startPt); - coordList.push(startPt); - } else { - coordList.pop(); + first = false; } return coordList; } @@ -733,10 +929,10 @@ export function SVGToBezier(name: SVGType, attributes: any): Point[] { const coords: RegExpMatchArray[] = Array.from(attributes.points.matchAll(/(-?\d+\.?\d*),(-?\d+\.?\d*)/g)); let list: Point[] = []; coords.forEach(coord => { - list.push({ X: parseInt(coord[1]), Y: parseInt(coord[2]) }); - list.push({ X: parseInt(coord[1]), Y: parseInt(coord[2]) }); - list.push({ X: parseInt(coord[1]), Y: parseInt(coord[2]) }); - list.push({ X: parseInt(coord[1]), Y: parseInt(coord[2]) }); + list.push({ X: +coord[1], Y: +coord[2] }); + list.push({ X: +coord[1], Y: +coord[2] }); + list.push({ X: +coord[1], Y: +coord[2] }); + list.push({ X: +coord[1], Y: +coord[2] }); }); const firstPts = list.splice(0, 2); list = list.concat(firstPts); diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index ca830aa6f..655894e40 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -541,12 +541,17 @@ export function CollectionSubView() { DocUtils.uploadYoutubeVideoLoading(files, {}, loading); } else { generatedDocuments.push( - ...files.map(file => { - const loading = Docs.Create.LoadingDocument(file, options); - Doc.addCurrentlyLoading(loading); - DocUtils.uploadFileToDoc(file, {}, loading); - return loading; - }) + ...(await Promise.all( + files.map(async file => { + if (file.name.endsWith('svg')) { + return (await DocUtils.openSVGfile(file, options)) as Doc; + } + const loading = Docs.Create.LoadingDocument(file, options); + Doc.addCurrentlyLoading(loading); + DocUtils.uploadFileToDoc(file, {}, loading); + return loading; + }) + )) ); } if (generatedDocuments.length) { diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index aa9b9b0fa..b3d908da4 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -30,7 +30,7 @@ import { CompileScript } from '../../../util/Scripting'; import { ScriptingGlobals } from '../../../util/ScriptingGlobals'; import { freeformScrollMode, SnappingManager } from '../../../util/SnappingManager'; import { Transform } from '../../../util/Transform'; -import { undoable, undoBatch, UndoManager } from '../../../util/UndoManager'; +import { undoable, UndoManager } from '../../../util/UndoManager'; import { Timeline } from '../../animationtimeline/Timeline'; import { ContextMenu } from '../../ContextMenu'; import { InkingStroke } from '../../InkingStroke'; @@ -1232,7 +1232,6 @@ export class CollectionFreeFormView extends CollectionSubView { const sm = SmartDrawHandler.Instance; - sm.CreateDrawingDoc = this.createDrawingDoc; sm.RemoveDrawing = this.removeDrawing; sm.AddDrawing = this.addDrawing; (regenerate ? sm.displayRegenerate : sm.displaySmartDrawHandler)(x, y, NumCast(this.layoutDoc[this.scaleFieldKey])); @@ -1240,38 +1239,6 @@ export class CollectionFreeFormView extends CollectionSubView { - this._drawing = []; - const xf = this.screenToFreeformContentsXf; - strokeData.forEach((stroke: [InkData, string, string]) => { - const bounds = InkField.getBounds(stroke[0]); - const B = xf.transformBounds(bounds.left, bounds.top, bounds.width, bounds.height); - const inkWidth = ActiveInkWidth() * this.ScreenToLocalBoxXf().Scale; - const inkDoc = Docs.Create.InkDocument( - stroke[0], - { title: 'stroke', - x: B.x - inkWidth / 2, - y: B.y - inkWidth / 2, - _width: B.width + inkWidth, - _height: B.height + inkWidth, - stroke_showLabel: BoolCast(Doc.UserDoc().activeHideTextLabels)}, // prettier-ignore - inkWidth, - opts.autoColor ? stroke[1] : ActiveInkColor(), - ActiveInkBezierApprox(), - stroke[2] === 'none' ? ActiveInkFillColor() : stroke[2], - ActiveInkArrowStart(), - ActiveInkArrowEnd(), - ActiveInkDash(), - ActiveIsInkMask() - ); - this._drawing.push(inkDoc); - }); - return MarqueeView.getCollection(this._drawing, undefined, true, { left: opts.x, top: opts.y, width: 1, height: 1 }); - }; /** * Part of regenerating a drawing--deletes the old drawing. @@ -2007,7 +1974,6 @@ export class CollectionFreeFormView extends CollectionSubView { - SmartDrawHandler.Instance.CreateDrawingDoc = this.createDrawingDoc; SmartDrawHandler.Instance.AddDrawing = this.addDrawing; SmartDrawHandler.Instance.RemoveDrawing = this.removeDrawing; !SmartDrawHandler.Instance.ShowRegenerate ? SmartDrawHandler.Instance.displayRegenerate(this._downX, this._downY - 10) : SmartDrawHandler.Instance.hideRegenerate(); diff --git a/src/client/views/pdf/AnchorMenu.tsx b/src/client/views/pdf/AnchorMenu.tsx index f7070c780..28371594e 100644 --- a/src/client/views/pdf/AnchorMenu.tsx +++ b/src/client/views/pdf/AnchorMenu.tsx @@ -139,7 +139,7 @@ export class AnchorMenu extends AntimodeMenu { createDrawingAnnotation = action((drawing: Doc, opts: DrawingOptions, gptRes: string) => { this.AddDrawingAnnotation(drawing); const docData = drawing[DocData]; - docData.title = opts.text.match(/^(.*?)~~~.*$/)?.[1] || opts.text; + docData.title = opts.text?.match(/^(.*?)~~~.*$/)?.[1] || opts.text; docData.ai_drawing_input = opts.text; docData.ai_drawing_complexity = opts.complexity; docData.ai_drawing_colored = opts.autoColor; diff --git a/src/client/views/smartdraw/SmartDrawHandler.tsx b/src/client/views/smartdraw/SmartDrawHandler.tsx index fbf471900..ca308015d 100644 --- a/src/client/views/smartdraw/SmartDrawHandler.tsx +++ b/src/client/views/smartdraw/SmartDrawHandler.tsx @@ -27,16 +27,19 @@ import { ActiveInkArrowEnd, ActiveInkArrowStart, ActiveInkBezierApprox, ActiveIn import { FireflyDimensionsMap, FireflyImageData, FireflyImageDimensions } from './FireflyConstants'; import './SmartDrawHandler.scss'; import { Upload } from '../../../server/SharedMediaTypes'; +import { PointData } from '../../../pen-gestures/GestureTypes'; export interface DrawingOptions { - text: string; - complexity: number; - size: number; - autoColor: boolean; - x: number; - y: number; + text?: string; + complexity?: number; + size?: number; + autoColor?: boolean; + x?: number; + y?: number; } +type svgparsedData = [PointData[], string, string]; + /** * The SmartDrawHandler allows users to generate drawings with GPT from text input. Users are able to enter * the item to draw, how complex they want the drawing to be, how large the drawing should be, and whether @@ -102,7 +105,7 @@ export class SmartDrawHandler extends ObservableReactComponent { * classes to customize the way the drawing docs get created. For example, the freeform canvas has a different way of * defining document bounds, so CreateDrawingDoc is redefined when that class calls gpt draw functions. */ - public CreateDrawingDoc: (strokeList: [InkData, string, string][], opts: DrawingOptions, gptRes: string, containerDoc?: Doc) => Doc | undefined = (strokeList: [InkData, string, string][], opts: DrawingOptions) => { + public static CreateDrawingDoc: (strokeList: [InkData, string, string][], opts: DrawingOptions, gptRes: string, containerDoc?: Doc) => Doc | undefined = (strokeList: [InkData, string, string][], opts: DrawingOptions) => { const drawing: Doc[] = []; strokeList.forEach((stroke: [InkData, string, string]) => { const bounds = InkField.getBounds(stroke[0]); @@ -114,7 +117,7 @@ export class SmartDrawHandler extends ObservableReactComponent { y: bounds.top - inkWidth / 2, _width: bounds.width + inkWidth, _height: bounds.height + inkWidth, - stroke_showLabel: !BoolCast(Doc.UserDoc().activeHideTextLabels)}, // prettier-ignore + stroke_showLabel: false}, // prettier-ignore inkWidth, opts.autoColor ? stroke[1] : ActiveInkColor(), ActiveInkBezierApprox(), @@ -188,9 +191,9 @@ export class SmartDrawHandler extends ObservableReactComponent { /** * This allows users to press the return/enter key to send input. */ - handleKeyPress = (event: React.KeyboardEvent) => { - if (event.key === 'Enter') { - this.handleSendClick(); + handleKeyPress = (e: React.KeyboardEvent) => { + if (e.key === 'Enter') { + this.handleSendClick(this._pageX, this._pageY); } }; @@ -200,7 +203,7 @@ export class SmartDrawHandler extends ObservableReactComponent { * what the user sees. */ @action - handleSendClick = async () => { + handleSendClick = async (X: number, Y: number) => { if ((!this.ShowRegenerate && this._userInput == '') || (!this._generateImage && !this._generateDrawing)) return; this._isLoading = true; this._canInteract = false; @@ -213,7 +216,7 @@ export class SmartDrawHandler extends ObservableReactComponent { await this.createImageWithFirefly(this._userInput); } if (this._generateDrawing) { - await this.drawWithGPT({ X: this._pageX, Y: this._pageY }, this._userInput, this._complexity, this._size, this._autoColor); + await this.drawWithGPT({ X, Y }, this._userInput, this._complexity, this._size, this._autoColor); } this.hideSmartDrawHandler(); } catch (err) { @@ -229,14 +232,14 @@ export class SmartDrawHandler extends ObservableReactComponent { /** * Calls GPT API to create a drawing based on user input. */ - drawWithGPT = async (startPt: { X: number; Y: number }, input: string, complexity: number, size: number, autoColor: boolean) => { + drawWithGPT = async (screenPt: { X: number; Y: number }, input: string, complexity: number, size: number, autoColor: boolean) => { if (input) { - this._lastInput = { text: input, complexity: complexity, size: size, autoColor: autoColor, x: startPt.X, y: startPt.Y }; + this._lastInput = { text: input, complexity: complexity, size: size, autoColor: autoColor, x: screenPt.X, y: screenPt.Y }; const res = await gptAPICall(`"${input}", "${complexity}", "${size}"`, GPTCallType.DRAW, undefined, true); if (res) { - const strokeData = await this.parseSvg(res, startPt, false, autoColor); - const drawingDoc = strokeData && this.CreateDrawingDoc(strokeData.data, strokeData.lastInput, strokeData.lastRes); - drawingDoc && this.AddDrawing(drawingDoc, this._lastInput, res); + const strokeData = await this.parseSvg(res, { X: 0, Y: 0 }, false, autoColor); + const drawingDoc = strokeData && SmartDrawHandler.CreateDrawingDoc(strokeData.data, strokeData.lastInput, strokeData.lastRes); + drawingDoc && this.AddDrawing(drawingDoc, this._lastInput, res, screenPt.X, screenPt.Y); drawingDoc && this._selectedDocs.push(drawingDoc); return strokeData; } else { @@ -334,9 +337,9 @@ export class SmartDrawHandler extends ObservableReactComponent { return gptAPICall(`"${this._lastInput.text}", "${this._lastInput.complexity}", "${this._lastInput.size}"`, GPTCallType.DRAW, undefined, true); })(); if (res) { - const strokeData = await this.parseSvg(res, { X: this._lastInput.x, Y: this._lastInput.y }, true, lastInput?.autoColor || this._autoColor); + const strokeData = await this.parseSvg(res, { X: this._lastInput.x ?? 0, Y: this._lastInput.y ?? 0 }, true, lastInput?.autoColor || this._autoColor); this.RemoveDrawing !== unimplementedFunction && this.RemoveDrawing(true, doc); - const drawingDoc = strokeData && this.CreateDrawingDoc(strokeData.data, strokeData.lastInput, strokeData.lastRes); + const drawingDoc = strokeData && SmartDrawHandler.CreateDrawingDoc(strokeData.data, strokeData.lastInput, strokeData.lastRes); drawingDoc && this.AddDrawing(drawingDoc, this._lastInput, res); } else { console.error('GPT call failed'); @@ -356,20 +359,35 @@ export class SmartDrawHandler extends ObservableReactComponent { */ parseSvg = async (res: string, startPoint: { X: number; Y: number }, regenerate: boolean, autoColor: boolean) => { const svg = res.match(/]*>([\s\S]*?)<\/svg>/g); + if (svg) { this._lastResponse = svg[0]; const svgObject = await parse(svg[0]); + console.log(res, svgObject); const svgStrokes: INode[] = svgObject.children; const strokeData: [InkData, string, string][] = []; + + const tl = { X: Number.MAX_SAFE_INTEGER, Y: Number.MAX_SAFE_INTEGER }; + let last: PointData = { X: 0, Y: 0 }; svgStrokes.forEach(child => { - const convertedBezier: InkData = SVGToBezier(child.name as SVGType, child.attributes); + const convertedBezier: InkData = SVGToBezier(child.name as SVGType, child.attributes, last); + last = convertedBezier.lastElement(); strokeData.push([ - convertedBezier.map(point => ({ X: startPoint.X + (point.X - startPoint.X) * this._scale, Y: startPoint.Y + (point.Y - startPoint.Y) * this._scale })), + convertedBezier.map(point => { + if (point.X < tl.X) tl.X = point.X; + if (point.Y < tl.Y) tl.Y = point.Y; + return { X: point.X, Y: point.Y }; + }), (regenerate ? this._lastInput.autoColor : autoColor) ? child.attributes.stroke : '', (regenerate ? this._lastInput.autoColor : autoColor) ? child.attributes.fill : '', ]); }); - return { data: strokeData, lastInput: this._lastInput, lastRes: svg[0] }; + const mapStroke = (pd: PointData): PointData => ({ X: startPoint.X + (pd.X - tl.X) * this._scale, Y: startPoint.Y + (pd.Y - tl.Y) * this._scale }); + return { + data: strokeData.map(sdata => [sdata[0].map(mapStroke), sdata[1], sdata[2]] as svgparsedData), + lastInput: this._lastInput, + lastRes: svg[0], + }; } }; @@ -439,7 +457,7 @@ export class SmartDrawHandler extends ObservableReactComponent { }, }} checked={this._generateImage} - onChange={() => this._canInteract && (this._generateImage = !this._generateImage)} + onChange={action(() => this._canInteract && (this._generateImage = !this._generateImage))} /> @@ -566,7 +584,7 @@ export class SmartDrawHandler extends ObservableReactComponent { icon={this._isLoading ? : } iconPlacement="right" color={SettingsManager.userColor} - onClick={this.handleSendClick} + onClick={() => this.handleSendClick(this._pageX, this._pageY)} /> {this._showOptions && ( @@ -598,7 +616,7 @@ export class SmartDrawHandler extends ObservableReactComponent { icon={this._isLoading && this._regenInput !== '' ? : } iconPlacement="right" color={SettingsManager.userColor} - onClick={this.handleSendClick} + onClick={() => this.handleSendClick(this._pageX, this._pageY)} /> ); @@ -644,7 +662,7 @@ export class SmartDrawHandler extends ObservableReactComponent { tooltip="Regenerate" icon={this._isLoading && this._regenInput === '' ? : } color={SettingsManager.userColor} - onClick={this.handleSendClick} + onClick={() => this.handleSendClick(this._pageX, this._pageY)} /> } color={SettingsManager.userColor} onClick={action(() => (this._showEditBox = !this._showEditBox))} /> {this._showEditBox ? this.renderRegenerateEditBox() : null} -- cgit v1.2.3-70-g09d2 From 980d7d9f747b1ee0ca0ad86df18f98684de5b193 Mon Sep 17 00:00:00 2001 From: aaravkumar Date: Thu, 6 Mar 2025 13:42:53 -0500 Subject: daily journal node --- package-lock.json | 19 +++- src/client/documents/DocUtils.ts | 13 +++ src/client/documents/DocumentTypes.ts | 2 + src/client/documents/Documents.ts | 52 ++++++++++ src/client/views/Main.tsx | 4 +- .../views/nodes/formattedText/DailyJournal.tsx | 108 +++++++++++++++++++++ src/server/ApiManagers/AssistantManager.ts | 3 +- 7 files changed, 195 insertions(+), 6 deletions(-) create mode 100644 src/client/views/nodes/formattedText/DailyJournal.tsx (limited to 'src/client/documents/DocUtils.ts') diff --git a/package-lock.json b/package-lock.json index 3bbc68a99..e7e666911 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31233,6 +31233,15 @@ "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", "license": "MIT" }, + "node_modules/openai/node_modules/web-streams-polyfill": { + "version": "4.0.0-beta.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz", + "integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/opentype.js": { "version": "1.3.4", "resolved": "https://registry.npmjs.org/opentype.js/-/opentype.js-1.3.4.tgz", @@ -39095,12 +39104,14 @@ } }, "node_modules/web-streams-polyfill": { - "version": "4.0.0-beta.3", - "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz", - "integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.1.0.tgz", + "integrity": "sha512-A7Jxrg7+eV+eZR/CIdESDnRGFb6/bcKukGvJBB5snI6cw3is1c2qamkYstC1bY1p08TyMRlN9eTMkxmnKJBPBw==", "license": "MIT", + "optional": true, + "peer": true, "engines": { - "node": ">= 14" + "node": ">= 8" } }, "node_modules/webidl-conversions": { diff --git a/src/client/documents/DocUtils.ts b/src/client/documents/DocUtils.ts index 23032b62e..5a39a9720 100644 --- a/src/client/documents/DocUtils.ts +++ b/src/client/documents/DocUtils.ts @@ -396,6 +396,19 @@ export namespace DocUtils { event: e => (DocumentView.Selected().lastElement().ComponentView as CollectionFreeFormView)?.showSmartDraw(e?.x || 0, e?.y || 0), icon: 'file', }); + + // AARAV ADD // + documentList.push({ + description: ':Daily Journal', + event: undoable(() => { + const newDoc = Docs.Create.DailyJournalDocument('', { x, y }); + docAdder?.(newDoc); + }, 'Create Daily Journal'), + icon: 'book', + }); + + // AARAV ADD // + ContextMenu.Instance.addItem({ description: 'Create document', subitems: documentList, diff --git a/src/client/documents/DocumentTypes.ts b/src/client/documents/DocumentTypes.ts index 8aa844c0b..65cd499c9 100644 --- a/src/client/documents/DocumentTypes.ts +++ b/src/client/documents/DocumentTypes.ts @@ -44,6 +44,8 @@ export enum DocumentType { SCRIPTDB = 'scriptdb', // database of scripts GROUPDB = 'groupdb', // database of groups + + JOURNAL = 'journal', // AARAV ADD } export enum CollectionViewType { Invalid = 'invalid', diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index 891223952..ce8c8a6b7 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -19,6 +19,7 @@ import { DocServer } from '../DocServer'; import { dropActionType } from '../util/DropActionTypes'; import { CollectionViewType, DocumentType } from './DocumentTypes'; import { Id } from '../../fields/FieldSymbols'; +import { DailyJournal } from '../views/nodes/formattedText/DailyJournal'; class EmptyBox { public static LayoutString() { @@ -569,6 +570,19 @@ export namespace Docs { options: { acl: '' }, }, ], + + // AARAV ADD // + [ + DocumentType.JOURNAL, + { + layout: { view: EmptyBox, dataField: 'text' }, + options: { + title: 'Daily Journal', + acl_Guest: SharingPermissions.View, + }, + }, + ], + // AARAV ADD // ]); const suffix = 'Proto'; @@ -651,6 +665,7 @@ export namespace Docs { return undefined; } const { layout } = template; + // create title const upper = suffix.toUpperCase(); const title = prototypeId.toUpperCase().replace(upper, `_${upper}`); @@ -899,6 +914,43 @@ export namespace Docs { return InstanceFromProto(Prototypes.get(DocumentType.RTF), field, options, undefined, fieldKey); } + // AARAV ADD // + + export function DailyJournalDocument(text: string | RichTextField, options: DocumentOptions = {}, fieldKey: string = 'text') { + const rtf = { + doc: { + type: 'doc', + content: [ + { + type: 'paragraph', + content: [ + { + type: 'text', + text, + }, + ], + }, + ], + }, + selection: { type: 'text', anchor: 1, head: 1 }, + storedMarks: [], + Text: text, + }; + const field = text instanceof RichTextField ? text : text ? new RichTextField(JSON.stringify(rtf), text) : options.text instanceof RichTextField ? options.text : undefined; + + return InstanceFromProto(Prototypes.get(DocumentType.JOURNAL), field, { + title: new Date().toLocaleDateString(undefined, { + weekday: 'long', + year: 'numeric', + month: 'long', + day: 'numeric', + }), + ...options, + }); + } + + // AARAV ADD // + export function LinkDocument(source: Doc, target: Doc, options: DocumentOptions = {}, id?: string) { const linkDoc = InstanceFromProto( Prototypes.get(DocumentType.LINK), diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx index dda543470..8087a0e6d 100644 --- a/src/client/views/Main.tsx +++ b/src/client/views/Main.tsx @@ -29,7 +29,6 @@ import { CollectionSchemaView } from './collections/collectionSchema/CollectionS import { SchemaRowBox } from './collections/collectionSchema/SchemaRowBox'; import './global/globalScripts'; import { AudioBox } from './nodes/AudioBox'; -import { ChatBox } from './nodes/chatbot/chatboxcomponents/ChatBox'; import { ComparisonBox } from './nodes/ComparisonBox'; import { DataVizBox } from './nodes/DataVizBox/DataVizBox'; import { DiagramBox } from './nodes/DiagramBox'; @@ -53,6 +52,8 @@ import { ScriptingBox } from './nodes/ScriptingBox'; import { VideoBox } from './nodes/VideoBox'; import { WebBox } from './nodes/WebBox'; import { CalendarBox } from './nodes/calendarBox/CalendarBox'; +import { ChatBox } from './nodes/chatbot/chatboxcomponents/ChatBox'; +import { DailyJournal } from './nodes/formattedText/DailyJournal'; import { DashDocCommentView } from './nodes/formattedText/DashDocCommentView'; import { DashDocView } from './nodes/formattedText/DashDocView'; import { DashFieldView } from './nodes/formattedText/DashFieldView'; @@ -119,6 +120,7 @@ FieldLoader.ServerLoadStatus = { requested: 0, retrieved: 0, message: 'cache' }; DocumentContentsView.Init(KeyValueBox.LayoutString(), { StickerPalette: StickerPalette, FormattedTextBox, + DailyJournal, // AARAV ImageBox, FontIconBox, LabelBox, diff --git a/src/client/views/nodes/formattedText/DailyJournal.tsx b/src/client/views/nodes/formattedText/DailyJournal.tsx new file mode 100644 index 000000000..9decbfaf0 --- /dev/null +++ b/src/client/views/nodes/formattedText/DailyJournal.tsx @@ -0,0 +1,108 @@ +import { action, makeObservable, observable } from 'mobx'; +import * as React from 'react'; +import { RichTextField } from '../../../../fields/RichTextField'; +import { Docs } from '../../../documents/Documents'; +import { DocumentType } from '../../../documents/DocumentTypes'; +import { ViewBoxAnnotatableComponent } from '../../DocComponent'; +import { FieldView, FieldViewProps } from '../FieldView'; +import { FormattedTextBox, FormattedTextBoxProps } from './FormattedTextBox'; + +export class DailyJournal extends ViewBoxAnnotatableComponent() { + @observable journalDate: string; + + public static LayoutString(fieldStr: string) { + return FieldView.LayoutString(DailyJournal, fieldStr); + } + + constructor(props: FormattedTextBoxProps) { + super(props); + makeObservable(this); + this.journalDate = this.getFormattedDate(); + + console.log('Constructor: Setting initial title and text...'); + this.setDailyTitle(); + this.setDailyText(); + } + + getFormattedDate(): string { + const date = new Date().toLocaleDateString(undefined, { + weekday: 'long', + year: 'numeric', + month: 'long', + day: 'numeric', + }); + console.log('📆 getFormattedDate():', date); + return date; + } + + @action + setDailyTitle() { + console.log('setDailyTitle() called...'); + console.log('Current title before update:', this.dataDoc.title); + + if (!this.dataDoc.title || this.dataDoc.title !== this.journalDate) { + console.log('Updating title to:', this.journalDate); + this.dataDoc.title = this.journalDate; + } + + console.log('New title after update:', this.dataDoc.title); + } + + @action + setDailyText() { + console.log('setDailyText() called...'); + const initialText = `Journal Entry - ${this.journalDate}\n\nStart writing here...`; + + console.log('Checking if dataDoc has text field...'); + + console.log('Setting new text field with:', initialText); + this.dataDoc[this.fieldKey] = new RichTextField( + JSON.stringify({ + doc: { + type: 'doc', + content: [{ type: 'paragraph', content: [{ type: 'text', text: initialText }] }], + }, + selection: { type: 'text', anchor: 1, head: 1 }, + storedMarks: [], + }), + initialText + ); + + console.log('Current text field:', this.dataDoc[this.fieldKey]); + } + + componentDidMount(): void { + console.log('componentDidMount() triggered...'); + this.setDailyTitle(); + this.setDailyText(); + } + + componentDidUpdate(prevProps: Readonly): void { + console.log('componentDidUpdate() triggered...'); + super.componentDidUpdate(prevProps); + this.setDailyTitle(); + this.setDailyText(); + } + + render() { + return
+ + +
; + } +} + +Docs.Prototypes.TemplateMap.set(DocumentType.JOURNAL, { + layout: { view: DailyJournal, dataField: 'text' }, + options: { + acl: '', + _height: 35, + _xMargin: 10, + _yMargin: 10, + _layout_nativeDimEditable: true, + _layout_reflowVertical: true, + _layout_reflowHorizontal: true, + defaultDoubleClick: 'ignore', + systemIcon: 'BsFileEarmarkTextFill', + }, +}); diff --git a/src/server/ApiManagers/AssistantManager.ts b/src/server/ApiManagers/AssistantManager.ts index c41f697db..e859e5c5f 100644 --- a/src/server/ApiManagers/AssistantManager.ts +++ b/src/server/ApiManagers/AssistantManager.ts @@ -25,6 +25,7 @@ import { DashUploadUtils } from '../DashUploadUtils'; import { Method } from '../RouteManager'; import { filesDirectory, publicDirectory } from '../SocketData'; import ApiManager, { Registration } from './ApiManager'; +import { env } from 'process'; // Enumeration of directories where different file types are stored export enum Directory { @@ -89,7 +90,7 @@ export default class AssistantManager extends ApiManager { protected initialize(register: Registration): void { // Initialize Google Custom Search API const customsearch = google.customsearch('v1'); - const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY }); + const openai = new OpenAI({ apiKey: env.OPENAI_API_KEY }); // Register Wikipedia summary API route register({ -- cgit v1.2.3-70-g09d2 From 1ab6f6c87b746e0c2898694216db50d5faadf7f5 Mon Sep 17 00:00:00 2001 From: bobzel Date: Thu, 6 Mar 2025 19:46:43 -0500 Subject: a bunch of changes to improve how docs are selected automatically when created. --- src/client/documents/DocUtils.ts | 5 +- src/client/views/GlobalKeyHandler.ts | 19 +++-- src/client/views/MarqueeAnnotator.tsx | 2 +- src/client/views/SidebarAnnos.tsx | 9 +-- .../collections/CollectionMasonryViewFieldRow.tsx | 7 +- .../views/collections/CollectionNoteTakingView.tsx | 2 +- .../collections/CollectionNoteTakingViewColumn.tsx | 21 ++---- .../views/collections/CollectionStackingView.tsx | 2 +- .../CollectionStackingViewFieldColumn.tsx | 26 ++----- .../views/collections/CollectionTreeView.tsx | 2 +- src/client/views/collections/TreeView.tsx | 2 +- .../collectionFreeForm/CollectionFreeFormView.tsx | 5 +- .../collections/collectionFreeForm/MarqueeView.tsx | 2 +- .../collectionGrid/CollectionGridView.tsx | 2 +- src/client/views/nodes/DocumentView.tsx | 6 ++ src/client/views/nodes/EquationBox.tsx | 10 +-- src/client/views/nodes/LabelBox.tsx | 5 +- src/client/views/nodes/MapBox/MapBox.tsx | 2 +- src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx | 2 +- .../views/nodes/MapboxMapBox/MapboxContainer.tsx | 2 +- .../views/nodes/formattedText/DailyJournal.tsx | 25 +++---- .../views/nodes/formattedText/FormattedTextBox.tsx | 41 +++------- src/fields/Doc.ts | 4 - src/fields/RichTextField.ts | 87 +++++++++++++++++----- 24 files changed, 149 insertions(+), 141 deletions(-) (limited to 'src/client/documents/DocUtils.ts') diff --git a/src/client/documents/DocUtils.ts b/src/client/documents/DocUtils.ts index 7e0416447..9797a407b 100644 --- a/src/client/documents/DocUtils.ts +++ b/src/client/documents/DocUtils.ts @@ -385,7 +385,7 @@ export namespace DocUtils { newDoc.x = x; newDoc.y = y; newDoc[DocData].backgroundColor = Doc.UserDoc().textBackgroundColor; - Doc.SetSelectOnLoad(newDoc); + DocumentView.SetSelectOnLoad(newDoc); if (pivotField) { newDoc[pivotField] = pivotValue; } @@ -408,6 +408,7 @@ export namespace DocUtils { description: ':Daily Journal', event: undoable(() => { const newDoc = Docs.Create.DailyJournalDocument('', { x, y }); + DocumentView.SetSelectOnLoad(newDoc); docAdder?.(newDoc); }, 'Create Daily Journal'), icon: 'book', @@ -456,7 +457,7 @@ export namespace DocUtils { newDoc.author = ClientUtils.CurrentUserEmail(); newDoc.x = x; newDoc.y = y; - Doc.SetSelectOnLoad(newDoc); + DocumentView.SetSelectOnLoad(newDoc); if (pivotField) { newDoc[pivotField] = pivotValue; } diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts index b200aff65..2d342d1b1 100644 --- a/src/client/views/GlobalKeyHandler.ts +++ b/src/client/views/GlobalKeyHandler.ts @@ -23,7 +23,6 @@ import { CollectionFreeFormDocumentView } from './nodes/CollectionFreeFormDocume import { DocumentLinksButton } from './nodes/DocumentLinksButton'; import { DocumentView } from './nodes/DocumentView'; import { OpenWhereMod } from './nodes/OpenWhere'; -import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox'; import { AnchorMenu } from './pdf/AnchorMenu'; const modifiers = ['control', 'meta', 'shift', 'alt']; @@ -75,7 +74,6 @@ export class KeyManager { public handle = action((e: KeyboardEvent) => { // accumulate buffer of characters to insert in a new text note. once the note is created, it will stop keyboard events from reaching this function. - if (FormattedTextBox.SelectOnLoadChar) FormattedTextBox.SelectOnLoadChar += e.key === 'Enter' ? '\n' : e.key; const keyname = e.key && e.key.toLowerCase(); this.handleGreedy(/* keyname */); @@ -106,6 +104,10 @@ export class KeyManager { }; private unmodified = action((keyname: string, e: KeyboardEvent) => { + const nothing = { + stopPropagation: false, + preventDefault: false, + }; switch (keyname) { case 'u': if (document.activeElement?.tagName !== 'INPUT' && document.activeElement?.tagName !== 'TEXTAREA') { @@ -169,17 +171,14 @@ export class KeyManager { return { stopPropagation: true, preventDefault: true }; } break; - case 'arrowleft': return (e.target as any).type !== 'text' && this.nudge(-1, 0, 'nudge left') // if target is an input box, then we don't want to nudge any Docs since we're justing moving within the text itself. - case 'arrowright': return (e.target as any).type !== 'text' && this.nudge( 1, 0, 'nudge right'); - case 'arrowup': return (e.target as any).type !== 'text' && this.nudge(0, -1, 'nudge up'); - case 'arrowdown': return (e.target as any).type !== 'text' && this.nudge(0, 1, 'nudge down'); + case 'arrowleft': return (e.target as HTMLInputElement)?.type !== 'text' ? this.nudge(-1, 0, 'nudge left') : nothing; // if target is an input box, then we don't want to nudge any Docs since we're justing moving within the text itself. + case 'arrowright': return (e.target as HTMLInputElement)?.type !== 'text' ? this.nudge( 1, 0, 'nudge right') : nothing; + case 'arrowup': return (e.target as HTMLInputElement)?.type !== 'text' ? this.nudge(0, -1, 'nudge up') : nothing; + case 'arrowdown': return (e.target as HTMLInputElement | null)?.type !== 'text'? this.nudge(0, 1, 'nudge down'): nothing; default: } // prettier-ignore - return { - stopPropagation: false, - preventDefault: false, - }; + return nothing; }); private shift = action((keyname: string) => { diff --git a/src/client/views/MarqueeAnnotator.tsx b/src/client/views/MarqueeAnnotator.tsx index 02516264c..3f4200dce 100644 --- a/src/client/views/MarqueeAnnotator.tsx +++ b/src/client/views/MarqueeAnnotator.tsx @@ -199,7 +199,7 @@ export class MarqueeAnnotator extends ObservableReactComponent { const target = DocUtils.GetNewTextDoc('Note linked to ' + this.props.Document.title, 0, 0, 100, 100, annotationOn, 'yellow'); - Doc.SetSelectOnLoad(target); + DocumentView.SetSelectOnLoad(target); return target; }; DragManager.StartAnchorAnnoDrag([ele], new DragManager.AnchorAnnoDragData(this.props.docView(), sourceAnchorCreator, targetCreator), e.pageX, e.pageY, { diff --git a/src/client/views/SidebarAnnos.tsx b/src/client/views/SidebarAnnos.tsx index 87076bf65..816fc8ed3 100644 --- a/src/client/views/SidebarAnnos.tsx +++ b/src/client/views/SidebarAnnos.tsx @@ -3,23 +3,23 @@ import { observer } from 'mobx-react'; import * as React from 'react'; import { ClientUtils, returnAll, returnFalse, returnOne, returnZero } from '../../ClientUtils'; import { emptyFunction } from '../../Utils'; -import { Doc, DocListCast, Field, FieldType, FieldResult, StrListCast } from '../../fields/Doc'; +import { Doc, DocListCast, Field, FieldResult, FieldType, StrListCast } from '../../fields/Doc'; import { DocData } from '../../fields/DocSymbols'; import { Id } from '../../fields/FieldSymbols'; import { List } from '../../fields/List'; import { RichTextField } from '../../fields/RichTextField'; import { DocCast, NumCast, StrCast } from '../../fields/Types'; +import { DocUtils } from '../documents/DocUtils'; import { CollectionViewType, DocumentType } from '../documents/DocumentTypes'; import { Docs } from '../documents/Documents'; -import { DocUtils } from '../documents/DocUtils'; import { SearchUtil } from '../util/SearchUtil'; import { Transform } from '../util/Transform'; import { ObservableReactComponent } from './ObservableReactComponent'; import './SidebarAnnos.scss'; import { StyleProp } from './StyleProp'; import { CollectionStackingView } from './collections/CollectionStackingView'; +import { DocumentView } from './nodes/DocumentView'; import { FieldViewProps } from './nodes/FieldView'; -import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox'; interface ExtraProps { fieldKey: string; @@ -81,8 +81,7 @@ export class SidebarAnnos extends ObservableReactComponent this.addNewTextDoc('-typed text-', false, true), 'add text note'); - // addNewTextDoc is called when a user starts typing in a column to create a new node - @action - addNewTextDoc = (value: string, shiftDown?: boolean, forceEmptyNote?: boolean) => { - if (!value && !forceEmptyNote) return false; + addTextNote = undoable(() => { const key = this._props.pivotField; - const newDoc = Docs.Create.TextDocument(value, { _height: 18, _width: 200, _layout_fitWidth: true, title: value, _layout_autoHeight: true }); + const newDoc = Docs.Create.TextDocument('', { _height: 18, _width: 200, _layout_fitWidth: true, _layout_autoHeight: true }); const colValue = this.getValue(this._props.heading); newDoc[key] = colValue; - Doc.SetSelectOnLoad(newDoc); - FormattedTextBox.SelectOnLoadChar = forceEmptyNote ? '' : ' '; + DocumentView.SetSelectOnLoad(newDoc); return this._props.addDocument?.(newDoc) || false; - }; + }, 'add text note'); // deleteColumn is called when a user deletes a column using the 'trash' icon in the button area. // If the user deletes the first column, the documents get moved to the second column. Otherwise, @@ -177,7 +172,7 @@ export class CollectionNoteTakingViewColumn extends ObservableReactComponent { const key = this._props.pivotField; doc[key] = this.getValue(this._props.heading); - Doc.SetSelectOnLoad(doc); + DocumentView.SetSelectOnLoad(doc); return this._props.addDocument?.(doc); }, this._props.addDocument, @@ -249,7 +244,7 @@ export class CollectionNoteTakingViewColumn extends ObservableReactComponent
- +
diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx index 96125f2c2..72e6bb8f2 100644 --- a/src/client/views/collections/CollectionStackingView.tsx +++ b/src/client/views/collections/CollectionStackingView.tsx @@ -321,7 +321,7 @@ export class CollectionStackingView extends CollectionSubView { SnappingManager.IsDragging && (this._background = '#b4b4b4'); } // prettier-ignore @action pointerLeave = () => { this._background = 'inherit'}; // prettier-ignore - @undoBatch typedNote = () => this.addNewTextDoc('-typed text-', false, true); - - @action - addNewTextDoc = (value: string, shiftDown?: boolean, forceEmptyNote?: boolean) => { - if (!value && !forceEmptyNote) return false; + @undoBatch typedNote = () => { const key = this._props.pivotField; - const newDoc = Docs.Create.TextDocument(value, { _height: 18, _width: 200, _layout_fitWidth: true, title: value, _layout_autoHeight: true }); + const newDoc = Docs.Create.TextDocument('', { _height: 18, _width: 200, _layout_fitWidth: true, _layout_autoHeight: true }); key && (newDoc[key] = this.getValue(this._props.heading)); const maxHeading = this._props.docList.reduce((prevHeading, doc) => (NumCast(doc.heading) > prevHeading ? NumCast(doc.heading) : prevHeading), 0); const heading = maxHeading === 0 || this._props.docList.length === 0 ? 1 : maxHeading === 1 ? 2 : 3; newDoc.heading = heading; - Doc.SetSelectOnLoad(newDoc); - FormattedTextBox.SelectOnLoadChar = forceEmptyNote ? '' : ' '; + DocumentView.SetSelectOnLoad(newDoc); return this._props.addDocument?.(newDoc) || false; }; @@ -240,7 +235,7 @@ export class CollectionStackingViewFieldColumn extends ObservableReactComponent< const height = this._ele ? DivHeight(this._ele) : 0; DocUtils.addDocumentCreatorMenuItems( doc => { - Doc.SetSelectOnLoad(doc); + DocumentView.SetSelectOnLoad(doc); return this._props.addDocument?.(doc); }, this._props.addDocument, @@ -394,14 +389,7 @@ export class CollectionStackingViewFieldColumn extends ObservableReactComponent< onKeyDown={e => e.stopPropagation()} className="collectionStackingView-addDocumentButton" style={{ width: 'calc(100% - 25px)', maxWidth: this._props.columnWidth / this._props.numGroupColumns - 25, marginBottom: 10 }}> - } - menuCallback={this.menuCallback} - /> + } menuCallback={this.menuCallback} />
) : null} diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx index a60cd98ac..e93724dd4 100644 --- a/src/client/views/collections/CollectionTreeView.tsx +++ b/src/client/views/collections/CollectionTreeView.tsx @@ -174,7 +174,7 @@ export class CollectionTreeView extends CollectionSubView 0 && prev) { - Doc.SetSelectOnLoad(prev); + DocumentView.SetSelectOnLoad(prev); DocumentView.getDocumentView(prev, this.DocumentView?.())?.select(false); } return true; diff --git a/src/client/views/collections/TreeView.tsx b/src/client/views/collections/TreeView.tsx index ab4d8b060..6208905fe 100644 --- a/src/client/views/collections/TreeView.tsx +++ b/src/client/views/collections/TreeView.tsx @@ -1300,7 +1300,7 @@ export class TreeView extends ObservableReactComponent { const fieldKey = Doc.LayoutFieldKey(newParent); if (remove && fieldKey && Cast(newParent[fieldKey], listSpec(Doc)) !== undefined) { remove(child); - Doc.SetSelectOnLoad(child); + DocumentView.SetSelectOnLoad(child); TreeView._editTitleOnLoad = editTitle ? { id: child[Id], parent } : undefined; Doc.AddDocToList(newParent, fieldKey, child, addAfter, false); newParent.treeView_Open = true; diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 43addfc29..89aa53c35 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -293,7 +293,7 @@ export class CollectionFreeFormView extends CollectionSubView this.childLayoutPairs.filter(pair => this.isCurrent(pair.layout)).map(pair => pair.layout); isAnyChildContentActive = () => this._props.isAnyChildContentActive(); addLiveTextBox = (newDoc: Doc) => { - Doc.SetSelectOnLoad(newDoc); // track the new text box so we can give it a prop that tells it to focus itself when it's displayed + DocumentView.SetSelectOnLoad(newDoc); // track the new text box so we can give it a prop that tells it to focus itself when it's displayed this.addDocument(newDoc); }; selectDocuments = (docs: Doc[]) => { @@ -1496,8 +1496,7 @@ export class CollectionFreeFormView extends CollectionSubView { const text = Docs.Create.TextDocument('', { _width: 150, _height: 50 }); - Doc.SetSelectOnLoad(text); // track the new text box so we can give it a prop that tells it to focus itself when it's displayed + DocumentView.SetSelectOnLoad(text); // track the new text box so we can give it a prop that tells it to focus itself when it's displayed Doc.AddDocToList(this.Document, this._props.fieldKey, text); this.setLayoutList(this.addLayoutItem(this.savedLayoutList, this.makeLayoutItem(text, this.screenToCell(clickEv.clientX, clickEv.clientY)))); }), diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 595abc7f8..a514ee04e 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -1110,6 +1110,12 @@ export class DocumentView extends DocComponent() { public static DeselectAll: (except?: Doc) => void | undefined; public static DeselectView: (dv: DocumentView | undefined) => void | undefined; public static SelectView: (dv: DocumentView | undefined, extendSelection: boolean) => void | undefined; + + public static SelectOnLoad: Doc | undefined; + public static SetSelectOnLoad(doc?: Doc) { + DocumentView.SelectOnLoad = doc; + doc && DocumentView.addViewRenderedCb(doc, dv => dv.select(false)); + } /** * returns a list of all currently selected DocumentViews */ diff --git a/src/client/views/nodes/EquationBox.tsx b/src/client/views/nodes/EquationBox.tsx index 576b5bbe0..dcc6e27ed 100644 --- a/src/client/views/nodes/EquationBox.tsx +++ b/src/client/views/nodes/EquationBox.tsx @@ -30,12 +30,12 @@ export class EquationBox extends ViewBoxBaseComponent() { componentDidMount() { this._props.setContentViewBox?.(this); - if (Doc.SelectOnLoad === this.Document && (!DocumentView.LightboxDoc() || DocumentView.LightboxContains(this.DocumentView?.()))) { + if (DocumentView.SelectOnLoad === this.Document && (!DocumentView.LightboxDoc() || DocumentView.LightboxContains(this.DocumentView?.()))) { this._props.select(false); - this._ref.current!.mathField.focus(); - this.dataDoc.text === 'x' && this._ref.current!.mathField.select(); - Doc.SetSelectOnLoad(undefined); + this._ref.current?.mathField.focus(); + this.dataDoc.text === 'x' && this._ref.current?.mathField.select(); + DocumentView.SetSelectOnLoad(undefined); } reaction( () => this._props.isSelected(), @@ -66,7 +66,7 @@ export class EquationBox extends ViewBoxBaseComponent() { color: StrCast(this.Document.color), fontSize: this.fontSize, }); - Doc.SetSelectOnLoad(nextEq); + DocumentView.SetSelectOnLoad(nextEq); this._props.addDocument?.(nextEq); e.stopPropagation(); } diff --git a/src/client/views/nodes/LabelBox.tsx b/src/client/views/nodes/LabelBox.tsx index dcf9e1fed..7fb83571f 100644 --- a/src/client/views/nodes/LabelBox.tsx +++ b/src/client/views/nodes/LabelBox.tsx @@ -17,6 +17,7 @@ import { FieldView, FieldViewProps } from './FieldView'; import './LabelBox.scss'; import { FormattedTextBox } from './formattedText/FormattedTextBox'; import { RichTextMenu } from './formattedText/RichTextMenu'; +import { DocumentView } from './DocumentView'; @observer export class LabelBox extends ViewBoxBaseComponent() { @@ -230,8 +231,8 @@ export class LabelBox extends ViewBoxBaseComponent() { if (this._divRef) { this._divRef.addEventListener('beforeinput', this.beforeInput); - if (Doc.SelectOnLoad === this.Document) { - Doc.SelectOnLoad = undefined; + if (DocumentView.SelectOnLoad === this.Document) { + DocumentView.SetSelectOnLoad(undefined); this._divRef.focus(); } this.fitTextToBox(this._divRef); diff --git a/src/client/views/nodes/MapBox/MapBox.tsx b/src/client/views/nodes/MapBox/MapBox.tsx index 541b41bf7..792cb6b46 100644 --- a/src/client/views/nodes/MapBox/MapBox.tsx +++ b/src/client/views/nodes/MapBox/MapBox.tsx @@ -353,7 +353,7 @@ export class MapBox extends ViewBoxAnnotatableComponent() { const targetCreator = (annotationOn: Doc | undefined) => { const target = DocUtils.GetNewTextDoc('Note linked to ' + this.Document.title, 0, 0, 100, 100, annotationOn, 'yellow'); - Doc.SetSelectOnLoad(target); + DocumentView.SetSelectOnLoad(target); return target; }; const docView = this.DocumentView?.(); diff --git a/src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx b/src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx index eb0431b85..a27a8bda1 100644 --- a/src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx +++ b/src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx @@ -32,7 +32,7 @@ // addNoteClick = (e: React.PointerEvent) => { // setupMoveUpEvents(this, e, returnFalse, emptyFunction, e => { // const newDoc = Docs.Create.TextDocument('Note', { _layout_autoHeight: true }); -// Doc.SetSelectOnLoad(newDoc); // track the new text box so we can give it a prop that tells it to focus itself when it's displayed +// DocumentView.SetSelectOnLoad(newDoc); // track the new text box so we can give it a prop that tells it to focus itself when it's displayed // Doc.AddDocToList(this.props.place, 'data', newDoc); // this._stack?.scrollToBottom(); // e.stopPropagation(); diff --git a/src/client/views/nodes/MapboxMapBox/MapboxContainer.tsx b/src/client/views/nodes/MapboxMapBox/MapboxContainer.tsx index 95f89a573..0627d382e 100644 --- a/src/client/views/nodes/MapboxMapBox/MapboxContainer.tsx +++ b/src/client/views/nodes/MapboxMapBox/MapboxContainer.tsx @@ -237,7 +237,7 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent const targetCreator = (annotationOn: Doc | undefined) => { const target = DocUtils.GetNewTextDoc('Note linked to ' + this.Document.title, 0, 0, 100, 100, annotationOn, 'yellow'); - Doc.SetSelectOnLoad(target); + DocumentView.SetSelectOnLoad(target); return target; }; const docView = this.DocumentView?.(); diff --git a/src/client/views/nodes/formattedText/DailyJournal.tsx b/src/client/views/nodes/formattedText/DailyJournal.tsx index 9decbfaf0..7999357b0 100644 --- a/src/client/views/nodes/formattedText/DailyJournal.tsx +++ b/src/client/views/nodes/formattedText/DailyJournal.tsx @@ -51,22 +51,13 @@ export class DailyJournal extends ViewBoxAnnotatableComponent() @action setDailyText() { console.log('setDailyText() called...'); - const initialText = `Journal Entry - ${this.journalDate}\n\nStart writing here...`; + const placeholderText = 'Start writing here...'; + const initialText = `Journal Entry - ${this.journalDate}\n${placeholderText}`; console.log('Checking if dataDoc has text field...'); console.log('Setting new text field with:', initialText); - this.dataDoc[this.fieldKey] = new RichTextField( - JSON.stringify({ - doc: { - type: 'doc', - content: [{ type: 'paragraph', content: [{ type: 'text', text: initialText }] }], - }, - selection: { type: 'text', anchor: 1, head: 1 }, - storedMarks: [], - }), - initialText - ); + this.dataDoc[this.fieldKey] = RichTextField.textToRtf(initialText, undefined, placeholderText.length); console.log('Current text field:', this.dataDoc[this.fieldKey]); } @@ -85,10 +76,11 @@ export class DailyJournal extends ViewBoxAnnotatableComponent() } render() { - return
- - -
; + return ( +
+ +
+ ); } } @@ -99,6 +91,7 @@ Docs.Prototypes.TemplateMap.set(DocumentType.JOURNAL, { _height: 35, _xMargin: 10, _yMargin: 10, + _layout_autoHeight: true, _layout_nativeDimEditable: true, _layout_reflowVertical: true, _layout_reflowHorizontal: true, diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index c2a2caecf..83ee619d0 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -99,7 +99,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent { [key: string]: NodeViewConstructor }) { FormattedTextBox._nodeViews = nodeViews; } // prettier-ignore public static PasteOnLoad: ClipboardEvent | undefined; - public static DontSelectInitialText = false; // whether initial text should be selected or not public static SelectOnLoadChar = ''; public static LiveTextUndo: UndoManager.Batch | undefined; // undo batch when typing a new text note into a collection @@ -286,7 +285,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent { const target = DocUtils.GetNewTextDoc('Note linked to ' + this.Document.title, 0, 0, 100, 100, annotationOn); - Doc.SetSelectOnLoad(target); + DocumentView.SetSelectOnLoad(target); return target; }; @@ -1542,44 +1541,26 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent m.type !== mark.type), mark]; + let { tr } = this.EditorView.state; if (selLoadChar) { - const $from = this.EditorView.state.selection.anchor ? this.EditorView.state.doc.resolve(this.EditorView.state.selection.anchor - 1) : undefined; - const mark = schema.marks.user_mark.create({ userid: ClientUtils.CurrentUserEmail(), modified: Math.floor(Date.now() / 1000) }); - const curMarks = this.EditorView.state.storedMarks ?? $from?.marksAcross(this.EditorView.state.selection.$head) ?? []; - const storedMarks = [...curMarks.filter(m => m.type !== mark.type), mark]; const tr1 = this.EditorView.state.tr.setStoredMarks(storedMarks); - const tr2 = selLoadChar === 'Enter' ? tr1.insert(this.EditorView.state.doc.content.size - 1, schema.nodes.paragraph.create()) : tr1.insertText(selLoadChar, this.EditorView.state.doc.content.size - 1); - const tr = tr2.setStoredMarks(storedMarks); - - this.EditorView.dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(tr.doc.content.size)))); - this.tryUpdateDoc(true); // calling select() above will make isContentActive() true only after a render .. which means the selectAll() above won't write to the Document and the incomingValue will overwrite the selection with the non-updated data - } else if (!FormattedTextBox.DontSelectInitialText) { - const mark = schema.marks.user_mark.create({ userid: ClientUtils.CurrentUserEmail(), modified: Math.floor(Date.now() / 1000) }); - selectAll(this.EditorView.state, (tx: Transaction) => { - this.EditorView?.dispatch(tx.addStoredMark(mark)); - }); - this.EditorView?.dispatch(this.EditorView.state.tr.setSelection(new TextSelection(this.EditorView.state.doc.resolve(1)))); - this.tryUpdateDoc(true); // calling select() above will make isContentActive() true only after a render .. which means the selectAll() above won't write to the Document and the incomingValue will overwrite the selection with the non-updated data - } else { - const $from = this.EditorView.state.selection.anchor ? this.EditorView.state.doc.resolve(this.EditorView.state.selection.anchor - 1) : undefined; - const mark = schema.marks.user_mark.create({ userid: ClientUtils.CurrentUserEmail(), modified: Math.floor(Date.now() / 1000) }); - const curMarks = this.EditorView.state.storedMarks ?? $from?.marksAcross(this.EditorView.state.selection.$head) ?? []; - const storedMarks = [...curMarks.filter(m => m.type !== mark.type), mark]; - const { tr } = this.EditorView.state; - this.EditorView.dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(tr.doc.content.size))).setStoredMarks(storedMarks)); - this.tryUpdateDoc(true); // calling select() above will make isContentActive() true only after a render .. which means the selectAll() above won't write to the Document and the incomingValue will overwrite the selection with the non-updated data + tr = selLoadChar === 'Enter' ? tr1.insert(this.EditorView.state.doc.content.size - 1, schema.nodes.paragraph.create()) : tr1.insertText(selLoadChar, this.EditorView.state.doc.content.size - 1); } + this.EditorView.dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(tr.doc.content.size))).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(); } if (this._props.isContentActive()) this.prepareForTyping(); diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts index bdd41b0bb..805bb4526 100644 --- a/src/fields/Doc.ts +++ b/src/fields/Doc.ts @@ -468,10 +468,6 @@ export class Doc extends RefField { } } export namespace Doc { - export let SelectOnLoad: Doc | undefined; - export function SetSelectOnLoad(doc: Doc | undefined) { - SelectOnLoad = doc; - } export let DocDragDataName: string = ''; export function SetDocDragDataName(name: string) { DocDragDataName = name; diff --git a/src/fields/RichTextField.ts b/src/fields/RichTextField.ts index dc636031a..4b1403b88 100644 --- a/src/fields/RichTextField.ts +++ b/src/fields/RichTextField.ts @@ -48,26 +48,77 @@ export class RichTextField extends ObjectField { '' ); } + static Initialize = (initial?: string) => { + const content: object[] = []; + const state = { + doc: { + type: 'doc', + content, + }, + selection: { + type: 'text', + anchor: 0, + head: 0, + }, + }; + if (initial && initial.length) { + content.push({ + type: 'paragraph', + content: { + type: 'text', + text: initial, + }, + }); + state.selection.anchor = state.selection.head = initial.length + 1; + } + return JSON.stringify(state); + }; + + private static ToProsemirrorState = (plainText: string, selectBack?: number, delimeter = '\n') => { + // Remap the text, creating blocks split on newlines + const elements = plainText.split(delimeter); + + // Google Docs adds in an extra carriage return automatically, so this counteracts it + !elements[elements.length - 1].length && elements.pop(); + + // Preserve the current state, but re-write the content to be the blocks + const parsed: Record = JSON.parse(RichTextField.Initialize()); + (parsed.doc as Record).content = elements.map(text => { + const paragraph: object = { + type: 'paragraph', + content: text.length ? [{ type: 'text', marks: [], text }] : undefined, // An empty paragraph gets treated as a line break + }; + return paragraph; + }); - public static textToRtf(text: string, imgDocId?: string) { + // If the new content is shorter than the previous content and selection is unchanged, may throw an out of bounds exception, so we reset it + parsed.selection = { type: 'text', anchor: 2 + plainText.length - (selectBack ?? 0), head: 2 + plainText.length }; + + // Export the ProseMirror-compatible state object we've just built + return JSON.stringify(parsed); + }; + + public static textToRtf(text: string, imgDocId?: string, selectBack?: number) { return new RichTextField( - JSON.stringify({ - // this is a RichText json that has the question text placed above a related image - doc: { - type: 'doc', - content: [ - { - type: 'paragraph', - attrs: { align: 'center', color: null, id: null, indent: null, inset: null, lineSpacing: null, paddingBottom: null, paddingTop: null }, - content: [ - ...(text ? [{ type: 'text', text }] : []), // - ...(imgDocId ? [{ type: 'dashDoc', attrs: { width: '200px', height: '200px', title: 'dashDoc', float: 'unset', hidden: false, docId: imgDocId } }] : []), - ], - }, - ], - }, - selection: { type: 'text', anchor: 2, head: 2 }, - }), + !imgDocId + ? this.ToProsemirrorState(text, selectBack) + : JSON.stringify({ + // this is a RichText json that has the question text placed above a related image + doc: { + type: 'doc', + content: [ + { + type: 'paragraph', + attrs: { align: 'center', color: null, id: null, indent: null, inset: null, lineSpacing: null, paddingBottom: null, paddingTop: null }, + content: [ + ...(text ? [{ type: 'text', text }] : []), // + ...(imgDocId ? [{ type: 'dashDoc', attrs: { width: '200px', height: '200px', title: 'dashDoc', float: 'unset', hidden: false, docId: imgDocId } }] : []), + ], + }, + ], + }, + selection: { type: 'text', anchor: 2 + text.length, head: 2 + text.length }, + }), text ); } -- cgit v1.2.3-70-g09d2 From 1b1dd47cd640eced707840f306ab71861a53fb8b Mon Sep 17 00:00:00 2001 From: bobzel Date: Fri, 7 Mar 2025 11:05:47 -0500 Subject: made daily journal a tool and removed unneeded explicit mention of it in ':' menu --- src/client/documents/DocUtils.ts | 13 ------------- src/client/util/CurrentUserUtils.ts | 5 ++++- 2 files changed, 4 insertions(+), 14 deletions(-) (limited to 'src/client/documents/DocUtils.ts') diff --git a/src/client/documents/DocUtils.ts b/src/client/documents/DocUtils.ts index 9797a407b..42ccd0810 100644 --- a/src/client/documents/DocUtils.ts +++ b/src/client/documents/DocUtils.ts @@ -403,19 +403,6 @@ export namespace DocUtils { icon: 'file', }); - // AARAV ADD // - documentList.push({ - description: ':Daily Journal', - event: undoable(() => { - const newDoc = Docs.Create.DailyJournalDocument('', { x, y }); - DocumentView.SetSelectOnLoad(newDoc); - docAdder?.(newDoc); - }, 'Create Daily Journal'), - icon: 'book', - }); - - // AARAV ADD // - ContextMenu.Instance.addItem({ description: 'Create document', subitems: documentList, diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index 8736225d0..ce875ff6c 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -402,7 +402,9 @@ pie title Minerals in my tap water {key: "WebCam", creator: opts => Docs.Create.WebCamDocument("", opts), opts: { _width: 400, _height: 200, recording:true, isSystem: true, cloneFieldFilter: new List(["isSystem"]) }}, {key: "Button", creator: Docs.Create.ButtonDocument, opts: { _width: 150, _height: 50, _xPadding: 10, _yPadding: 10, title_custom: true, waitForDoubleClickToClick: 'never'}, scripts: {onClick: FollowLinkScript()?.script.originalScript ?? ""}}, {key: "Script", creator: opts => Docs.Create.ScriptingDocument(null, opts), opts: { _width: 200, _height: 250, }}, - {key: "DataViz", creator: opts => Docs.Create.DataVizDocument("", opts), opts: { _width: 300, _height: 300 }}, + {key: "DataViz", creator: opts => Docs.Create.DataVizDocument("", opts), opts: { _width: 300, _height: 300, }}, + // AARAV ADD // + {key: "DailyJournal",creator:opts => Docs.Create.DailyJournalDocument("", opts),opts:{ _width: 300, _height: 300}}, {key: "Chat", creator: Docs.Create.ChatDocument, opts: { _width: 500, _height: 500, _layout_fitWidth: true, }}, {key: "Header", creator: headerTemplate, opts: { _width: 300, _height: 120, _header_pointerEvents: "all", _header_height: 50, _header_fontSize: 9,_layout_autoHeightMargins: 50, _layout_autoHeight: true, treeView_HideUnrendered: true}}, {key: "ViewSlide", creator: slideView, opts: { _width: 400, _height: 300, _xMargin: 3, _yMargin: 3,}}, @@ -441,6 +443,7 @@ pie title Minerals in my tap water { toolTip: "Tap or drag to create a button", title: "Button", icon: "circle", dragFactory: doc.emptyButton as Doc, clickFactory: DocCast(doc.emptyButton)}, { toolTip: "Tap or drag to create a scripting box", title: "Script", icon: "terminal", dragFactory: doc.emptyScript as Doc, clickFactory: DocCast(doc.emptyScript), funcs: { hidden: "IsNoviceMode()"}}, { toolTip: "Tap or drag to create a data viz node", title: "DataViz", icon: "chart-bar", dragFactory: doc.emptyDataViz as Doc, clickFactory: DocCast(doc.emptyDataViz)}, + { toolTip: "Tap or drag to create a journal entry", title: "Journal", icon: "note", dragFactory: doc.emptyDailyJournal as Doc,clickFactory:DocCast(doc.emptyDataJournal), }, { toolTip: "Tap or drag to create a bullet slide", title: "PPT Slide", icon: "person-chalkboard", dragFactory: doc.emptySlide as Doc, clickFactory: DocCast(doc.emptySlide), openFactoryLocation: OpenWhere.overlay,funcs: { hidden: "IsNoviceMode()"}}, { toolTip: "Tap or drag to create a view slide", title: "View Slide", icon: "address-card", dragFactory: doc.emptyViewSlide as Doc, clickFactory: DocCast(doc.emptyViewSlide), openFactoryLocation: OpenWhere.overlay,funcs: { hidden: "IsNoviceMode()"}}, { toolTip: "Tap or drag to create a data note", title: "DataNote", icon: "window-maximize", dragFactory: doc.emptyHeader as Doc, clickFactory: DocCast(doc.emptyHeader), openFactoryAsDelegate: true, funcs: { hidden: "IsNoviceMode()"} }, -- cgit v1.2.3-70-g09d2 From 82ba2c85e22fb809f1a5fba827c73555db0e4cd9 Mon Sep 17 00:00:00 2001 From: bobzel Date: Fri, 7 Mar 2025 13:46:59 -0500 Subject: fixed columnWidth settings for sidebar. fixed color picker and other dropdowns to toggle when button is clicked. fixed dash field views to collapse properly. fixed rtf to honor marks that were set before text box is created and typing starts. --- .../src/components/ColorPicker/ColorPicker.tsx | 336 +++++++++------------ .../src/components/IconButton/IconButton.tsx | 280 +++++++++-------- packages/components/src/components/Popup/Popup.tsx | 61 ++-- src/client/documents/DocUtils.ts | 4 +- src/client/documents/Documents.ts | 3 +- src/client/util/CurrentUserUtils.ts | 8 +- .../collectionSchema/SchemaTableCell.tsx | 2 +- .../views/nodes/FontIconBox/FontIconBox.scss | 20 +- src/client/views/nodes/FontIconBox/FontIconBox.tsx | 5 +- .../views/nodes/formattedText/DashFieldView.tsx | 4 +- .../views/nodes/formattedText/FormattedTextBox.tsx | 3 +- 11 files changed, 340 insertions(+), 386 deletions(-) (limited to 'src/client/documents/DocUtils.ts') diff --git a/packages/components/src/components/ColorPicker/ColorPicker.tsx b/packages/components/src/components/ColorPicker/ColorPicker.tsx index 51b820a37..632b470f0 100644 --- a/packages/components/src/components/ColorPicker/ColorPicker.tsx +++ b/packages/components/src/components/ColorPicker/ColorPicker.tsx @@ -1,204 +1,156 @@ -import React, { useState } from 'react' -import { GithubPicker, ChromePicker, BlockPicker, SliderPicker, SketchPicker } from 'react-color' -import { IGlobalProps, Size, Type , getFormLabelSize } from '../../global' -import { Button } from '../Button' -import { IconButton } from '../IconButton' -import { Popup, PopupTrigger } from '../Popup' -import './ColorPicker.scss' -import { Dropdown, DropdownType } from '../Dropdown' +import React, { useRef, useState } from 'react'; +import { GithubPicker, ChromePicker, BlockPicker, SliderPicker, SketchPicker } from 'react-color'; +import { IGlobalProps, Size, Type, getFormLabelSize } from '../../global'; +import { Button } from '../Button'; +import { IconButton } from '../IconButton'; +import { Popup, PopupTrigger } from '../Popup'; +import './ColorPicker.scss'; +import { Dropdown, DropdownType } from '../Dropdown'; -export const ColorPickerArray= ["Classic", "Chrome", "GitHub", "Block", "Slider"] -export type ColorPickerType= typeof ColorPickerArray[number]; +export const ColorPickerArray = ['Classic', 'Chrome', 'GitHub', 'Block', 'Slider']; +export type ColorPickerType = (typeof ColorPickerArray)[number]; export interface IColorPickerProps extends IGlobalProps { - text?: string - icon?: JSX.Element | string - colorPickerType?: ColorPickerType - defaultPickerType?: ColorPickerType - selectedColor?: string - setSelectedColor: (color: any) => unknown - setFinalColor: (color:any) => unknown + text?: string; + icon?: JSX.Element | string; + colorPickerType?: ColorPickerType; + defaultPickerType?: ColorPickerType; + selectedColor?: string; + setSelectedColor: (color: any) => unknown; + setFinalColor: (color: any) => unknown; } export const ColorPicker = (props: IColorPickerProps) => { - const [selectedColorLoc, setSelectedColorLoc] = useState(); - const { defaultPickerType, text, colorPickerType, fillWidth, formLabelPlacement, size = Size.SMALL, type = Type.TERT, icon, selectedColor = selectedColorLoc, setSelectedColor = setSelectedColorLoc, setFinalColor = setSelectedColorLoc, tooltip, color='black', formLabel } = props - const [isOpen, setOpen] = useState(false) - const [pickerSelectorOpen, setPickerSelectorOpen] = useState(false); - const decimalToHexString = (number: number) => { - if (number < 0) { - number = 0xffffffff + number + 1; - } - return (number < 16 ? '0' : '') + number.toString(16).toUpperCase(); -} - const colorString = (color: any ) => { - return color.hex === 'transparent' ? color.hex: color.hex + (color.rgb.a ? decimalToHexString(Math.round(color.rgb.a * 255)) : 'ff'); - } - const onChange = (color: any) => { - setSelectedColor(colorString(color) as any); - } - const onChangeComplete = (color: any) => { - setFinalColor(colorString(color) as any); - } - const [picker, setPicker] = useState(defaultPickerType ?? "Classic") + const [selectedColorLoc, setSelectedColorLoc] = useState(); + const { + defaultPickerType, + text, + colorPickerType, + fillWidth, + formLabelPlacement, + size = Size.SMALL, + type = Type.TERT, + icon, + selectedColor = selectedColorLoc, + setSelectedColor = setSelectedColorLoc, + setFinalColor = setSelectedColorLoc, + tooltip, + color = 'black', + formLabel, + } = props; + const [isOpen, setOpen] = useState(false); + const [pickerSelectorOpen, setPickerSelectorOpen] = useState(false); + const decimalToHexString = (number: number) => { + if (number < 0) { + number = 0xffffffff + number + 1; + } + return (number < 16 ? '0' : '') + number.toString(16).toUpperCase(); + }; + const colorString = (color: any) => { + return color.hex === 'transparent' ? color.hex : color.hex + (color.rgb.a ? decimalToHexString(Math.round(color.rgb.a * 255)) : 'ff'); + }; + const onChange = (color: any) => { + setSelectedColor(colorString(color) as any); + }; + const onChangeComplete = (color: any) => { + setFinalColor(colorString(color) as any); + }; + const [picker, setPicker] = useState(defaultPickerType ?? 'Classic'); - const getToggle = () => { - if (icon && !text) { - return ( - - ) - } else if (text) { - return ( - - (this._showCode = !this._showCode))} /> + + (this._showCode = !this._showCode))} /> +
{this._showCode ? ( diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx index aab8a183a..47c5734f7 100644 --- a/src/client/views/nodes/DocumentContentsView.tsx +++ b/src/client/views/nodes/DocumentContentsView.tsx @@ -23,7 +23,6 @@ interface HTMLtagProps { htmltag: string; onClick?: ScriptField; onInput?: ScriptField; - scaling: number; children?: JSX.Element[]; } @@ -43,7 +42,7 @@ interface HTMLtagProps { export class HTMLtag extends React.Component { click = () => { const clickScript = this.props.onClick as Opt; - clickScript?.script.run({ this: this.props.Document, scale: this.props.scaling }); + clickScript?.script.run({ this: this.props.Document }); }; onInput = (e: React.FormEvent) => { const onInputScript = this.props.onInput as Opt; @@ -56,7 +55,6 @@ export class HTMLtag extends React.Component { 'dragStarting', 'dragEnding', 'htmltag', - 'scaling', 'Document', 'key', 'onInput', @@ -65,7 +63,7 @@ export class HTMLtag extends React.Component { ]).omit; const replacer = (match: string, expr: string) => // bcz: this executes a script to convert a property expression string: { script } into a value - (ScriptField.MakeFunction(expr, { this: Doc.name, scale: 'number' })?.script.run({ this: this.props.Document, scale: this.props.scaling }).result as string) || ''; + (ScriptField.MakeFunction(expr, { this: Doc.name })?.script.run({ this: this.props.Document }).result as string) || ''; Object.keys(divKeys).forEach((prop: string) => { const p = (this.props as unknown as { [key: string]: string })[prop] as string; style[prop] = p?.replace(/{([^.'][^}']+)}/g, replacer); @@ -166,12 +164,11 @@ export class DocumentContentsView extends ObservableReactComponent[^{]*)[^=]\{([^.'][^<}]+)\}([^}]*<)/g, replacer); // replace HTML with corresponding HTML tag as in: becomes - const replacer2 = (match: string, p1: string) => ` ` with as in: becomes const replacer3 = (/* match: any, p1: string, offset: any, string: any */) => `; } // prettier-ignore @computed get showCaption() { return this.style(this.layoutDoc, StyleProp.ShowCaption) as string ?? ""; } // prettier-ignore @computed get headerMargin() { return this.style(this.layoutDoc, StyleProp.HeaderMargin) as number ?? 0; } // prettier-ignore diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index 017ef7191..5b06e9fc5 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -148,9 +148,9 @@ export class ImageBox extends ViewBoxAnnotatableComponent() { ); const { layoutDoc } = this; this._disposers.path = reaction( - () => ({ nativeSize: this.nativeSize, width: NumCast(this.layoutDoc._width) }), - ({ nativeSize, width }) => { - if ((layoutDoc === this.layoutDoc && !this.layoutDoc._layout_nativeDimEditable) || !this.layoutDoc._height) { + () => ({ nativeSize: this.nativeSize, width: NumCast(this.layoutDoc._width), height: this.layoutDoc._height }), + ({ nativeSize, width, height }) => { + if ((layoutDoc === this.layoutDoc && !this.layoutDoc._layout_nativeDimEditable) || !height) { this.layoutDoc._height = (width * nativeSize.nativeHeight) / nativeSize.nativeWidth; } }, @@ -834,14 +834,10 @@ export class ImageBox extends ViewBoxAnnotatableComponent() { const file = input.files?.[0]; if (file) { const disposer = OverlayView.ShowSpinner(); - const [{ result }] = await Networking.UploadFilesToServer({ file }); - if (result instanceof Error) { - alert('Error uploading files - possibly due to unsupported file types'); - } else { - this.dataDoc[this.fieldKey] = new ImageField(result.accessPaths.agnostic.client); - !(result instanceof Error) && DocUtils.assignImageInfo(result, this.dataDoc); - } - disposer(); + DocUtils.uploadFileToDoc(file, {}, this.Document).then(doc => { + disposer(); + doc && (doc.height = undefined); + }); } else { console.log('No file selected'); } diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts index dc4a5a011..fc89dcbe7 100644 --- a/src/fields/Doc.ts +++ b/src/fields/Doc.ts @@ -1517,7 +1517,6 @@ export namespace Doc { case DocumentType.MAP: return 'map-marker-alt'; case DocumentType.DATAVIZ: return 'chart-bar'; case DocumentType.EQUATION: return 'calculator'; - case DocumentType.SIMULATION: return 'rocket'; case DocumentType.CONFIG: return 'folder-closed'; default: } -- cgit v1.2.3-70-g09d2